All pages

Releasing

How a Vantage release is cut - the version and its channel, the checks before the tag, what the pipeline does on its own, where the jars land, the release notes, the announcements, and what the maintainer still does by hand.

A Vantage release is a git tag. Push an annotated v<version> tag on a commit whose gradle.properties says the same version, and the pipeline builds a jar for each Minecraft version, uploads them, and creates the GitLab Release with its notes. This page walks through the whole cut, says plainly which steps run unattended and which a person does, and ends with what a fork changes to release under its own name. Everything here is read from .gitlab-ci.yml, tools/release.py, tools/modrinth.py (on main), tools/announce.py and build.gradle.

Versions and channels

mod_version in gradle.properties is the one place the version lives. The build stamps it everywhere else: the jar's name and manifest carry <mod_version>+<minecraft_version>, and fabric.mod.json holds only a ${version} placeholder that the build fills in.

A version is MAJOR.MINOR.PATCH, optionally followed by -alpha.N or -beta.N, and the suffix is its channel:

VersionChannelWho is told about it
1.0.11-alpha.2alphaClients following the alpha channel
1.0.11-beta.1betaClients following beta or alpha
1.0.11stableEveryone

Alphas and betas continue the open cycle, and the stable release closes it. A client follows one channel and hears about that channel and everything more stable; until the player picks one in the Update checker, it follows the channel of the build it is running.

The automatic bump, and why a release does not use it

build.gradle bumps the version by itself. Any jar-producing task (jar, remapJar, build, assemble, publish, publishToMavenLocal) run without -PnoVersionBump increments the last dot-separated number of mod_version and writes it back to gradle.properties before anything compiles, so the jar being built carries the new number. The build log says so: Vantage version <old> -> <new> (<commit>).

That is convenient for a quick patch build and wrong for a release. It cannot make a minor or major bump, and a jar built in CI would carry a number that no commit holds. So a release sets mod_version by hand, and the pipeline builds every release jar with -PnoVersionBump, so the jar says exactly what the tag says. If a bump happens by accident, git checkout -- gradle.properties puts the file back.

Before the tag

These are the maintainer's checks. Every pipeline repeats two of them, the default target's compile and tests and the tracker check; the rest happen on the maintainer's machine. Rule 9 of ARCHITECTURE.md names the mixin check and the access-token grep as release gates.

  1. The change is verified on both Minecraft versions, with the smoke test read. Testing lists the commands.

  2. Every mixin target exists on every supported version: python tools/mixin_targets.py.

  3. The Minecraft access token has one call site. This must print exactly one line, in core/stats/sync/IdentityProof.java:

    grep -rn "getAccessToken\|getSessionId" src/main/java
  4. The generated files are current: python tools/tracker.py --check and python tools/sitedocs.py --check, plus python tools/stats_catalogue.py --check when Vantage Stats changed.

  5. CHANGELOG.md has a section for the version. Its heading is ## <version> — <date>, with (Alpha) or (Beta) after the date for a pre-release. The file describes itself as compact, user-facing notes per release, and the release notes are built from that section.

Cutting it

By hand, the cut is one commit and one tag on the default branch:

# gradle.properties: mod_version=<version>        CHANGELOG.md: ## <version> — <date>
git add gradle.properties CHANGELOG.md
git commit -m "release: <version> for both targets"
git tag -a v<version> -m "Vantage <version>"
git push origin main
git push origin v<version>

The tag must be annotated (-a). Its message becomes the release notes when CHANGELOG.md has no section for the version. The maintainer's private Discord bot can make the same commit and tag; either way, pushing the tag is the moment everything below starts.

What the pipeline does on its own

The release job runs for any tag that starts with v and a digit. In order:

  1. It reads the version from the tag and refuses to go on if gradle.properties disagrees, printing what each says. A jar whose version differs from its tag would lie about itself.
  2. It builds both jars, 26.2 first and then 26.1.2, each with -PnoVersionBump.
  3. It runs python3 tools/release.py publish --version <version>, which does three things and stops at the first that fails:
    • checks the jars: one for every Minecraft version in the tool's TARGETS, and each carrying a real Vet card key (64 hexadecimal characters) rather than the placeholder a build without the key leaves behind;
    • uploads every jar, sources jar and checksum for the version to the project's generic package registry;
    • creates the GitLab Release, named Vantage <version>, with the notes and one link per uploaded file.
  4. It keeps the jars and checksums as job artifacts for 90 days.

The tool is written to be run again for the same tag: when the Release already exists, it refreshes the notes and adds only the links that are missing.

Where the files land

The tool uploads to the project's generic package registry, which is public, so every link works without a token:

https://gitlab.com/api/v4/projects/<project id>/packages/generic/vantage/<version>/<file>

Each release carries three files per Minecraft version:

FileWhat it isLinked as
vantage-<version>+26.2.jarThe jar for 26.2Package
vantage-<version>+26.2.jar.sha256Its SHA-256Other
vantage-<version>+26.2-sources.jarIts sourcesOther
vantage-<version>+26.1.2.jarThe jar for 26.1.2Package
vantage-<version>+26.1.2.jar.sha256Its SHA-256Other
vantage-<version>+26.1.2-sources.jarIts sourcesOther

A checksum file is one line in the usual <hash> <file name> form, so it checks with the standard tool:

sha256sum -c vantage-<version>+26.2.jar.sha256

