build(publish): ship the release train to Maven Central as one deployment - #803
Merged
Merged
Conversation
…ment publish.yml deployed the lockstep train as eight separate `-f <module>/pom.xml -P release deploy` runs, so one GraphCompose version was eight Central deployments - eight Release Count events, eight validation queues, eight Publish clicks - and a failure part-way left the first modules of a version published and the rest missing. The train now deploys in one reactor run over the root aggregator, `-P release deploy -pl` the eight train artifacts. The central-publishing plugin stages each module and uploads one central-bundle.zip from the last, so a version is one deployment holding eight components. The root aggregator, the build-only modules, fonts and emoji are not selected; fonts and emoji keep their own workflows. start_at is replaced by skip_published (boolean, default false), passed as -DignorePublishedComponents for recovering a partially published version. The uploaded zip is kept as a workflow artifact. PublishTrainGuardTest holds the deploy to one -pl run over exactly the lockstep modules derived from the poms, forbids also-make, keeps fonts, emoji and build-only modules out, requires identical plugin declarations across the train, and keeps the recovery switch opt-in. PublishedModules reads -pl deploys so the CodeQL scope guard keeps its inventory.
--staged-repo <dir> / -StagedRepo <dir> resolves the GraphCompose coordinates from a Maven repository-layout directory - the unzipped central-bundle.zip - and everything else from Central, through a settings file whose Central-only mirror excludes that one repository. The version defaults to the single core version staged there. A scenario passes only if every GraphCompose artifact of that version it resolved records the staged repository in _remote.repositories, so a stale cache or a Central copy cannot stand in for the staged bytes, and a scenario that resolved none fails. Staged mode refuses --warm.
The runbook explains the one-deployment train, how to dry-run it against a dead endpoint and smoke the staged bundle, and replaces the per-module start_at recovery with the failure cases a single deployment has. CHANGELOG entry under v2.4.2.
…per file PublishTrainGuardTest fails on any non-comment line of a publish workflow that names the deploy goal in a shape PublishedModules does not read (`mvn`, a `- run:` list item, a `\` continuation), so a second deployment cannot ship invisible to the one-deploy and train checks. The deploy step's id and the install step's name no longer contain the word. The staged release smoke checks every file line of _remote.repositories, not one line per artifact, and fails a train artifact resolved at any version other than the staged one: a staged POM pinning a sibling at a drifted version would otherwise pull it from Central unchecked. fonts and emoji stay exempt. The settings file is written without a BOM, its path is XML-escaped, and run.sh converts the path with cygpath where it exists. The bundle artifact uploads whenever the deploy step ran rather than always. The runbook adds the validation-timeout case (do not re-dispatch: a deployment exists) and smoking the VALIDATED deployment's bundle artifact before publishing. The root aggregator's comment describes the reactor deploy.
…ter an upload A workflow_dispatch runs the workflow file of the ref it is dispatched from, and main - the Actions UI default - is merged only after the tag, so a recovery dispatched from main could run the previous per-module publish workflow. Every recovery now dispatches from the tag: gh workflow run publish.yml --ref vX.Y.Z -f tag=vX.Y.Z. Once the log shows "Uploaded bundle successfully ... deploymentId", a deployment exists whatever turned the job red afterwards; the plugin does not retry its status polling, so one transient error is enough. The runbook now states that rule once and acts on the deployment's portal state instead of re-dispatching, which would upload a second deployment of the same coordinates. Upload rejections without a deploymentId (a bad token, an outage) get their own row; the staged smoke is documented as unavailable for a skip_published bundle. The release smoke refuses an empty --staged-repo / -StagedRepo value, which used to switch staged mode off and smoke the published default from Central. run.sh reads a final _remote.repositories line without a newline and infers the staged version from directories only, matching run.ps1; run.ps1 requires the staged path to be a directory. PublishTrainGuardTest requires the deploy line to invoke Maven once, chain no other command and carry one module selection, treats a direct central-publishing publish goal as an upload, holds SKIP_PUBLISHED to a single definition, and requires one plugin declaration per train pom. PublishedModules reads every -pl / --projects selection. The bundle artifact name carries the run attempt, and the concurrency comment records that GitHub keeps one pending run per group.
… never re-run after an upload A tag cut before the single-deployment workflow carries the old publish workflow, which declares start_at and no skip_published, so dispatching it from the tag with -f skip_published=true is rejected as an unexpected input. The partial-publication recovery now names both cases: skip_published for tags cut with the single-deployment workflow, the tag's own start_at for v2.4.1 and earlier. "Re-run failed jobs" after the log shows deploymentId uploads a second deployment just as a dispatch does. The rule now covers both, and names a re-run of the tag's own run as the other way to get the tag's workflow file. The publish.yml header and deploy-step comment no longer suggest running the workflow again after an upload. A validated-but-wrong deployment needs a patch version when the cause is in the content. PublishTrainGuardTest reads deploy tokens unquoted, treats a single `&` as chaining, and counts the --projects prefixes Maven accepts as a module selection; PublishedModules reads those spellings too. A central-publishing plugin block is recognised whatever order its child elements are written in. run.sh refuses an empty --version= value; run.ps1 resolves the staged path literally.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
publish.ymldeployed the lockstep train as eight separate./mvnw -f <module>/pom.xml -P release deployruns. Thecentral-publishing-maven-pluginbuilds and uploads one bundle per Maven run, so one GraphCompose version became eight Central deployments: eight validation queues, eight Publish clicks (autoPublish=false), and eight entries against the organisation's monthly Release Count, which Central defines as one per "distinct publish operation" and enforces from 2026-10-01. A failure part-way also left the first modules uploaded as separate deployments and the rest missing, which is what thestart_atresume input existed to patch.What moved
One reactor run, one deployment. The eight deploy steps and the
start_atplan step become one step:./mvnw -B -ntp -P release -DskipTests -Dgpg.skip=false -DdeploymentName="GraphCompose $TAG" -DignorePublishedComponents=$SKIP_PUBLISHED deploy -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-bundleMojoUtilsImpl.isThisLastProjectWithThisMojoInExecution). A failing module stops the build before that last module, so nothing partial is uploaded.-plover the root aggregator is notdeployfrom the root: the aggregator itself is not selected, and no train module inherits itsmaven.deploy.skip(they are standalone poms). examples, benchmarks, qa, coverage, fonts and emoji are not selected. publish.yml's japicmp step already uses the same-plpattern.Recovery is opt-in.
start_atis replaced by a booleanskip_publisheddispatch input (defaultfalse), passed as-DignorePublishedComponents: the plugin asks the Portal which components are already published and leaves them out. On a tag push it is alwaysfalse.The bundle is kept. The uploaded
central-bundle.zipis a workflow artifact (central-bundle-v<X.Y.Z>-attempt-<N>; the attempt keeps a re-run from colliding with an earlier attempt's artifact), so the exact bytes can be audited or smoked before the Publish click.PublishTrainGuardTest(new, 7 tests) holdspublish.ymlto that shape. Its train is derived from the poms (standalone, declares the plugin, on the reactor version), not restated. It requires:-pl/--projects);-am, which would pull fonts and emoji (core's test-scope deps) into the deployment, and no-amd;-plset equal to the derived train, with fonts, emoji and the build-only modules never deployed;deployor directcentral-publishing…:publishline in apublish*.ymlto be one the parser reads;ignorePublishedComponents/skipPublishing/excludeArtifacts, since a pom value overrides the workflow's-D;skip_publishedto stay a boolean defaulting tofalse, with a singleSKIP_PUBLISHEDdefinition.PublishedModulesnow reads-pl/--projectsdeploys as well as-fones, soCodeQlScopeGuardTestkeeps its deployed-module inventory. Itsorder=-based train test is gone with theorder=line; the train is held against the poms by the new guard.Release smoke before publishing.
scripts/release-smoke/run.sh --staged-repo <dir>/run.ps1 -StagedRepo <dir>runs the nine consumer scenarios against an unzipped bundle: the GraphCompose coordinates resolve from<dir>through a settings file whose Central-only mirror excludes that one repository, everything else from Central.stagedin_remote.repositories, and no train artifact resolved at another version (fonts and emoji, independently versioned, are exempt).--staged-repovalue is refused rather than silently smoking the published default.Runbook (
docs/contributing/release-process.md): §2.F explains the one-deployment train with a dead-endpoint dry-run recipe, and step 9 adds the smoke-the-VALIDATED-bundle step. The recovery table is rewritten around two rules:--ref v<X.Y.Z>, or re-run the tag's run), since a dispatch uses the workflow file of the ref it is dispatched from andmainis merged after the tag;deploymentId, never run the deploy again, because the plugin does not retry status polling and a second run uploads a second deployment.Partial publication of a tag cut before this change uses that tag's own
start_at.CHANGELOG.md(v2.4.2: Build / Tests / Documentation), the rootpom.xmlcomment and aci.ymlcomment describe the reactor deploy.Verification
./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose-qa,:graph-compose-coverage -am→ BUILD SUCCESS, 3260 tests, 0 failures, 2 skipped (both pre-existing).PublishTrainGuardTest7/7,CodeQlScopeGuardTest3/3,BinaryCompatibilityGateGuardTest7/7.publish.yml/bundle/pom.xmleach turn the intended test red:-pl,-am, fonts added to the train (plain, a second-pl,--projects=,--proj), a typo'd selector;- run:item,\continuation,mvn,&&,&with quoted tokens, a direct pluginpublishgoal);true, a secondSKIP_PUBLISHED);autoPublish,skipPublishing, a second artifactId-first plugin block).publish.yml, in a throwaway worktree with a separate local repository, versions flipped to2.4.2,-DcentralBaseUrl=http://127.0.0.1:9, signed with a throwaway key:Created bundle successfully, oneGoing to upload(deployment namedGraphCompose v2.4.2), thenConnection refused;.asc, no SNAPSHOT or aggregator reference in any POM;index.html,GraphCompose.htmlandDocumentSession.html, built from core's sources inside the same reactor.run.sh --staged-repoandrun.ps1 -StagedRepo: 9/9 scenarios each, every train file resolved from the bundle, fonts 1.1.0 and emoji 1.0.0 from Central.2.4.1(not in the bundle): 9/9 Maven builds succeed and all 9 scenarios fail provenance.2.4.1: exactly the 5 scenarios that consume the wrapper fail ("resolved at 2.4.1, not the staged 2.4.2").Notes for review
skipPublishingas "creates only the bundle", but 0.11.0 drops every artifact before staging when it is set. Do not use it for dry runs; use a deadcentralBaseUrl, as the runbook recipe does.skip_publishedand the Portal round trip need credentials and a tag, so they are proven by the dry run and the plugin code, not by a live run. On the first release cut with this workflow, confirm:VALIDATED;waitMaxTime(1800 s) instead of 1800 s per module. A timeout afterdeploymentIdleaves a valid deployment; the runbook says to act on it in the portal, not to run the deploy again.publish-fonts.yml/publish-emoji.ymlstill publish them alone, on their own tags.Lane: build — release tooling (publish workflow, guard tests, release-smoke scripts, runbook); no library code, no module pom, no public API change.
Pre-merge checklist
develop(notmain); branch isbuild/central-single-deployment../mvnw -B -ntp clean verify(the reactor gate above) passes locally — this is the Verification proof above.CHANGELOG.mdcarries Build / Tests / Documentation entries under## v2.4.2 — Planned.