Skip to content

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

git tag v0.1.0
git push origin v0.1.0

That runs .github/workflows/release.yml, which:

  1. Runs ./gradlew build — the whole suite, the module-boundary invariant and the coverage floor.
  2. Runs the PIT parity gate and the Maven plugin smoke test.
  3. 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.
  4. Builds and signs the Central bundle.
  5. Uploads it to the Central Portal, which validates and then publishes automatically.
  6. Publishes the Gradle plugin to the Plugin Portal.
  7. 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:

cd jzap-maven
mvn -B -Prelease deploy -DskipTests

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:

mvn -B -Prelease verify -DskipTests
ls target/*.asc          # jar, sources, javadoc and pom signatures

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.xml rather 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.kts if 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 engineVersion convention 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 from repo1.maven.org. Resolvable first, searchable later — do not assume a failure from a search that comes up empty.