Fix Maven Central publishing and version artifacts as <semver>-<date> - #235
Merged
Conversation
The last consumable release is 0.0.2 (July 2025). Since the vanniktech plugin
moved to the Central Portal API, every tagged release has uploaded its
deployment as USER_MANAGED — it parks in the Portal waiting for a manual
Publish click while Gradle reports BUILD SUCCESSFUL. `automaticRelease = true`
releases it, and the default `validateDeployment` turns a Central rejection
into a red job instead of a green one that published nothing.
`trailblaze-android-gradle` has never published at all. The aggregator that
delegates into the composite-included build hung off `tasks.matching { name ==
"publishAllPublicationsToMavenCentralRepository" }` in the root project, which
applies no publish plugin and therefore has no task of that name — nothing ever
depended on it. Registering the aggregator *under* that name is what makes the
release command reach the included build.
That included build reads its own gradle.properties and never saw the git-tag
version override, so it would have released its SNAPSHOT default. release.yml
now passes `-Pversion=<tag minus v>`, which propagates into the composite and
puts the plugin on the same YYYY.MM.DD coordinate as the libraries.
Release triggers narrow from `**` to `v*`: throwaway tags like `backup/…`
matched and would have run a publish at the SNAPSHOT version. Snapshot
publishing on `main` is unchanged — SNAPSHOT versions bypass the deployment
flow entirely, so `automaticRelease` is inert there.
Fixes #226
A bare `2026.08.11` coordinate sorts ABOVE any future `1.0.0` in both Gradle's conflict resolution and Maven's ComparableVersion — 2026 beats 1 on the first numeric part — so publishing CalVer would foreclose a stable semver line permanently, with no epoch escape hatch short of renaming the artifacts. Since only 0.0.1/0.0.2 have ever been released, the door is still open. The Maven version is now `<semver base from gradle.properties>-<tag date>`, e.g. `0.1.0-2026.08.11`. The base carries the API-compatibility signal and is bumped by hand; the date says when the build was cut. Verified in both Gradle and Maven: the date orders within a base, the base dominates the date, and `0.9.0-<date>` < `1.0.0`, so graduating to a bare stable release works. One rule this shape imposes, documented at the derivation site: a dated build of the same base outranks the bare version (`1.0.0-2026.11.01` > `1.0.0`), so don't ship dated builds of a version you intend to release bare — `-rc-<date>` sorts below the final in both tools.
handstandsam
force-pushed
the
fix-maven-central-publishing
branch
from
August 11, 2026 14:25
004484d to
39db0cf
Compare
Nothing structurally prevented a CalVer coordinate from shipping. The release tag is `vYYYY.MM.DD`, so the tag name is the obvious thing to feed to `-Pversion`, and `gradle/git-version.gradle.kts` already sets `version = 2026.08.11` on any build whose HEAD carries a release tag — which is exactly the state a release runs in. One correct line in release.yml was the only thing standing between a tagged build and an irreversible mistake. `gradle/maven-version-guard.gradle.kts` fails the publish if the version's leading component is a calendar year, wired into `prepareMavenCentralPublishing` (the earliest task on the Central path, so nothing is staged, signed, or uploaded) and every PublishToMavenRepository as a backstop. Applied by both the root build and the trailblaze-android-gradle composite build, which needs its own copy — a separate Gradle build the root's apply() doesn't reach, and the one most exposed since its version arrives via propagated `-Pversion`. Scoped to the Central publish path on purpose: the git-version override puts a CalVer version on every ordinary build made from a tagged commit, and those must keep working. publishToMavenLocal is untouched. release.yml asserts the same rule before Gradle starts, so the constraint stays visible where the version is composed. Verified: bare CalVer blocked on both builds; `0.1.0-2026.08.11` passes the guard and proceeds to the real upload; SNAPSHOT builds unaffected.
Both tools sort -rc-<date> and -rc.N below the bare release, but numbered rc qualifiers are the established convention and the dot is what keeps rc.10 sorting above rc.2.
Four fixes from a pre-merge review pass:
- The CalVer guard split only on '.', so a dash/underscore CalVer version
(2026-08-11) passed it while still outranking 1.0.0 — Gradle and Maven split
version parts on '.', '-', and '_' equivalently. The guard now splits on all
three.
- Tag triggers tightened from v* to v[0-9]* (release.yml and build-desktop.yml),
and release.yml now asserts the full vYYYY.MM.DD[.N] shape plus a strict
<major>.<minor>.<patch> check on the extracted base. With automaticRelease
there is no portal gate left, so a stray tag like 'vendor-x' would otherwise
have shipped immutable 0.1.0-endor-x clutter to Central; the base check also
catches a duplicate version= line in gradle.properties.
- The computed version now reaches the publish step via env instead of ${{ }}
interpolation into the run script — it derives from the tag name, and inlining
it into the step that holds the publish secrets is the standard
template-injection shape. Also: permissions: contents: read.
- The root build delegates publishToMavenCentral and
publishAndReleaseToMavenCentral into the included build too; those vanniktech
alias names would otherwise reintroduce the missing-plugin bug on manual runs.
A comment warns that --dry-run executes delegated included-build tasks for
real.
Docs: the "one coordinate" claim was wrong — androidx.test:runner is not in
trailblaze-android's published graph (only monitor, as implementation), so the
snippet now includes it, and custom-llmClient consumers are pointed at the koog
client dependency.
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.
Fixes #226.
After this PR: pushing a
vYYYY.MM.DDtag publishes every library module and thetrailblaze-android-gradleplugin to Maven Central as0.1.0-YYYY.MM.DD, fully automatically. Merges tomainkeep publishing0.1.0-SNAPSHOTto the Central snapshot repository, unchanged. Today the last consumable release is0.0.2(July 2025) and the Gradle plugin has never been published, despite every release run since reporting success.Why nothing landed since 0.0.2
Three independent defects:
1. Deployments uploaded but never published.
publishToMavenCentral()defaults toautomaticRelease = false: under the Central Portal API the deployment uploads asUSER_MANAGEDand waits for a manual Publish click in the portal — while Gradle printsBUILD SUCCESSFUL. (The v2026.08.07 job log ends withSkipping deployment validation!, emitted from inside that upload block.) NowautomaticRelease = true, and the defaultvalidateDeployment = trueblocks the job until Central reaches a terminal state — a rejected deployment turns the job red instead of green-but-published-nothing.2. The Gradle plugin was never in the release task graph. The aggregator delegating into the composite-included
trailblaze-android-gradlebuild hung offtasks.matching { name == "publishAllPublicationsToMavenCentralRepository" }in the root project, which applies no publish plugin — that container is empty, so nothing ever depended on the aggregator. Registering the delegating task under that name (plus vanniktech'spublishToMavenCentral/publishAndReleaseToMavenCentralaliases) is what makes the release command reach the included build.3. The included build would have released its own SNAPSHOT default. It reads its own
gradle.propertiesand never sees the root's git-tag version override. The workflow now passes-Pversionexplicitly, which propagates into the composite (verified with an init-script probe across all three builds), putting the plugin on the same coordinate as the libraries.Versioning:
<semver base>-<tag date>The tag stays
vYYYY.MM.DD, but the Maven coordinate is not the bare tag.2026.08.11sorts above any future1.0.0in both Gradle's conflict resolution and Maven'sComparableVersion(2026 beats 1 on the first numeric component), so one bare-CalVer release would permanently foreclose a stable semver line — Maven coordinates have no epoch escape hatch. Only0.0.1/0.0.2exist on Central, so the door is still open; this PR is what keeps it open.The version is
<semver base from gradle.properties>-<date from the tag>, e.g.0.1.0-2026.08.14:gradle.propertiesfiles, root and plugin) only when the contract moves — it is not bumped per release;Ordering verified against both Gradle 8.14's real conflict resolution and Maven's
ComparableVersion— they agree on every case:0.0.2(current Central max)0.1.0-2026.08.140.1.0-2026.08.110.1.0-2026.09.010.1.0-2026.08.110.2.0-2026.08.010.9.0-2026.12.011.0.01.0.0— graduating to a bare stable release works1.0.02026.08.112026.08.11— the trap this scheme avoidsOne rule the shape imposes, documented at the derivation site: a dated build of the same base outranks the bare version (
1.0.0-2026.11.01>1.0.0), so never ship dated builds of a version you intend to release bare — numbered-rc.Nqualifiers (1.0.0-rc.1) sort below the final in both tools, and the dot keepsrc.10aboverc.2.Guardrails: bare CalVer can never ship
Nothing structurally prevented the mistake:
gradle/git-version.gradle.ktssetsversion = 2026.08.14on any build whose HEAD carries av*tag — exactly the state a release runs in — so one workflow line was the only defense. Now:gradle/maven-version-guard.gradle.ktshard-fails any Maven-Central publish whose version's leading component is a calendar year, hooked intoprepareMavenCentralPublishing(before anything is staged, signed, or uploaded) and everyPublishToMavenRepositoryas backstop. It splits on.,-, and_— Gradle and Maven treat all three as part separators, so2026-08-11is as poisonous as2026.08.11. Applied by the root build and (separately — the root'sapply()can't reach a composite build) bytrailblaze-android-gradle. Ordinary builds,publishToMavenLocal, and SNAPSHOT publishing are untouched.v[0-9]*(release + build-desktop;**previously matchedbackup/…tags), and release.yml asserts the fullvYYYY.MM.DD[.N]shape plus a strict<major>.<minor>.<patch>base — withautomaticReleasethere is no portal gate left, so a strayvendor-xtag would otherwise ship immutable0.1.0-endor-xclutter to Central. The base check also catches a duplicatedversion=line.env:rather than${{ }}inside therun:that holds the publish secrets, and the workflow declarespermissions: contents: read.Docs
docs/android_on_device.mdnow names thetrailblaze-androidcoordinate (nothing in the docs ever did), including theandroidx.test:runnerline that is not part of the published graph, and explains the version format. The plugin README links its Central artifact.Verification
--dry-runbefore/after: the included build was absent from the graph before, present after, alongside all 14 subproject publish tasks.-PversionandORG_GRADLE_PROJECT_*propagation into all three composite builds probed directly.2026.08.11and2026-08-11blocked before any staging;0.1.0-2026.08.11passes through to the real upload; SNAPSHOT unaffected../gradlew -p trailblaze-android-gradle publishToMavenLocalproduces jar, sources jar,.module, POM, and thexyz.block.trailblaze.android-gradle.gradle.pluginmarker POM.mkdocs build --strictpasses. No javadoc-jar risk: 0.0.2 published sources-only under the sameJavadocJar.None()config.Operational notes
USER_MANAGEDdeployments, one per tag since the Portal migration, each at a bare CalVer version. They are invisible to consumers and block nothing — but whoever has portal access should drop them (not publish: publishing any one of them springs the CalVer trap permanently).--dry-rundoes not extend into the included build — delegated tasks execute for real. Don't sanity-check the publish aggregators locally with credentials exported (now warned in a comment at the aggregator).