The jars are checksummed, not signed. The signRelease task in build.gradle writes a checksum beside every jar on every build and signs only when a keystore is passed to it (-PsignKeystore, -PsignAlias, -PsignStorePass). The release job passes none: a real signature needs a private key that belongs to whoever publishes the mod, and the build is never meant to hold one.

Modrinth, by hand

On 1 October 2026, after 2.0.1 was released, the tag pipeline on main gained a second home for a release: a modrinth job in the release stage. It never runs by itself. A person presses play on the tag pipeline once the GitLab Release looks right, because a version published on Modrinth cannot really be taken back once someone may have downloaded it.

The job reuses the jars the release job built and kept, so both homes carry the same bytes, and runs python tools/modrinth.py publish --version <version>. The tool borrows release.py's jar checks and its notes, then makes one Modrinth version per Minecraft target:

Modrinth fieldValue
Version numberThe bare mod version, the same string the mod uses
Game versionsThe one target the jar was built for, checked against Modrinth's own list before anything uploads
LoaderFabric
Version typeRelease, beta or alpha, from the version's channel
DependencyFabric API, required

It skips a version that already exists with the same number and game versions, so pressing play again after a partial failure only finishes the rest. --dry-run does every check and every read and uploads nothing. The job needs a MODRINTH_TOKEN variable, masked and protected (so, again, the v* tags must be protected), and reads the project from MODRINTH_PROJECT, defaulting to vt-vantage.

Release notes

release.py builds the notes from three parts:

  1. A channel banner, for an alpha or a beta only. It says the build ships early to people on that channel of the update checker, that it may break, and that the stable release closing the cycle will carry the same changes.
  2. The body: the version's section of CHANGELOG.md, else the tag's message, else just "Vantage <version>.".
  3. A "Which jar" table, with a row per Minecraft version giving the jar's name and its declared range, and one sentence under it on the loader, Fabric API and Java both jars need.

To see the notes without publishing anything, run the tool with --dry-run after building both jars. It checks the jars, Vet key included, and prints the notes; it needs no token. --notes-file <path> replaces the body with a file of your own.

How players hear about it

Two separate things tell a running client, and only the first is automatic.

The update checker. Update checker asks GitLab's releases list once per session and shows a single toast when a newer build exists on the channel the client follows. It needs nothing except the Release the pipeline created.

Announcements. announcements.json, at the root of the repository, is read raw from the default branch by every client's Announcements feature, and each entry is shown once. Publishing one is a commit. tools/announce.py writes the entry:

python tools/announce.py add --kind release --version <version> --channel beta \
    --title "<version> is out" --url https://gitlab.com/Ekoss/vantage/-/releases/v<version> --body "What changed: …"
python tools/announce.py list
FieldMeans
kindrelease, news, warning or maintenance
versionRelease only. Older clients see an update prompt on the card; a client already on it, or newer, gets the card as notes for its build
channelOnly clients following this channel, or a less stable one, see it. A stable release can leave it out
minVersion, maxVersion, minecraftNarrow it to some mod versions or some Minecraft versions
expiresA date after which clients stop showing it

Removing an entry withdraws it. In the maintainer's practice an alpha usually gets no announcement, because the update checker already tells alpha testers.

Discord. When a push to the default branch changes announcements.json, the announce-discord job runs python tools/announce.py ci, which posts only the entries that push added, through a channel webhook held in the masked DISCORD_ANNOUNCEMENTS_WEBHOOK variable. Without the variable it prints a warning and succeeds, so a missing webhook is visible in the log without turning the pipeline red. A commit whose message contains [bot-announced] is skipped, because the bot has already posted it. python tools/announce.py post --id <id> posts one entry by hand. Announcements and updates describes all of this from the player's side.

Unattended, and by hand

StepDone by
Choosing the version, editing gradle.properties, writing the changelog sectionThe maintainer
The mixin check and the access-token grepThe maintainer
The release commit, the annotated tag and pushing themThe maintainer, or the maintainer's bot
Building both jars, checking the Vet key, uploading, creating the ReleaseThe release job
Telling running clients a build existsThe update checker, once the Release exists
Writing an announcementThe maintainer, with announce.py add and a commit
Posting it to DiscordThe announce-discord job
Publishing to ModrinthThe modrinth job, only when the maintainer presses play on the tag pipeline
Signing a jarNobody, unless the maintainer passes a keystore by hand

The two manual rows are manual on purpose. A Modrinth version cannot be withdrawn once it may have been downloaded, so a person decides; a signature needs a private key the build must never hold.

For a fork

Most of this works for a fork unchanged, and a few lines point at Vantage's own project:

  • The project. In a pipeline, release.py publishes to the project the pipeline runs in. By hand, pass --project-id <id> and set GITLAB_TOKEN.
  • The Vet key. release.py refuses a jar without a real Vet card key. Set your own VANTAGE_VET_KEY, or remove the check if your fork drops Vet; Forking explains what the key is for.
  • The URLs. The update checker's project id and releases page, the announcement feed's address, and the project and icon addresses in announce.py all name gitlab.com/Ekoss/vantage. Point them at your project.
  • Modrinth. Set MODRINTH_PROJECT to your own project's slug or id, and give the job a token of your own, or delete the modrinth job if you do not publish there.
  • The targets. If your fork supports other Minecraft versions, change TARGETS and the release job together, as Multi-version describes.