Skip to content

Releasing

Versions, branches and releases. How the project is built: Development.

In short

  1. Work on rc (or on branches merged into rc), noting changes under ## [Unreleased] in CHANGELOG.md.
  2. With the first release candidate, on rc, set gradle.properties to the next version: mod_version=0.5.0.
  3. Whenever you want testers to try rc, publish a release candidate: Actions tab → Release candidate → Run workflow, Use workflow from: rc (or gh workflow run rc.yml --ref rc). It runs CI's checks and puts 0.5.0-rc+<commit> on Modrinth as Alpha, replacing the previous candidate. Repeat as often as you like.
  4. When ready, on rc, in CHANGELOG.md: under the empty ## [Unreleased], add ## [0.5.0] - 2026-10-15 with what changed (square brackets and this exact format: the release notes are taken from it). Optionally publish a last candidate of this commit: then the release pull request doesn't rebuild it.
  5. Optionally run the Game tests workflow on rc (Actions tab → Game tests → Run workflow, Use workflow from: rc): CI doesn't play them.
  6. Open a pull request from rc into main, wait for CI (its release check confirms the new version), merge.
  7. Done. GitHub creates release/0.5.0, the tag v0.5.0-fabload0.19.5+api0.162.0-mc26.3 and the release (a pre-release, Beta on Modrinth: promote it by hand) with the jar, the .mrpack, the Default Content datapack and its resource pack, then uploads the jar to Modrinth (about a minute: Actions tab, then Releases) and deletes the release candidate from Modrinth; then it adds the Pack Editor's portable apps for Windows, macOS and Linux (about ten more minutes).
  8. rc is brought up to date with main automatically; locally, run git pull on rc.

The sections below explain each part.

Version numbers

MineDnD uses Semantic Versioning: MAJOR.MINOR.PATCH.

Change Bump Example
Bug fixes only PATCH 0.0.1 → 0.0.2
New features, new datapack formats, balance changes MINOR 0.0.2 → 0.1.0
Changes that break existing worlds or datapacks (after 1.0.0) MAJOR 1.4.2 → 2.0.0

While the version starts with 0., anything can still change between minor versions.

The version lives in one place: mod_version in gradle.properties. It flows into the jar name (MineDnD-<version>.jar), fabric.mod.json (shown in the mods list) and the modpack (MineDnD-<version>.mrpack).

Branches and tags

Name What it is
main What's released. Every merge into main is a release: pull requests into it come from rc and carry a new version.
rc Release candidate: where changes collect and get tested (installToModrinth, playtests, release candidates on Modrinth) before a release.
feature/..., fix/..., chore/... Short-lived work branches, merged into rc by pull request (or commit straight on rc).
release/X.Y.Z Frozen copy of a released version, to fix it later without taking new features. Created automatically.
tag vX.Y.Z-fabloadL+apiA-mcM The exact commit that was released, e.g. v0.4.2-fabload0.19.5+api0.162.0-mc26.3: mod version, Fabric Loader version (loader_version), Fabric API version (fabric_version without +<minecraft>) and Minecraft version (minecraft_version). Created automatically. Up to 0.4.1 tags were plain vX.Y.Z.

Making a release

