Thunderstore packaging and releases
Markdown options
The public product is Megabonk Level Editor; its package identifier is
Megabonk_Level_Editor. The repository remains megabonk_mods, the assembly is
MegabonkLevels.Mod.dll and the stable BepInEx GUID is megabonklevels.runtime.
The team namespace is configured in the root thunderstore.toml.
Package source and build
Section titled “Package source and build”pkg/ contains the package’s player-facing README, reference manifest, icon and
NLayer license. The root README describes the repository, not the package page.
The 256×256 icon is a rasterization of site/public/favicon.svg; regenerate it
with rsvg-convert -w 256 -h 256 -o pkg/icon.png site/public/favicon.svg.
From the repository root:
task package:thunderstoretask package:thunderstore:checkThe build installs tcli 0.2.4 locally in .tools/tcli/ if missing, builds the
Release DLLs (with Task’s .env loading), runs package tests, invokes tcli build
and validates the resulting .tools/thunderstore/<team>-Megabonk_Level_Editor-<version>.zip.
It never publishes. After CLI installation and a Release build,
.tools/tcli/tcli build also builds the ZIP directly.
tcli generates the ZIP’s manifest.json from thunderstore.toml; pkg/manifest.json
is a reviewable reference, checked against that generated manifest. tcli adds
namespace and FullName fields; the ZIP checker permits those only with the
configured identity. Update both metadata sources and the BepInPlugin display
version together for each release.
The ZIP has exactly these files, with no enclosing directory:
manifest.jsonREADME.mdicon.pngBepInEx/plugins/MegabonkLevels/MegabonkLevels.Mod.dllBepInEx/plugins/MegabonkLevels/NLayer.dllBepInEx/plugins/MegabonkLevels/NLayer.LICENSE.txtThe dependency is BepInEx-BepInExPack_IL2CPP-6.0.788. The package does not
redistribute BepInEx, its .NET runtime, Harmony, generated game/Unity interop,
game assets, PDBs, installers or developer test data. The ZIP checker,
scripts/check_thunderstore_package.py, rejects unexpected files and DLLs that
differ from the current Release output.
The older package:windows, package:linux and package:macos tasks describe
the legacy MelonLoader installer format and are internal: Task hides them from
listings and rejects direct CLI invocation. Do not expose a wrapper that combines
them with this BepInEx DLL. Rebuilding legacy packages requires a separately
retained legacy checkout, not the public BepInEx source history.
Shared release version
Section titled “Shared release version”release-please-config.json defines one root release, with Git tags such as
v0.1.0. The Python release strategy updates pyproject.toml; targeted
extra-files keep the other product-version fields synchronized:
- the root project entry in
uv.lock, selected by name rather than array index - the mod project’s
<Version>and the BepInPlugin display version - Thunderstore’s
versionNumberand the reference manifest’sversion_number - the site’s
package.jsonand both root version fields inpackage-lock.json
The Go installer has no embedded product-version field: its source follows the
same Git tag. Do not change the go toolchain directive to a product version.
Dependencies, game builds, level/pack schemas and the standalone boot spike have
their own versions and are not part of this synchronization.
Keep the plugin’s inline x-release-please-version annotation; the generic
updater uses it to change only that line. The uv.lock selector accepts both
plain names and the tagged-value objects used by Release Please’s TOML parser,
without updating third-party package versions. Two JSON updaters target the npm
lockfile’s top-level version and packages[""].version; no dependency entries
are selected. Version examples in player documentation use placeholders so they
do not become stale on the next release.
First release and subsequent bumps
Section titled “First release and subsequent bumps”.release-please-manifest.json is Release Please’s current/last-released version
ledger, not the requested next version. It is bootstrapped at 0.1.0; the
temporary "release-as": "0.1.0" override ensures the first release PR targets
exactly that version instead of deriving a higher one from existing commits.
Remove the override after the first release PR is merged and the GitHub release
exists. Otherwise later runs remain pinned at 0.1.0. The option is supported
but deprecated upstream; use it only for this bootstrap, not normal releases.
After that, let conventional commits and Release Please advance the ledger and all version files together. Do not independently release the website or add separate language packages to the manifest unless their release lifecycles are intentionally being split.
.github/workflows/release-please.yml runs on pushes to main. It reads the
repository Actions secret RELEASE_PLEASE_TOKEN with contents and pull-request
write permissions. The optional placeholder in .env.example is for local
tooling only: Actions does not read .env. Keep the real token out of Git,
command arguments and logs. This workflow creates release PRs and GitHub
releases; it does not publish to Thunderstore, npm, NuGet or PyPI, build a
player package, or deploy the website. A push that adds the workflow can start
release automation, so review the configuration before pushing.
Check current version consistency with:
uv run pytest tests/test_release_versions.py -n 0For the optional offline future-bump integration test, install the upstream updaters locally first; this does not contact GitHub or create a release:
npm install --prefix .tools/release-please --no-save --ignore-scripts release-please@17.11.2uv run pytest tests/test_release_versions.py -n 0The integration test applies Release Please’s actual updaters and composite update handling to temporary copies at a different version, then checks that all product fields moved and dependency/toolchain versions did not.
Dependency updates
Section titled “Dependency updates”.github/renovate.json5 groups npm, NuGet, Go modules and Python dependencies.
Python uses pep621 for pyproject.toml/uv.lock and an explicit pep723 file
pattern for inline script metadata under scripts/. There is no Cargo project,
so no Rust rule is configured. Other default Renovate managers are not disabled.
The schedule permits new update branches during Saturday 10–11 AM America/Chicago, including lockfile maintenance. It is an allowed window, not a guarantee that the bot will run at exactly 10:00. Validate without running dependency updates:
npx --yes --package renovate@44.222.5 -- renovate-config-validator --strict .github/renovate.json5Validate a manifest offline
Section titled “Validate a manifest offline”scripts/validate_thunderstore_manifest.py reimplements
the site’s Manifest Validator,
which needs a logged-in account and a team you belong to. It ports
ManifestV1Serializer and the Django 4.2 / DRF validators behind it, including their
message text, so a line printed here matches the line the site prints.
uv run scripts/validate_thunderstore_manifest.py # pkg/manifest.jsonuv run scripts/validate_thunderstore_manifest.py --online # plus thunderstore.iouv run scripts/validate_thunderstore_manifest.py .tools/thunderstore/<team>-<package>.ziptask package:thunderstore:manifest # same, via Tasktask package:thunderstore:manifest ONLINE=trueIt takes a manifest.json or a built package ZIP, and the team defaults to
thunderstore.toml’s [package].namespace; --team picks another, --json emits
machine-readable output. Exit 1 means the manifest would be rejected, exit 2 means the
input could not be read. task package:thunderstore runs it on pkg/manifest.json
before building, and task package:thunderstore:check runs it on the built ZIP.
Offline it checks everything the serializer can decide from the document alone: required
fields, the name and version patterns and length caps, a version that is not written in
its canonical form, the URL rules, dependency strings, self-dependency, duplicate
dependencies, the dependency and installer list caps, installer identifier shape and
reuse, and the team name’s shape. The four checks that read the site’s database run only
with --online, and only once the offline rules pass – as on the site, where they are
serializer-level checks that valid fields must reach:
- each dependency resolves to a published package version
- this
namespace-name-versionis not already published - no package exists with the same name in a different capitalization
- installer identifiers exist, which stays a note because that table is not public
--online uses the public read-only JSON API through scripts/asset_download.py, so no
token is involved: /api/experimental/package/<ns>/<name>/<version>/ and
/api/cyberstorm/listing/<community>/<ns>/ for the team’s package list. The
api/v1/package/... detail paths return 404 for the same package, so those two are
deliberate. An unreachable host is a note, not a failure.
Behaviours worth knowing before debugging a disagreement with the site:
- Extra keys are ignored, which is why tcli’s
namespaceandFullNameare harmless. PackageReference.parsetakes the version and then the name from the right, soDash-Ed-Team-Mythic-TestMod-1.1.0is one reference and a version with two dots is mandatory.- A bare
mailto:address is rejected whilemailto://anythingpasses, andipfs://needs something that looks like a host:ipfs://CIDfails.
Content and licensing
Section titled “Content and licensing”The Thunderstore package is runtime-only: no levels, third-party packs or
music are included. In particular, extracted THPS assets and music must not
be published with it. Screenshots in the README are publicly hosted links, not
bundled game assets. NLayer 1.16.0 is redistributed under MIT with its upstream
license notice preserved in pkg/NLayer.LICENSE.txt.
The repository currently has no project-wide license file. Packaging does not choose a code/content license on the maintainer’s behalf. Confirm the intended release terms and rights to any newly added content before first publication; adding a level or pack is a separate licensing and manager-layout review.
Test the final ZIP, not the build folder
Section titled “Test the final ZIP, not the build folder”- Quit the game. Back up the manager’s settings/profile metadata and inventory the affected game files. Preserve the existing profile and manual development DLLs outside the manager’s sync tree.
- Create a new Megabonk profile with only the declared BepInEx dependency. Do not reuse a development profile or another plugin’s configuration files.
- Import the built ZIP with the manager’s local/custom mod import, then Apply. Do not copy the plugin DLL manually as a substitute for this test.
- Check that the installed plugin and NLayer hashes match the ZIP, and that only
one copy of
megabonklevels.runtimecan load. The manager may wrap or flatten the package’s plugin subfolder; inspect the actual installed paths. - Launch and check
BepInEx/LogOutput.logforLoading [Megabonk Level Editor <version>],MegabonkLevels mod initialized.andChainloader startup complete. Check errors associated with this plugin, not just the chainloader’s ownErrorLog.log. - Confirm the console opens and a separately supplied, pack-free level appears
on Map Selection and plays. Enter the editor, make and undo an edit, and save
a copy. No mod-specific BepInEx config is expected; developer tools require
MEGABONK_DEV=truein the launch environment. - Review
pkg/README.mdusing Thunderstore’s Markdown Preview, including both externally hosted screenshots. Validate the generated manifest withtask package:thunderstore:manifest ONLINE=true, and confirm it with the Manifest Validator, which is the final word. The screenshots use the public showcase site, not private-repository raw URLs. Recheck the editor image URL after a website rebuild changes its asset hash. - Record the ZIP SHA-256, profile, installed hashes, log evidence and screenshots. Rebuild and repeat the import test if any packaged file changes afterward.
On the tested macOS setup, r2modmac 1.1.0 manages files but does not successfully
launch the Windows game through plain Wine. Use the existing Wine launch path.
Its Apply omits the loader pack’s dotnet/, .doorstop_version and changelog.txt;
supply these from the dependency ZIP, not from this mod package. Set the prefix’s
winhttp=native,builtin override (the repo launcher passes it in the environment).
Park the MelonLoader version.dll; both proxies must not be active together.
See Running BepInEx under Wine.
For an isolated test game outside the Wine prefix, inspect the manager-written
doorstop_config.ini before launching. r2modmac may write a host path such as
\Users\... without a Windows drive letter. Back up that file, then use paths
relative to the game folder instead:
[General]target_assembly=BepInEx\core\BepInEx.Unity.IL2CPP.dll
[UnityMono]dll_search_path_override=BepInEx\coreKeep the existing sections and other settings, including the relative
dotnet\coreclr.dll runtime path. Re-Apply may overwrite these changes. Launch
with the game folder as the working directory and the test copy’s executable
explicitly; the repo’s current plain-Wine launcher still names the default Steam
executable even when MEGABONK_GAME_DIR changes its working/log directory.
Use an explicit Z:\...\Megabonk.exe path for a host-side test copy, and point
MEGABONK_GAME_DIR at that same folder for harness commands. Do not mistake a
successful launch of the development install for a manager-install test.
The manager’s sync is bidirectional: deleting installed mod files can also remove them from its profile at the next sync. Restore backups with the manager stopped; do not delete an installed tree to simulate disabling a mod.
With r2modmac 1.1.0, local ZIP imports appear as
Local-Megabonk_Level_Editor-<version>. Apply preserves the plugin’s nested folder:
BepInEx/plugins/Local-Megabonk_Level_Editor-<version>/MegabonkLevels/ contains the
DLL, NLayer and its license. That layout loads correctly; do not flatten it by
hand. A clean-profile install was verified under Wine 11.18 with the omitted
runtime supplied from the dependency ZIP and relative Doorstop paths. Console,
pack-free level loading/gameplay, terrain editing, undo and save-copy all work.
Release gate and publishing
Section titled “Release gate and publishing”Before the first publish:
- Use the BepInEx runtime on main. Legacy MelonLoader rollback source lives only in the maintainer’s private history backup; it is not a public download or tag. Preserve local loader backups when testing a cutover or rollback.
- Confirm the team’s service account has package-upload permission. Read Global Rules and any Megabonk-specific rules. Do not distribute game assemblies or unlicensed assets.
- Confirm all linked documentation and feedback destinations are publicly accessible. A private GitHub repository’s source/docs/issues links do not work for players without access; make them public or replace them before release.
- Complete the clean-profile ZIP test, manifest validation and README preview. Record rights/licensing decisions and the exact tested archive hash.
- Confirm community
megabonk, categoriesmodsandtools, and NSFW false for every upload. The category API currently exposes no AI Generated category; check again before uploading and apply it if it becomes available and applies. - Obtain explicit publication approval. Building a package is not approval to upload it or to merge the cutover.
Create a team service account, not a personal token, at Thunderstore’s team
settings. Supply its token only through TCLI_AUTH_TOKEN in the shell environment
or a gitignored local environment file. Never put it in TOML, command arguments,
task notes or logs. After approval, validate and publish the already tested ZIP:
task package:thunderstore:check.tools/tcli/tcli publish --file .tools/thunderstore/<team>-Megabonk_Level_Editor-<version>.zipDo not use bare tcli publish: it rebuilds by default and could upload a ZIP that
was not tested. Recheck the stored SHA-256 immediately before the upload.
After publishing, verify the page’s title, icon, screenshots, dependency, category
and NSFW settings, then install the published version in another clean profile.
That last download check cannot be performed before publication.
Package versions are immutable. Updates require a higher version with the same team/name identity. Retain superseded releases; after a soak period and approval, deprecate, never delete an obsolete package. Do not automatically deprecate anything during the initial upload.
Rollback
Section titled “Rollback”Quit the game and disable the package in the manager; Apply and verify the plugin
is no longer in the active plugin tree. Keep user-authored UserData/ files.
For a loader rollback, park winhttp.dll and doorstop_config.ini, restore the
preserved MelonLoader version.dll, and deploy the DLL from the final legacy tag,
not this BepInEx package. Leave inactive loader trees intact. Reverse the proxy
renames to return to BepInEx. Restore the original manager profile/settings and
manual development artifacts from the pre-test inventory if required.