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.
On this page
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:
| Version | Channel | Who is told about it |
|---|---|---|
1.0.11-alpha.2 | alpha | Clients following the alpha channel |
1.0.11-beta.1 | beta | Clients following beta or alpha |
1.0.11 | stable | Everyone |
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.
The change is verified on both Minecraft versions, with the smoke test read. Testing lists the commands.
Every mixin target exists on every supported version:
python tools/mixin_targets.py.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/javaThe generated files are current:
python tools/tracker.py --checkandpython tools/sitedocs.py --check, pluspython tools/stats_catalogue.py --checkwhen Vantage Stats changed.CHANGELOG.mdhas 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:
- It reads the version from the tag and refuses to go on if
gradle.propertiesdisagrees, printing what each says. A jar whose version differs from its tag would lie about itself. - It builds both jars, 26.2 first and then 26.1.2, each with
-PnoVersionBump. - 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.
- checks the jars: one for every Minecraft version in the tool's
- 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:
| File | What it is | Linked as |
|---|---|---|
vantage-<version>+26.2.jar | The jar for 26.2 | Package |
vantage-<version>+26.2.jar.sha256 | Its SHA-256 | Other |
vantage-<version>+26.2-sources.jar | Its sources | Other |
vantage-<version>+26.1.2.jar | The jar for 26.1.2 | Package |
vantage-<version>+26.1.2.jar.sha256 | Its SHA-256 | Other |
vantage-<version>+26.1.2-sources.jar | Its sources | Other |
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.sha256The 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 field | Value |
|---|---|
| Version number | The bare mod version, the same string the mod uses |
| Game versions | The one target the jar was built for, checked against Modrinth's own list before anything uploads |
| Loader | Fabric |
| Version type | Release, beta or alpha, from the version's channel |
| Dependency | Fabric 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:
- 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.
- The body: the version's section of
CHANGELOG.md, else the tag's message, else just "Vantage <version>.". - 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| Field | Means |
|---|---|
kind | release, news, warning or maintenance |
version | Release 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 |
channel | Only clients following this channel, or a less stable one, see it. A stable release can leave it out |
minVersion, maxVersion, minecraft | Narrow it to some mod versions or some Minecraft versions |
expires | A 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
| Step | Done by |
|---|---|
Choosing the version, editing gradle.properties, writing the changelog section | The maintainer |
| The mixin check and the access-token grep | The maintainer |
| The release commit, the annotated tag and pushing them | The maintainer, or the maintainer's bot |
| Building both jars, checking the Vet key, uploading, creating the Release | The release job |
| Telling running clients a build exists | The update checker, once the Release exists |
| Writing an announcement | The maintainer, with announce.py add and a commit |
| Posting it to Discord | The announce-discord job |
| Publishing to Modrinth | The modrinth job, only when the maintainer presses play on the tag pipeline |
| Signing a jar | Nobody, 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.pypublishes to the project the pipeline runs in. By hand, pass--project-id <id>and setGITLAB_TOKEN. - The Vet key.
release.pyrefuses a jar without a real Vet card key. Set your ownVANTAGE_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.pyall namegitlab.com/Ekoss/vantage. Point them at your project. - Modrinth. Set
MODRINTH_PROJECTto your own project's slug or id, and give the job a token of your own, or delete themodrinthjob if you do not publish there. - The targets. If your fork supports other Minecraft versions, change
TARGETSand thereleasejob together, as Multi-version describes.
Related
- Testing: the checks behind "verified".
- Multi-version: why there are two jars, and how to make it three.
- Forking: the names and addresses a fork changes.
- Announcements and updates: what a player sees.