Publish to Maven Central with zpublish, a portable single-file publisher - #4
Open
senleft wants to merge 1 commit into
Open
Publish to Maven Central with zpublish, a portable single-file publisher#4senleft wants to merge 1 commit into
zpublish, a portable single-file publisher#4senleft wants to merge 1 commit into
Conversation
…isher Fixes AdamBien#3. Z* tools cannot be onboarded where the only approved intake path is an internal Maven mirror. ## Read this first — namespace gate This PR is dead weight until one thing is confirmed: **`com.airhacks` must be a verified namespace in your Central Portal account.** The newest artifact under `com.airhacks` dates from 2020-06-08, five years before the OSSRH sunset. Sonatype says all OSSRH namespaces were migrated to the Central Portal, but publishing still depends on using an account that has access to the migrated namespace. Artifacts on `repo1.maven.org` prove the coordinate exists, not that the current account can publish to it. Check Portal → Namespaces before adding any secrets. Log in with the same username/password account used for OSSRH. If `com.airhacks` is still missing, contact Central Support so they can restore the existing publishing access; do not register a replacement namespace or change the coordinate as a first response. ## Problem Corporate environments mirror open source through Nexus or Artifactory proxying Central. The intake is automated and GitHub is usually blocked. zb ships only via GitHub Releases, so in those environments it can't be adopted at all. Publishing to Central needs no per-company setup — every mirror already proxying Central proxies `com.airhacks:zb`: ```bash mvn dependency:get -Dartifact=com.airhacks:zb:<version> curl -O https://<mirror>/com/airhacks/zb/<version>/zb-<version>.jar ``` Central versions are immutable by definition. GitHub can also lock future release assets when release immutability is enabled, but that still does not make them available through corporate mirrors of Central. ## Approach No `pom.xml`, no Maven, no Gradle, no JReleaser. zb keeps building itself with zb. A single-file Java 25 script assembles the Central bundle by hand — jar, POM, sources jar, javadoc jar, each with a detached GPG signature and MD5/SHA-1 — and uploads it to the Central Portal. JReleaser was the obvious alternative and was rejected: with no Maven or Gradle in the repo it still needs the staging layout and POM built by hand, so it saves less than it looks like while adding a ~40 MB CI download. A publish-only `pom.xml` was rejected for putting a `pom.xml` in the repository of a tool whose pitch is that you don't need one. ## What this adds 6 files, 4605 insertions, 1 deletion. | Path | | | --- | --- | | `zpublish` | new, 4139 lines (1897 production + 2242 in-script tests) | | `.github/workflows/publish-central.yml` | new, 312 lines — `pull_request` validation plus a `workflow_dispatch` `preflight` → `build` → `publish` chain | | `.zpublish` | new, 28 lines — zb's POM metadata, and the worked example siblings copy | | `README.md` | Corporate/Maven install, Releasing, adoption recipe | | `llms.txt` | Maven coordinates | | `AGENTS.md` | short Publishing section | `.github/workflows/release.yml` is untouched. Nothing under `src/` changes. ## Portability — this is not a zb-only tool `zpublish` has no zb-specific code. Configuration splits along ownership: - `.zb` supplies build paths — `jar.dir`, `jar.file.name`, `classpath`. - `.zpublish` supplies POM metadata. - Sources, resources and `version.txt` are **discovered**, matching `AppArguments:45-46`. Note `sources.dir` and `resources.dir` in `.zb` are inert — zb writes them and never reads them — so honouring them would have created drift rather than preventing it. Proven on two other projects, not just zb: dry-runs in throwaway copies of `zsmith` and `lightmetal`, from an unmodified `zpublish` plus a `.zpublish` each. Both resolve their own layout — zsmith's nested module and module-root `version.txt`, lightmetal's `lightmetal.jar`. Every metadata element of the generated zsmith POM matches the hand-written `zsmith/zsmith/pom.xml`, and both generated POMs validate against `maven-4.0.0.xsd`. Adoption elsewhere is: copy `zpublish`, write a `.zpublish`, run `-dry-run`. ## Verification ```bash java --source 25 zpublish -selftest # 644 assertions java --source 25 zpublish -dry-run # stage, sign, checksum, bundle - no upload ``` Both run on every pull request. The selftest binds a loopback `HttpServer` on an ephemeral port — no traffic reaches Central, despite log lines that say `PUBLISHED`. Not verified, and not verifiable locally: the authenticated upload. There is no Central sandbox. That's why `publishingType` defaults to `USER_MANAGED` — a deployment stops at `VALIDATED` and waits for a human to release it, so a bad first attempt gets dropped in the Portal instead of becoming an immutable release. `-automatic` opts out and cannot be undone. ## Two things to push back on **The file is 4139 lines.** In a repo that ships a 30 KB jar, that's the obvious objection. 2242 of those lines are the in-script test suite; production is 1897. The tests live in the script because `zpublish` sits outside `src/` and zunit can't reach it. Splitting them into a second file would make adoption a two-file copy, which defeats the point. Happy to split anyway if you'd rather. **A generic publisher inside `zb` is backwards.** It serves the whole family from inside one member, and every sibling that adopts it copies from a repo it otherwise has nothing to do with. The right end state is its own repository with zb consuming it like any other sibling. That stays cheap precisely because the file has no zb-specific code left. Say the word and I'll extract it instead. ## Setup required from you Five repository secrets: | Secret | | | --- | --- | | `CENTRAL_TOKEN_USERNAME`, `CENTRAL_TOKEN_PASSWORD` | Central Portal **user token**, not the account password | | `GPG_PRIVATE_KEY` | ASCII-armored private key | | `GPG_PASSPHRASE` | its passphrase | | `GPG_KEY_ID` | key id or fingerprint | The public key must be on a keyserver Central supports — `keys.openpgp.org`, `keyserver.ubuntu.com`, or `pgp.mit.edu` — or validation fails. Until those exist the PR changes nothing — the publish job fails fast on missing secrets and nothing else in the repo is affected. `workflow_dispatch` also only appears in the Actions tab once the workflow is on `main`, so this can't be exercised from the PR branch. ## Releasing after merge `release.yml` is unchanged: every push to `main` still tags and attaches to a GitHub Release. Central is separate and on demand — run `publish-central` from the Actions tab with the tag as input. It checks out that tag, stamps `version.txt` with the Maven version, rebuilds, and publishes. One consequence of that stamp: the Central jar's `Implementation-Version` is the full Maven version (`2026.08.16.01.19`), where the GitHub Release asset carries `2026.08.16.01`. Without it `java -jar zb.jar` would print a version resolvable in no repository. It's one line in the workflow if you'd rather drop it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ur3dbMkDBneKoGkiSVPzmx
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 #3.
Z* tools cannot be onboarded where the only approved intake path is an internal Maven mirror.
Read this first — namespace gate
This PR is dead weight until one thing is confirmed:
com.airhacksmust be a verifiednamespace in your Central Portal account.
The newest artifact under
com.airhacksdates from 2020-06-08, five years before the OSSRHsunset. Sonatype says all OSSRH namespaces were migrated to the Central Portal, but publishing
still depends on using an account that has access to the migrated namespace. Artifacts on
repo1.maven.orgprove the coordinate exists, not that the current account can publish to it.Check Portal → Namespaces before adding any secrets.
Log in with the same username/password account used for OSSRH. If
com.airhacksis stillmissing, contact Central Support so they can restore the existing publishing access; do not
register a replacement namespace or change the coordinate as a first response.
Problem
Corporate environments mirror open source through Nexus or Artifactory proxying Central. The
intake is automated and GitHub is usually blocked. zb ships only via GitHub Releases, so in
those environments it can't be adopted at all.
Publishing to Central needs no per-company setup — every mirror already proxying Central
proxies
com.airhacks:zb:Central versions are immutable by definition. GitHub can also lock future release assets when
release immutability is enabled, but that still does not make them available through corporate
mirrors of Central.
Approach
No
pom.xml, no Maven, no Gradle, no JReleaser. zb keeps building itself with zb. Asingle-file Java 25 script assembles the Central bundle by hand — jar, POM, sources jar,
javadoc jar, each with a detached GPG signature and MD5/SHA-1 — and uploads it to the Central
Portal.
JReleaser was the obvious alternative and was rejected: with no Maven or Gradle in the repo it
still needs the staging layout and POM built by hand, so it saves less than it looks like while
adding a ~40 MB CI download. A publish-only
pom.xmlwas rejected for putting apom.xmlinthe repository of a tool whose pitch is that you don't need one.
What this adds
6 files, 4605 insertions, 1 deletion.
zpublish.github/workflows/publish-central.ymlpull_requestvalidation plus aworkflow_dispatchpreflight→build→publishchain.zpublishREADME.mdllms.txtAGENTS.md.github/workflows/release.ymlis untouched. Nothing undersrc/changes.Portability — this is not a zb-only tool
zpublishhas no zb-specific code. Configuration splits along ownership:.zbsupplies build paths —jar.dir,jar.file.name,classpath..zpublishsupplies POM metadata.version.txtare discovered, matchingAppArguments:45-46.Note
sources.dirandresources.dirin.zbare inert — zb writes them and never readsthem — so honouring them would have created drift rather than preventing it.
Proven on two other projects, not just zb: dry-runs in throwaway copies of
zsmithandlightmetal, from an unmodifiedzpublishplus a.zpublisheach. Both resolve their ownlayout — zsmith's nested module and module-root
version.txt, lightmetal'slightmetal.jar. Every metadata element of the generated zsmith POM matches the hand-writtenzsmith/zsmith/pom.xml, and both generated POMs validate againstmaven-4.0.0.xsd.Adoption elsewhere is: copy
zpublish, write a.zpublish, run-dry-run.Verification
Both run on every pull request. The selftest binds a loopback
HttpServeron an ephemeralport — no traffic reaches Central, despite log lines that say
PUBLISHED.Not verified, and not verifiable locally: the authenticated upload. There is no Central
sandbox. That's why
publishingTypedefaults toUSER_MANAGED— a deployment stops atVALIDATEDand waits for a human to release it, so a bad first attempt gets dropped in thePortal instead of becoming an immutable release.
-automaticopts out and cannot be undone.Two things to push back on
The file is 4139 lines. In a repo that ships a 30 KB jar, that's the obvious objection.
2242 of those lines are the in-script test suite; production is 1897. The tests live in the
script because
zpublishsits outsidesrc/and zunit can't reach it. Splitting them into asecond file would make adoption a two-file copy, which defeats the point. Happy to split
anyway if you'd rather.
A generic publisher inside
zbis backwards. It serves the whole family from inside onemember, and every sibling that adopts it copies from a repo it otherwise has nothing to do
with. The right end state is its own repository with zb consuming it like any other sibling.
That stays cheap precisely because the file has no zb-specific code left. Say the word and
I'll extract it instead.
Setup required from you
Five repository secrets:
CENTRAL_TOKEN_USERNAME,CENTRAL_TOKEN_PASSWORDGPG_PRIVATE_KEYGPG_PASSPHRASEGPG_KEY_IDThe public key must be on a keyserver Central supports —
keys.openpgp.org,keyserver.ubuntu.com, orpgp.mit.edu— or validation fails.Until those exist the PR changes nothing — the publish job fails fast on missing secrets and
nothing else in the repo is affected.
workflow_dispatchalso only appears in the Actions tabonce the workflow is on
main, so this can't be exercised from the PR branch.Releasing after merge
release.ymlis unchanged: every push tomainstill tags and attaches to a GitHub Release.Central is separate and on demand — run
publish-centralfrom the Actions tab with the tag asinput. It checks out that tag, stamps
version.txtwith the Maven version, rebuilds, andpublishes.
One consequence of that stamp: the Central jar's
Implementation-Versionis the full Mavenversion (
2026.08.16.01.19), where the GitHub Release asset carries2026.08.16.01. Withoutit
java -jar zb.jarwould print a version resolvable in no repository. It's one line in theworkflow if you'd rather drop it.