Releasing¶
What a release needs, and what is already automated. Everything here has been exercised except the two uploads, which need credentials — so those steps say exactly what to expect rather than claiming to be verified.
What goes where¶
| Destination | Why there | |
|---|---|---|
jzap-model, jzap-core, jzap-agent, jzap-wire, jzap-minion, jzap-git, jzap-report, jzap-cli |
Maven Central, io.github.huyz0 |
Ordinary libraries; the Maven plugin resolves the engine from here |
io.github.huyz0.jzap (the Gradle plugin) |
Gradle Plugin Portal | Where plugins { id ... } resolves from |
io.github.huyz0:jzap-maven-plugin |
Maven Central, published by Maven | Built by Maven because plugin descriptors are |
Fixtures, tools/, jzap-e2e |
Nowhere | They are how jzap is tested, not what it is |
The namespace is io.github.huyz0, verified by the GitHub account of that name. Java packages
match it — io.github.huyz0.jzap.* — so a coordinate can be guessed from an import and the other
way round. Central verifies only the groupId, so the two were briefly allowed to differ; making
them agree cost one mechanical change and removed a question every new reader would have asked.
One-time setup¶
Four accounts-and-keys tasks, none of which can be automated from here.
1. Central Portal namespace¶
Register at central.sonatype.com, add the namespace
io.github.huyz0, and verify it by creating the public repository it asks you to create. Then
generate a user token (Account → Generate User Token) — the token, not your password, is what
CI uses.
2. A signing key¶
Central requires a detached signature on every artefact.
gpg --full-generate-key # RSA 4096, no expiry or a long one
gpg --list-secret-keys --keyid-format=long
gpg --armor --export-secret-keys <KEYID> # this whole block is the GPG_SIGNING_KEY secret
gpg --keyserver keys.openpgp.org --send-keys <KEYID> # Central checks a public keyserver
Publishing the public key is not optional: Central validates the signature against a keyserver, and a key it cannot find fails the deployment after upload.
3. Gradle Plugin Portal¶
Create an account at plugins.gradle.org, then claim the
io.github.huyz0 namespace — the Portal verifies it against the same GitHub account. Take the
API key and secret from your profile.
4. Repository secrets¶
In the repository's Settings → Secrets and variables → Actions:
| Secret | What |
|---|---|
MAVEN_CENTRAL_USERNAME |
Central Portal user token username |
MAVEN_CENTRAL_PASSWORD |
Central Portal user token password |
GPG_SIGNING_KEY |
The ASCII-armoured private key, whole block including the header lines |
GPG_SIGNING_KEY_PASSWORD |
That key's passphrase |
GRADLE_PUBLISH_KEY |
Gradle Plugin Portal API key |
GRADLE_PUBLISH_SECRET |
Gradle Plugin Portal API secret |
Releasing¶
That runs .github/workflows/release.yml, which:
- Runs
./gradlew build— the whole suite, the module-boundary invariant and the coverage floor. - Runs the PIT parity gate and the Maven plugin smoke test.
- Validates the Gradle plugin against the Portal without publishing it, so a bad key or a rejected plugin id fails before anything has been uploaded anywhere.
- Builds and signs the Central bundle.
- Uploads it to the Central Portal, which validates and then publishes automatically.
- Publishes the Gradle plugin to the Plugin Portal.
- Creates the GitHub release with the signed bundle attached. The notes say Central validates asynchronously, because for a few minutes after the page appears the coordinates still do not resolve, and a note implying otherwise sends people to debug their own build.
Every gate runs before anything leaves the machine, because a published version cannot be withdrawn — Central is immutable by design. The parity gate is part of that on purpose: shipping a build whose verdicts disagree with PIT would be shipping a wrong answer confidently.
Nothing to press¶
The Central upload is publishingType=AUTOMATIC, so a green run publishes without further input.
Watch it at central.sonatype.com/publishing/deployments;
validation is asynchronous and can still fail after the workflow step reports success, so a
green run means uploaded and accepted rather than definitely live.
What stands in for a human pause is everything that runs before the upload: the whole suite, the coverage floor, the PIT parity oracle, the Maven smoke test and a plugin validation against the Portal. A person clicking publish on a deployment they had not inspected was never really a check — but it did mean a bad release could be abandoned rather than published. Pushing the tag is now the decision to publish, and Central is immutable, so the way to undo a mistake is to release the fix as a new version.
Dry-running it locally¶
The bundle is the part worth checking by hand, and it needs no accounts:
export GPG_SIGNING_KEY="$(gpg --armor --export-secret-keys <KEYID>)"
export GPG_SIGNING_KEY_PASSWORD=...
./gradlew centralBundle -PjzapVersion=0.1.0
unzip -l build/central/jzap-0.1.0-bundle.zip
Each module should show, for every one of the jar, sources jar, javadoc jar, POM and Gradle module
file: the file itself, .asc, .md5, .sha1, .sha256 and .sha512. Verify one for real:
cd $(mktemp -d) && unzip -q /path/to/jzap-0.1.0-bundle.zip
gpg --verify io/github/huyz0/jzap-model/0.1.0/jzap-model-0.1.0.jar{.asc,}
./gradlew publishToMavenLocal -PjzapVersion=0.1.0 installs into ~/.m2 instead, which is how to
try the Maven plugin against a real project before anything is published.
Two guards will stop you rather than letting a bad bundle reach the Portal:
- A snapshot version. The release endpoint does not take snapshots, and a snapshot's filenames
carry a build timestamp, so the rejection would arrive after the upload. Pass
-PjzapVersion. - No signing key. An unsigned bundle is rejected after upload too.
Known about the Plugin Portal¶
A plugin's first version is reviewed by hand. publishPlugins succeeds and reports it:
"Your new plugin has been submitted for approval by Gradle engineers. The request should be
processed within the next few days, at which point you will be contacted via email." Until that
completes the plugin id does not resolve and its page returns 400, so a release note claiming the
plugin is live is wrong for the first version — the workflow's generated notes say this.
Configuration-cache compatibility is not declared, and should be. The Portal asks for it on
publish, and jzap qualifies: JzapPluginTest runs mutationTestAll --configuration-cache and
fails on any problem. The obstacle is mechanical rather than a question of fact — the
compatibility { features { configurationCache = true } } block documented for
com.gradle.plugin-publish 2.1.0+ does not resolve on 2.2.1 here, and applying
org.gradle.plugin-compatibility by name registers no extension either, checked by listing the
project's extensions rather than inferred. Worth another look before the next release; an
undeclared feature reads on the Portal as unsupported.
The Maven plugin¶
Built by Maven, so it is released by Maven. Publishing lives in a release profile, so an
ordinary mvn install needs neither a signing key nor network access to Sonatype:
That attaches sources and javadoc, signs everything, and publishes to the Central Portal —
autoPublish=true with waitUntil=PUBLISHED, so the command does not return until Central has
actually published rather than merely accepted the upload. It reads the central server and the
gpg passphrase from ~/.m2/settings.xml.
Rehearse it without publishing:
Two things this does not yet do, both deliberate rather than overlooked:
- It is not in the release workflow. A
v*tag publishes the engine and the Gradle plugin; the Maven plugin is a separate command. Wiring it in means giving the workflow a Maven step with its own signing setup, and it is worth doing once the two have released together at least once by hand. - Its version is set by hand in
jzap-maven/pom.xmlrather than derived from the tag. The smoke test reads that version from the POM rather than pinning its own copy, so a bump cannot leave the test exercising a coordinate that is no longer built — which is exactly what happened the first time the version moved.
After a release¶
- Bump the default in
build.gradle.ktsif you want the next snapshot to read differently; the version is a property with a snapshot default, so nothing needs resetting. - The Gradle plugin's
engineVersionconvention comes from the plugin's own version, so a released plugin resolves the matching engine without configuration. - Central takes up to a few hours to appear on
search.maven.org, and about fifteen minutes to be resolvable fromrepo1.maven.org. Resolvable first, searchable later — do not assume a failure from a search that comes up empty.