Work happens on rc; merging rc into main releases.

  1. Make changes on rc (commits, or pull requests from feature/... branches into rc). CI doesn't run by itself there: run it by hand before merging (Actions tab → CI → Run workflow, on the branch; the result is in the Actions tab, not in the pull request's checks). Note them under [Unreleased] in CHANGELOG.md. Try them in game with gradlew installToModrinth -Pinstance="<folder>", or publish a release candidate for others to try.
  2. When rc is ready: on rc, set mod_version in gradle.properties (if a candidate didn't already need it) and move the [Unreleased] notes in CHANGELOG.md under a new ## [X.Y.Z] - YYYY-MM-DD heading.
  3. Open a pull request from rc into main. Its release-check job fails if the version is already released or the changelog has no section for it, so every merge into main really is a release. CI builds only what isn't checked yet (see Reviews and branch protection).
  4. Merge it. On every push to main, the Release workflow (.github/workflows/release.yml) checks whether X.Y.Z already has a tag:
  5. No: it creates release/X.Y.Z and the tag (e.g. v0.4.2-fabload0.19.5+api0.162.0-mc26.3, also the release title), builds, and publishes a GitHub Release with MineDnD-X.Y.Z.jar, the sources jar, MineDnD-X.Y.Z.mrpack, MineDnD-Default-Content-X.Y.Z.zip (the datapack) and MineDnD-Default-Content-Resources-X.Y.Z.zip (its resource pack), using the version's CHANGELOG.md section as notes. Every release is a pre-release on GitHub and Beta on Modrinth: promote it by hand when it's ready (Modrinth: the version's Edit → release channel; GitHub: Edit release). Then it uploads the jar to Modrinth (see below), and builds the Pack Editor's portable apps into the same release.
  6. Yes: nothing is released (the workflow says so in its summary).
  7. rc is brought up to date with main automatically: the Release workflow's last step merges main into rc (usually a fast-forward). Only after a conflict, which can happen if rc got new commits in the meantime, does it warn and ask you to run git checkout rc && git merge origin/main && git push yourself. Locally, git pull on rc gets the update.

Watch it in the repository's Actions tab; the release appears on the Releases page and on Modrinth.

Release candidates

A release candidate puts what's on rc on Modrinth for testers, before the release. It's published only when you run it: Actions tab → Release candidate → Run workflow, Use workflow from: rc (from a terminal: gh workflow run rc.yml --ref rc). The Release candidate workflow (.github/workflows/rc.yml):

  1. refuses to run on another branch, or while mod_version is a version already released: a candidate is always for the next version, so bump mod_version on rc with the first one;
  2. runs the same checks as CI (the mod, modpack and datapack build with the tests, the docs site, the Pack Editor) and stops if one fails;
  3. uploads the jar to Modrinth as Alpha, not featured, numbered X.Y.Z-rc-apiA+mcM (e.g. 0.5.0-rc-api0.162.0+mc26.3), with the version's CHANGELOG.md section (or [Unreleased] until it's written) as changelog. The jar itself says X.Y.Z-rc+<commit> (e.g. 0.5.0-rc+5a1788f), so the mods list and bug reports show which candidate it is.

Every run replaces the version's previous candidate (it deletes it, then uploads the new one), so Modrinth has at most one candidate, and the release deletes it. The built jar, .mrpack and datapack zip are also kept with the run for 7 days. Without the Modrinth secret and variable, only the checks run.

It also (re)makes the candidate's GitHub pre-release rc-vX.Y.Z at its commit (.github/scripts/rc-prerelease.sh), titled "MineDnD X.Y.Z release candidate ()", with the changelog and the candidate's Default Content datapack, MineDnD-Default-Content-X.Y.Z-rc-<commit>.zip, and its resource pack, MineDnD-Default-Content-Resources-X.Y.Z-rc-<commit>.zip. Like the Modrinth candidate, it holds only the latest candidate (at another commit it's deleted and made again), and the release deletes it. The tag starts with rc-, not v, so it doesn't start the Release workflow nor count as a release.

Two checkboxes (both on by default) publish the rest of the candidate, once the checks pass:

So testers get the mod, its content, its docs and the app of the same commit. Untick the boxes for a quick candidate without docs and app.

If the release pull request's commit is one published as a candidate, CI doesn't build it again (the candidate ran the same checks); only the release check runs.

Documentation site

The docs are also a website, https://minednd.pages.dev/ (MkDocs Material, English and Italian, with search), on Cloudflare Pages: free, and the repository can stay private. The Docs workflow (.github/workflows/docs.yml) builds and publishes it on every push to main that changes docs/, so the site always describes the released version; it can also be run by hand from the Actions tab.

There's a second site for the release candidate, https://rc.minednd.pages.dev/: the same workflow run on rc (from the Release candidate workflow's docs checkbox, or by hand) builds it with DOCS_CHANNEL=rc and deploys it as the Pages project's rc branch, which Cloudflare keeps out of search engines. Its bar on top says it describes the next version. On both sites, that bar has a Release / Release candidate switch that opens the same page, in the same language, on the other site. Until the first candidate is published, rc.minednd.pages.dev doesn't exist and the switch leads nowhere.

Set up once:

  1. Account ID: on dash.cloudflare.com, open Workers & Pages (or the account's home): the Account ID is in the right-hand column (also in the address bar: dash.cloudflare.com/<account id>/…). Store it as the secret CLOUDFLARE_ACCOUNT_ID: gh secret set CLOUDFLARE_ACCOUNT_ID (paste it when asked).
  2. API token: avatar → Profile → API Tokens → Create Token → Custom token. Permissions: Account → Cloudflare Pages → Edit; Account resources: your account. Store it as the secret CLOUDFLARE_API_TOKEN: gh secret set CLOUDFLARE_API_TOKEN.
  3. Run the Docs workflow (Actions tab → Docs → Run workflow, on main). The first run creates the Pages project minednd (another name: the repository variable CLOUDFLARE_PAGES_PROJECT, and site_url in mkdocs.yml).

Until the secrets are set, the workflow builds the site and only warns. CI builds the site too (the docs job, on release pull requests or by hand), so a broken link or anchor in either language fails there. How the site is made: Development.

Modrinth uploads

The Release workflow runs .github/scripts/modrinth-upload.sh after the GitHub release. It uploads MineDnD-X.Y.Z.jar as version X.Y.Z: Fabric, the Minecraft version from gradle.properties, Fabric API as a required dependency, the release notes as changelog, always Beta (promoted by hand). A version already on Modrinth is skipped, so re-running is safe. Then it deletes the version's release candidate, if there is one.

Set up once (until then the step only prints "skipped"):

  1. On modrinth.com: avatar → Settings → Personal access tokens → create one with the Create versions and Delete versions scopes (and Read projects, to find the project while it's a draft or private). Deleting is for release candidates: each one replaces the previous one, and the release removes it.
  2. Store it in the repository as the secret MODRINTH_TOKEN: gh secret set MODRINTH_TOKEN (paste it when asked), or Settings → Secrets and variables → Actions → New repository secret.
  3. Store the project's slug (e.g. minednd) as the variable MODRINTH_PROJECT: gh variable set MODRINTH_PROJECT --body minednd.

CI also runs two checks (on release pull requests, or when run by hand): - a dry run of the upload (MODRINTH_DRY_RUN=1), which checks the data without contacting Modrinth; - an access check (MODRINTH_CHECK=1), which reads the project with the token and lists its versions. It shows up in the run as "Modrinth project OK: …", or as an error when the slug or token is wrong.

Pack Editor downloads

The MineDnD Pack Editor's portable apps (nothing to install: an .exe, a zipped .app, an AppImage) are built by Pack Editor downloads (.github/workflows/pack-editor.yml). The Release workflow runs it for every release, with the new tag; run it by hand from the Actions tab to rebuild them (e.g. if that run failed) or to try a build.

  • Release tag set (e.g. v0.10.0-fabload0.19.5+api0.162.0-mc26.3): it builds that tag on Windows, macOS (universal) and Linux and adds MineDnD-Pack-Editor-X.Y.Z-windows-portable.exe, …-macos.zip and …-linux-x86_64.AppImage to that release.
  • Empty, on rc (or from the Release candidate workflow's pack_editor checkbox): the release candidate's app, MineDnD Pack Editor RC, with its own name and app id, so it runs next to the released one, and a Guide that opens the release candidate's docs site. It adds MineDnD-Pack-Editor-RC-X.Y.Z-windows-portable.exe, …-macos.zip and …-linux-x86_64.AppImage to the candidate's pre-release rc-vX.Y.Z (see Release candidates; run by hand on a newer commit, it makes that pre-release again, without the datapack). The Release workflow deletes the pre-release when it releases the version.
  • Empty, elsewhere: it builds the branch you pick and keeps the apps as the run's artifacts for 14 days, to try a build.

They're unsigned (no paid certificates); the Pack Editor page ("Download") tells users how to open them the first time.

Fixing an old release

  1. Branch from release/X.Y.Z, fix, bump PATCH (X.Y.Z+1), add a changelog entry, open a pull request into release/X.Y.Z.
  2. After merging, tag the release branch by hand: git tag -a "$(bash .github/scripts/release-tag.sh)" -m "MineDnD X.Y.(Z+1)" and push the tag. Pushing a release tag also publishes a release (the tag must be the one gradle.properties gives).
  3. Bring the fix to main too (another pull request or a cherry-pick).

Reviews and branch protection

.github/CODEOWNERS makes the owner the required reviewer of every file, and CI (.github/workflows/ci.yml) builds the mod and runs the tests on every release pull request (into main or release/*, where its check checks / build is required), and on any branch when run by hand. The checks themselves are in .github/workflows/checks.yml, shared with the Release candidate workflow.

On a pull request, CI skips what's already done, to run as little as possible:

  • The commit was published as a release candidate (a successful Release candidate run on it): no build, docs or Pack Editor job, since the candidate ran them. The skipped checks / build counts as passed.
  • Only documentation changed (docs/, mkdocs.yml, Markdown files at the top of the repository): only the docs site is built.

The rules that enforce this (pull request required, code owner approval, CI must pass, no force pushes or deletions on main and release/*; release tags can't be moved or deleted) are prepared in .github/rulesets/. GitHub only enforces rulesets on private repositories with a paid plan (GitHub Pro or Team); on a public repository they're free. To apply them, once that's the case:

gh api -X POST repos/less-hub/MineDnD/rulesets --input .github/rulesets/branches.json
gh api -X POST repos/less-hub/MineDnD/rulesets --input .github/rulesets/tags.json

The repository admin can still merge their own pull requests (GitHub doesn't let authors approve their own pull requests); everyone else's pull requests need the owner's approval.