This is the canonical release runbook for GraphCompose 1.x.
- Maven Central —
io.github.demchaav:graph-compose:<version>(canonical, from v1.6.6) - JitPack —
com.github.DemchaAV:GraphCompose:v<version>(legacy; resolves for callers pinned to v1.6.5 and earlier, no longer the documented install channel)
The release workflow is automated by scripts/cut-release.ps1. The script must run from the develop branch with a clean working tree. The agent (Claude / Codex) must complete every audit gate below before a release tag is cut, and must wait for explicit human approval ("yes, cut the tag" / "делаем тег") before invoking the script.
Agent contract: audit and pre-release fixes are local-only by default. The script is the only step that mutates remotes (push develop, push tag). Never tag, push tags, or merge to
mainwithout an explicit go-signal in the chat.
Run this every time, in order. Stop on the first red gate and fix it before continuing.
The shell setup and exact PowerShell commands live in the graphcompose-release-engineer skill (loaded via Skill tool). On Git Bash use ./mvnw instead of .\mvnw.cmd; the gates are identical.
- On
developbranch (or in adevelopworktree). Never tag frommain. -
git status --shortis clean. No??zero-byte stragglers ({,,0),[Help, etc.). Verify any leftover withwc -c <file>before deleting. -
git log origin/develop..origin/main --onelineis empty. If not, mergeorigin/mainintodevelopand resolve conflicts before proceeding (a hotfix onmainblocks the fast-forward at script Step 8). -
git rev-parse develop origin/developreturns identical SHAs (script enforces this in pre-flight).
-
./mvnw -B -ntp -q clean verify -pl .exits 0. Every test must pass — no skips, no flake retries. ConfirmTests run: <N>, Failures: 0, Errors: 0, Skipped: 0fromtarget/surefire-reports/*.txt. - Examples module compiles cleanly:
./mvnw -B -ntp -q -f examples/pom.xml clean compileexits 0. Catchesdouble → floatlossy narrowing and similar bugs that don't surface in the root module. - All examples regenerate:
./mvnw -B -ntp -q -f examples/pom.xml exec:java -Dexec.mainClass=com.demcha.examples.GenerateAllExamplesproduces 26+Generated:lines, exits 0, and emits noFixed column ... is smaller than required natural widthorSpanned cell ... requires extra widtherrors. (Requires./mvnw install -DskipTests -pl .once first so the local~/.m2resolves the current SNAPSHOT/beta version.) - Architecture-guard suite explicitly green:
./mvnw -B -ntp test -pl . -Dtest='CanonicalSurfaceGuardTest,DocumentationCoverageTest,DocumentationExamplesTest,InternalAnnotationCoverageTest,PublicApiNoEngineLeakTest,SemanticLayerNoPdfBoxDependencyTest,VersionConsistencyGuardTest'exits 0. These guard against legacy-API leakage in docs and engine internals leaking into the public surface, and — viaVersionConsistencyGuardTest— against version drift between the library pom, the aggregator, the inherited examples/benchmarks modules, and the README install snippets. They fail loudly when README/CHANGELOG drift from the canonical authoring surface.
-
CHANGELOG.mdhas a## v<target> — Plannedheader at the top. The script flipsPlanned→ today's date during release execution; if the header is missing or already dated, the script silently skips and the release ships with the wrong header. - CHANGELOG
v<target>section: every linked file resolves on disk. Common offenders: newdocs/adr/00XX-*.md,docs/migration-v1-N-to-v1-M.md, recipe pages. -
README.mdtest-count claim matches the actual surefire total (grep -E '[0-9]+ green tests' README.mdvs the surefire aggregate). -
README.mdinstall snippets match the currentpom.xmlversion (ondevelopbetween releases that is the last published version).VersionConsistencyGuardTestenforces README == pom, so the two move together:cut-release.ps1rewrites the README Maven + Gradle install snippets to the new version in the same release commit it bumps the POMs (section 1, Step 2/6). The README therefore flips to the new version at release-execution time, never ondevelopahead of the tag — a snippet pointing at a version that has not been published yet would 404 for any user who copies it. Do not hand-flip the README ahead of the script: that desyncs README from the still-unbumped pom and fails the guard at the verify gate. -
README.mdandexamples/README.mdlink audits resolve: every(./...)and(../...)link must exist on disk. Usegrep -oE '\(\.?\.?/[^)]+\.(md|java|png|pdf|jpg)\)' README.md examples/README.md | sed 's/^(//;s/)$//' | sort -u | xargs -I{} test -e {} || echo MISSING: {}. -
examples/README.mdgallery row count matches the file count:find examples/src/main/java -name '*Example.java' | wc -lequalsgrep -c '^| \[' examples/README.md. - For minor releases (
vX.Y.0):docs/migration-v1-<Y-1>-to-v1-<Y>.mdexists. Patch releases skip this.
The script's Step 1–4 mutates these. The agent only confirms the current state is one the script can transition from:
- The version lives in four sites that must stay in lockstep: the standalone library
pom.xml, the reactoraggregator/pom.xml, and the inherited<parent>version ofexamples/pom.xmlandbenchmarks/pom.xml(the children no longer pin their own<version>— they inherit fromgraph-compose-build, and declare<graphcompose.version>${project.version}</graphcompose.version>rather than a literal). All four read the same value: either the in-flight develop value or already the target.VersionConsistencyGuardTestasserts they agree;cut-release.ps1Step 1 moves all four (plus the README) together. Bumping by hand outside the script — ormvn versions:seton a single pom — is what previously left benchmarks on the prior release; if you must bump outside the script, usemvn -f aggregator/pom.xml versions:set -DnewVersion=<X>. -
examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.javaGH_BASEpoints to/blob/develop. The script flips it to/blob/v<target>and regeneratesdocs/examples.json.
-
git tag -l v<target>andgit ls-remote --tags origin v<target>both return empty. The script enforces this; if a stale tag remains from a failed previous attempt, delete it intentionally (git tag -d v<target>+git push origin :refs/tags/v<target>) only with explicit user approval.
Running pwsh ./scripts/cut-release.ps1 -Version <X.Y.Z> performs:
- Pre-flight — re-checks all of A above (branch, clean tree, in-sync, no existing tag).
- Bump versions to
<X.Y.Z>across the librarypom.xml, theaggregator/pom.xml, the inherited<parent>refs inexamples/pom.xmlandbenchmarks/pom.xml, and the README Maven + Gradle install snippets — all in one pass, soVersionConsistencyGuardTeststays green at Step 5. - Date the CHANGELOG — flips
## v<X.Y.Z> — Plannedto## v<X.Y.Z> — <today-ISO>. - Switch ShowcaseMetadata GH_BASE from
/blob/developto/blob/v<X.Y.Z>and regeneratedocs/examples.json. mvnw verify -pl .— full sanity build (skip with-SkipVerifyonly if you just ran it).- Commit as
Release v<X.Y.Z>. Files committed: the librarypom.xml,aggregator/pom.xml,examples/pom.xml,benchmarks/pom.xml,README.md(install snippets),CHANGELOG.md,ShowcaseMetadata.java, anddocs/examples.json.examples/README.mdand any other docs are NOT touched by the script — fix those pre-release. - Annotated tag
v<X.Y.Z>(git tag -a -m "Release v<X.Y.Z>"). - Push
developand the tag toorigin(skip with-SkipPush).
The script supports -DryRun (preview every step), -SkipPush (commit + tag locally only), and -PostReleaseOnly (skip release work entirely, only flip GH_BASE back to /blob/develop and push).
The script does not handle these. They are either pre-release or post-release responsibilities:
- Stale documentation claims — examples count, gallery descriptors, version-anchored prose. Fix in a
docs: pre-release fixes — <what>commit on develop, then commit, then push (or stage and let the user push). - CHANGELOG
## v<target> — Plannedheader — must exist before the script runs. If you bumped scope mid-cycle, ensure the planned header is still on the right version line. - Missing migration guide for minor releases — write
docs/migration-v1-<prev>-to-v1-<target>.mdif absent. InternalAnnotationCoverageTestand other guard tests — fix any failures by adjusting the source (annotation propagation, doc rewording), never by suppressing the test or extending the allowlist. Allowlist edits are reviewable evidence of an architecture decision; write or update an ADR before suppressing.
Run within 1 hour of the tag push. Independent steps can run in parallel.
- Wait for Maven Central artefact — once
.github/workflows/publish.ymlturns green (see step 9 below), pollmvn dependency:get -DgroupId=io.github.demchaav -DartifactId=graph-compose -Dversion=<target>until it resolves (usually 5–15 minutes after the workflow finishes). Then: - README install snippets — already flipped to
<target>bycut-release.ps1in the release commit (section 1, Step 2/6) and enforced byVersionConsistencyGuardTest. No separate post-release commit is needed; just confirm the Central artefact resolves (step 1 above) means the version the README now advertises actually exists. - Merge
develop→mainon GitHub so GitHub Pages picks up the new docs. Fast-forward only — never force-pushmain. If the push is rejected withnon-fast-forward, a hotfix landed onmainafter the audit and the merge has to be redone after mergingorigin/mainback intodevelop. - Verify CI green on main —
gh run list --branch main --limit 1showssuccessfor the tag commit. - Smoke-test the install snippet — minimal POM in
$env:TEMP,mvn dependency:resolveagainst the snippet copy-pasted from README, expect 0 exit. - Re-run all examples against the published artifact —
./mvnw -f examples/pom.xml clean packagefollowed byexec:java -Dexec.mainClass=com.demcha.examples.GenerateAllExamples. Expect 26+Generated:lines. - Flip ShowcaseMetadata back to develop —
pwsh ./scripts/cut-release.ps1 -PostReleaseOnly. This restores linkable "View Code" buttons for ongoing v1.x.y dev work. - GitHub Release — automated. Pushing the
v<target>tag triggers.github/workflows/release.yml: it re-runs./mvnw clean verify -pl .against the tagged commit, then creates the Release with that version's CHANGELOG section as the body (hyphenated tags likev1.7.0-rc.1ship as pre-releases; the step is idempotent — it edits the notes if the Release already exists). The workflow titles itGraphCompose v<target>; for a minor release, edit the title to add the codename (v1.4=cinematic,v1.5=intuitive,v1.6=expressive; patches drop it). Create the Release by hand (gh release create v<target> --notes-file <CHANGELOG section>) only if the workflow is unavailable. - Maven Central publish — automated (from v1.6.6). The same
v<target>tag push triggers.github/workflows/publish.yml: it re-runsmvnw verifyat the tagged commit, signs the four artefacts (main / sources / javadoc / pom) with the repo's GPG key, and uploads to Maven Central via thecentral-publishing-maven-plugin. Hyphenated tags (-rc,-alpha,-beta,-snapshot) are skipped — those go only to the GitHub Release pre-release surface.autoPublish=falsein the plugin config means the artefact lands in the Central validation queue; the maintainer flips the switch on central.sonatype.com for the first publish, then can opt into auto-release in a follow-up. Verify viamvn dependency:get -DgroupId=io.github.demchaav -DartifactId=graph-compose -Dversion=<target>once the artifact appears (usually 5–15 minutes after the workflow turns green). - Optional: GitHub Discussions announcement (mirror the prior release's style; close with "author intent, not coordinates"), LinkedIn post, r/java post.
The release is done only when steps 1–7 are all green; step 9 adds Maven Central availability once the D-track of v1.6.6 has shipped.
These steps are done once per repo before the publish workflow can succeed; they are not part of every release. Listed here so a future maintainer (or a future me) can reproduce the setup without spelunking through commit history.
- Generate a GPG key.
gpg --full-generate-key # RSA 4096, no expiry, real-name / email of maintainer gpg --list-secret-keys --keyid-format=long gpg --armor --export-secret-keys <KEYID> > private-key.asc # never commit this file
- Publish the public key to a keyserver pool. Central validates the signature against one of these. Two redundant pools is the conventional minimum:
gpg --keyserver keys.openpgp.org --send-keys <KEYID> gpg --keyserver keyserver.ubuntu.com --send-keys <KEYID>
keys.openpgp.orgrequires a one-time email-verification round-trip on the address attached to the key. - Register a Sonatype Central account. At central.sonatype.com — sign in with GitHub for the auto-verified
io.github.<gh-handle>namespace (theio.github.demchaavnamespace this repo claims). Verify the namespace via GitHub-account check or DNS TXT record on the published domain. - Generate a Central user token. Account → Generate User Token → copy the
usernameandpasswordhalves. These are credentials, not the GitHub login. - Wire four GitHub repo secrets. Repo Settings → Secrets and variables → Actions → New repository secret:
MAVEN_GPG_PRIVATE_KEY— full ASCII-armored private key (the contents ofprivate-key.ascfrom step 1).MAVEN_GPG_PASSPHRASE— the passphrase guarding the key.CENTRAL_USERNAME— token username from step 4.CENTRAL_TOKEN— token password from step 4.
- Test the wiring on a release-candidate tag before the first real release.
v1.6.6-rc.1(hyphenated) skips Central perpublish.yml'sif:guard, so it's safe — alternatively, cutv1.6.6for real and observe the workflow;autoPublish=falsemeans a failed validation does not pollute Central, the artefact just sits in the validation queue until manually released or deleted.
If any of these stop working between releases (key expired, token rotated), the publish workflow surfaces the failure inside the workflow run — the GitHub Release is still cut by the other workflow, and the legacy JitPack URL keeps resolving for callers pinned to earlier versions.
The published jar is final. Never force-move a tag that Maven Central has already validated — Central rejects re-uploading the same coordinates, and JitPack (still building legacy v1.6.5 and earlier) caches by tag SHA and won't rebuild either. Always fix forward with a vX.Y.Z+1 patch tag.
- Diagnose:
gh run view <run-id> --log-failed. TheTests run: <N>, Failures: <F>line is the source of truth, not the trace excerpt in the Actions UI annotation. - Fix the test or doc, not the published artifact.
- Commit + push to
develop, fast-forward tomain. - If the bug is in shipped runtime code (rare), publish a
vX.Y.Z+1patch via the full pipeline. Add a CHANGELOG patch entry under the new version header.
| Symptom | Root cause | Fix |
|---|---|---|
DocumentationCoverageTest.readme<X>SectionShouldUseCanonicalDsl red after a README rewrite |
Section-anchored guard pinned to a heading the rewrite removed | Replace with one whole-file scan: readmeShouldUseCanonicalDslAndAvoidLegacyApis |
ShapeContainerVisualRegressionTest (or any visual regression) red on Linux CI only |
Cross-platform PDFBox font drift between Windows-rendered baselines and Linux CI (~1–2 % pixel diff) | Bump mismatchedPixelBudget(0) to ~2_500 (calibrated against observed CI delta) |
Runtime Fixed column 0 width X is smaller than required natural width Y from GenerateAllExamples |
Table cell content's natural width exceeds its fixed column | Reduce font size, reduce padding, or pre-split via DocumentTableCell.lines(parts) so cellNaturalWidth measures the longest single line |
Spanned cell at row N over M fixed columns requires extra width |
Long unbroken text in a colSpan(M) cell over fixed columns |
Same fix — multi-line cells via lines(...) |
incompatible types: possible lossy conversion from double to float on .margin(...) |
DocumentInsets accessor returns double, the float overload narrows |
Switch the call to .margin(layout.margin()) (the DocumentInsets overload) |
GenerateAllExamples dies mid-run on a specific PDF |
Windows file lock from an open viewer | Ask the user to close the viewer; do not retry blindly |
cut-release.ps1 aborts at "Working tree has uncommitted changes" |
Untracked junk (zero-byte {,, 0) etc.) or unstaged pre-release fix |
Verify each is 0 bytes, delete by exact name; never git clean -fd blindly |
Each learning maps to a check above.
- v1.5.0 — README slim from 778 → 151 lines broke 4 section-anchored doc tests in a single push. Mitigation: section B ("Architecture-guard suite explicitly green") runs the guard suite on every release prep regardless of doc edits.
- v1.5.0 — Visual regression baselines were Windows-rendered, CI is Linux. 1.9 % pixel drift exceeded
mismatchedPixelBudget(0). Mitigation: any new visual regression test ships with a non-zero, CI-calibrated budget from the start. - v1.5.0 —
develophad not merged a v1.4.1 hotfix onmain;git push origin develop:mainwas rejected with non-fast-forward mid-release. Mitigation: section A enforcesgit log origin/develop..origin/main --onelineis empty before any tag work. - v1.5.0 — README claimed "the current release is v1.5.0" before the tag existed; install snippets would have failed for any new user landing on the README in that window. Mitigation: section C pins the README install snippet to the previous published tag until JitPack confirms the new tag built; the flip is post-release (section 2.B step 2).
- v1.5.0 —
Fixed column 0 width 90 is smaller than required natural width 92.44only surfaced onexec:java, notmvn test. Mitigation: section B mandates a fullGenerateAllExamplesregen before every release. - v1.5.0 — 8 zero-byte junk files (
examples/p,,{,,[Help, etc.) crept into the working tree from accidental shell-output expansions. Mitigation: section A hard-gates ongit status --shortcleanliness, not just on the script's pre-flight. - v1.6.0 prep — slimming the README to a marketing landing renamed the canonical
DocumentSession document = …example variable todoc, which silently brokeDocumentationCoverageTest.readmeShouldUseCanonicalDslAndAvoidLegacyApisbecause the test asserts the literal stringdocument.pageFlow(is present. Mitigation: any rewrite of the README "Hello world" snippet must keepDocumentSession documentas the variable name anddocument.pageFlow(,document.buildPdf(),GraphCompose.document(as the literal canonical fingerprints the guard scans for. Renaming the variable is a guard-test break, not a stylistic preference. - v1.6.0 post-release — the
examples-generationCI job introduced after v1.6.0 went red on the first run becauseexamples/pom.xmlandbenchmarks/pom.xmldeclare a<graphcompose.version>property used by theirgraphcomposedependency, andcut-release.ps1was only flipping the project's own<version>tag (the first<version>in each file). The subordinate POMs kept<graphcompose.version>1.6.0-beta.1</graphcompose.version>after the release commit; CI couldn't resolve a1.6.0-beta.1artifact (it never existed on any registry), somvnw -f examples/pom.xml clean compilefailed at dependency resolution. Mitigation:Update-PomVersionincut-release.ps1now flips both the first<version>tag and a<graphcompose.version>...</graphcompose.version>property if present, in the same call. Future agents need not touch this — running the script handles both. - v1.6.5 prep — the subordinate-POM
<graphcompose.version>property flip from the v1.6.0 lesson above is now superseded:examples/andbenchmarks/were converted to a reactor under a non-publishedaggregator/pom.xml, so they inherit their version fromgraphcompose-buildand declare<graphcompose.version>${project.version}</graphcompose.version>instead of a literal. The librarypom.xmlstays standalone, so JitPack coordinates never change. Version drift is now structurally impossible and caught byVersionConsistencyGuardTest(wired into CI's guard job and the section 0.B gate).cut-release.ps1bumps the library pom, the aggregator, both inherited parent refs, and the README install snippets in one commit;.github/workflows/release.ymlthen gates the tag onverifyand publishes the GitHub Release automatically. Mitigation: section 0.D verifies all four version sites agree; the guard fails the verify gate if any hand-edit leaves them out of sync. - v1.6.5 cut —
cut-release.ps1Step 4 (ShowcaseSync) aborted withCould not find artifact io.github.demchaav:graphcompose:jar:1.6.5 in centralafter Step 1 bumped the four pom.xml files to1.6.5: the examples module depends ongraphcompose:${project.version}, the previous release (1.6.4) was the only version in the local~/.m2, and Step 4 had no install gate to put the just-bumped version there first. Cut had to be finished by hand — install root, re-run ShowcaseSync, verify, commit, tag, push. Mitigation:Run-ShowcaseSyncnow runs./mvnw -B -ntp -DskipTests install -pl .immediately beforeexec:java, in both Release and PostReleaseOnly modes; the dry-run preview shows both steps. Pre-flight branch / clean / sync gates are now relaxed for-DryRunso the script can be previewed from a feature branch while iterating on it.
- Force-move a tag that Maven Central has already validated or that JitPack has already built — Central rejects re-upload of the same coordinates, JitPack caches by tag SHA. Publish a new patch tag instead.
- Skip the
origin/main → developmerge before tagging. - Use
git add .orgit add -A— the develop tree often has accidental untracked junk. Stage by exact filename. - Skip the full
GenerateAllExamplesregen —mvn testdoes not catch runtime layout exceptions in fixed-column tables. - Suppress a guard test or extend its allowlist to make the build green. Fix the source, or write an ADR documenting the carve-out before changing the guard.
- Commit a release with
Co-Authored-By: Claude(or any other tooling-attribution trailer). Releases are authored asDemchaAVonly. - Run
cut-release.ps1without explicit human approval in the chat for the specific version being cut. "Approved last release" does not approve this one.
The release is done when all of these are true:
- Tag visible at
https://github.com/DemchaAV/GraphCompose/releases/tag/v<version> - GitHub Release created with the CHANGELOG
v<version>body - CI green on
mainfor the tag commit -
.github/workflows/publish.ymlsucceeded for the tag - Maven Central artefact resolves:
mvn dependency:get -DgroupId=io.github.demchaav -DartifactId=graph-compose -Dversion=<version>exit 0 -
mvn dependency:resolvesucceeds against the README install snippet - README install snippets read
<version>(flipped by the release commit;VersionConsistencyGuardTestgreen) -
developandmainsynced at the same SHA - Working tree clean on develop (
git status --shortempty) -
ShowcaseMetadata.GH_BASEflipped back to/blob/develop(runcut-release.ps1 -PostReleaseOnly)
If any line is unchecked, the release is not done — even if the tag is up.