The maintainer's runbook. Everything here is done once per release except the first section, which is done once ever.
A publication to Maven Central is immutable — there is no delete, no overwrite and no re-publish of a botched version, only a new one. The workflow is built to fail loudly before it uploads anything, and this page exists so that the parts a workflow cannot check are not left to memory.
- The namespace
de.splatgamesmust be verified in the Central Portal. A verified parent namespace covers every child, sode.splatgames.aether.weaverneeds no separate verification. - Generate a user token at https://central.sonatype.com/usertoken. It has a username half and a password half; both become repository secrets below.
Releases are signed by a key that belongs to the release pipeline, not to a person's laptop.
gpg --full-generate-key # RSA 4096, no expiry or a long one
gpg --list-secret-keys --keyid-format=long # note the key id
gpg --keyserver keyserver.ubuntu.com --send-keys <key id>
gpg --armor --export-secret-keys <key id> # this whole block becomes a secretCentral checks the public key against keyserver.ubuntu.com, keys.openpgp.org and
pgp.mit.edu. Publish it to at least one of them before the first release, or validation
fails after the artefacts have already been built.
The public half is committed as KEYS at the repository root, and its fingerprint is
in SECURITY.md and the README, so that consumers can verify a
signature without asking for it. Rotating the key means replacing all three in the same commit.
Settings → Secrets and variables → Actions:
| Secret | What it is |
|---|---|
CENTRAL_USERNAME |
The user token's username half |
CENTRAL_TOKEN |
The user token's password half |
GPG_PRIVATE_KEY |
The armoured private key, including the BEGIN and END lines |
GPG_PASSPHRASE |
That key's passphrase |
A secret cannot be renamed, only deleted and recreated, so these names are the ones the workflow
follows rather than the other way round. They are not the same as the environment variables the
publish job sets from them — MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_TOKEN and
MAVEN_GPG_PASSPHRASE — and those are fixed: the first two are the placeholders settings.xml
resolves by name, and the third is what maven-gpg-plugin reads by default.
-
Environment
maven-central(Settings → Environments). The publish job runs in it; add a required reviewer there if you want a human gate in front of an irreversible upload. -
Default branch
main, withdevelopas the integration branch. Both are referenced by the workflows, the contribution links on the documentation site, andCONTRIBUTING.md. -
Private vulnerability reporting: on. SECURITY.md sends people to it.
-
Branch protection on
main: require theBuildchecks, require a pull request. -
The DCO app (https://github.com/apps/dco), which reads
.github/dco.yml. -
Description and topics, since they are how the repository is found:
A general-purpose bytecode weaving framework for the JVM, built on the standard Java Class-File API.
javajvmbytecodebytecode-manipulationclassfile-apiweavingaopmaven-pluginjava-agentframeworkHomepage:
https://software.splatgames.de/docs/aether-weaver/
Four of them the release workflow reads, and it refuses a tag that disagrees with any of them. Update those four in one commit:
| File | What to change |
|---|---|
CHANGELOG.md |
Rename ## [Unreleased] to ## [x.y.z] - <date>, open a fresh Unreleased, and update the link definitions at the bottom |
Writerside/writerside.cfg |
<instance … version="x.y.z"> |
Writerside/v.list |
<var name="version" value="x.y.z"/> |
aether-weaver-engine/…/engine/Weaver.java |
static final String VERSION = "x.y.z"; — it is stamped into every weave record |
Two more the release workflow does not read, but that must move with these or the gate fails: the
agent's own aether-weaver-agent/…/agent/WeaverAgent.java VERSION — its start-up banner — and the
built-in plugin id in aether-weaver-engine/…/engine/inject/CorePlugin.java. ExplainReportTest,
WeaverAgentEndToEndTest and WeaveMojoTest pin the printed version, so they move with it too.
The poms are not touched. They stay on -SNAPSHOT; the workflow stamps the release version in
from the tag. That way a build from main can never overwrite a released artefact.
The IDE plugin has three more, and they are set in step 6, not here.
It is a separate Gradle build the release workflow never sees, but the IntelliJ plugin CI check
builds it, and it resolves aether-weaver-api and -engine as ordinary published dependencies.
Bumping these before the release is on Central makes that check fail to resolve the version — so set
them once Central serves it, in step 6, where the plugin is actually built.
| File | What to change | What it breaks if you forget |
|---|---|---|
aether-weaver-ide/aether-weaver-idea/build.gradle.kts |
version = "x.y.z" |
The archive uploads as x.y.z-SNAPSHOT |
aether-weaver-ide/aether-weaver-idea/gradle.properties |
aetherWeaverVersion=x.y.z |
The plugin bundles the API and engine it resolves, so it ships the -SNAPSHOT jars from whoever built it rather than the published ones |
aether-weaver-ide/aether-weaver-idea/sample/pom.xml |
<aether.weaver.version>x.y.z</aether.weaver.version> |
checkSampleVersion fails the Gradle build: the sample would resolve a different API than the plugin was built against |
Also worth doing: Writerside/versions.json, if the site is to offer a version switcher.
mvn -B clean verify
python3 build-config/docsite/check-docs.py --buildBoth must exit 0. The second one takes about a minute and runs the real Writerside builder.
A dry run of the release packaging, which builds the sources and javadoc jars the way the release does, without signing or uploading anything:
mvn -B -Prelease clean package -DskipTestsgit tag -a v0.1.0 -m "Aether Weaver 0.1.0"
git push origin v0.1.0The tag is what triggers the release. Nothing else does.
The Release workflow runs four jobs:
- Verify — checks the version against those four files, stamps it into the poms, and runs the full gate.
- Publish — builds sources, javadoc and signatures, and uploads to Central with
autoPublishandwaitUntil=published. The job fails if Central rejects the deployment, rather than leaving it sitting inVALIDATEDfor somebody to notice. - SBOM — a CycloneDX bill of materials. Allowed to fail; it must not hold up a release.
- GitHub Release — created from this version's section of
CHANGELOG.md.
Artefacts usually appear on search.maven.org within a few minutes and on repo1.maven.org
shortly after.
- Publish the IDE plugin — the next section.
- Publish the documentation — section 3 below — and confirm https://software.splatgames.de/docs/aether-weaver/ serves the new version.
- Add the release's contributors to CONTRIBUTORS.md.
Wait until aether-weaver-api:x.y.z is actually on Central. The plugin resolves it as an
ordinary dependency and bundles it, so a build started before Central serves it either fails to
resolve or quietly bundles something else.
Set the three IDE-plugin versions now — they are left alone during step 1 because this build
resolves the published artefacts, so bumping them earlier fails the IntelliJ plugin CI check:
| File | What to change |
|---|---|
aether-weaver-ide/aether-weaver-idea/build.gradle.kts |
version = "x.y.z" |
aether-weaver-ide/aether-weaver-idea/gradle.properties |
aetherWeaverVersion=x.y.z |
aether-weaver-ide/aether-weaver-idea/sample/pom.xml |
<aether.weaver.version>x.y.z</aether.weaver.version> |
Then:
cd aether-weaver-ide/aether-weaver-idea
./gradlew buildPluginThe archive lands in build/distributions/. Upload it at
https://plugins.jetbrains.com/vendor/splatgames-software.
publishPlugin and signPlugin exist — the Gradle plugin registers them — but neither is
configured here: there is no Marketplace token and no signing certificate in build.gradle.kts.
Uploading is a manual step until that changes.
CI builds the site and fails on any error or warning the Writerside builder reports, but it does
not publish it. The site is served from software.splatgames.de; building and uploading is done
by hand, and this is the layout the result has to have.
Build it, either locally or by downloading the docs artefact from the Documentation workflow:
python3 build-config/docsite/check-docs.py --buildThe archive is webHelpAW2-all.zip, and it carries a file current.help.version holding the
version the builder used — that is what decides the directory, rather than anybody retyping the
number.
Under the document root, docs/aether-weaver/ must end up looking like this:
docs/aether-weaver/
├── 0.1.0/ the unzipped archive, under its own version
├── latest/ a copy of the release that versions.json marks current
├── index.html a redirect to latest/
├── versions.json Writerside/versions.json, copied verbatim
└── social-preview.png Writerside/images/social-preview.png, copied verbatim
Three of those are not optional and none of them comes out of the archive:
latest/is the address every link from outside the site uses — the README, the security policy, the issue templates. The pages inside it keep the versioned canonical URL the builder wrote, so the versioned path stays the one search engines index.versions.jsonis what the header's version switcher reads. It is not a topic, so the builder does not copy it, and a relative URL to it fails the build outright.social-preview.pngis the card a shared link unfurls as.<og-image>names it by URL, so without it every shared link carries a 404 image.
Keeping older version directories is optional and costs nothing; versions.json decides what the
switcher offers.
| Symptom | What it means |
|---|---|
| The verify job fails on a version mismatch | One of the four files was not updated. Fix it, delete the tag, tag again |
| Central rejects the deployment | Read the job log; it carries Central's own validation messages. Nothing was published |
| Signing fails, or the job hangs | The passphrase secret or the key secret is wrong. The key must be armoured, whole, including its BEGIN and END lines |
The deployment sits in VALIDATED |
autoPublish did not take effect. Publish it by hand in the portal, and fix the configuration before the next release |
The publish job fails but the portal says PUBLISHING |
The plugin stopped waiting; Central did not stop publishing. Nothing was lost and nothing may be re-uploaded. Wait for PUBLISHED, then create the GitHub Release by hand — the github-release job cannot be re-run alone, because it needs publish, and re-running failed jobs would deploy a second time |
The deployment sits in PUBLISHING for over an hour |
Sonatype's side. Check https://status.maven.org, then open a support ticket with the deployment id. Do not delete the tag and do not deploy again |
| The release published, the GitHub Release did not | Re-run only the github-release job. Central is already done and cannot be redone |
A released version is never re-released. If a release is wrong, release the next patch version.
A qualifier makes it one: v0.2.0-rc.1 publishes as a pre-release on GitHub and as an ordinary
version on Central, which has no notion of pre-release. The workflow decides that from the version
string alone.