Publish core/core-noop to Maven Central as com.callstack.appduct - #60
Conversation
…ack.appduct Changes the Maven coordinates from `com.callstackincubator.appduct` to `com.callstack.appduct` and wires the publish. The old group could never have been published: Central verifies a namespace against the reversed domain, and callstackincubator.com is not a registered domain at all. callstack.com is, and is the verified namespace on the account. Only the Maven coordinates move. The Kotlin package and the AGP namespace stay `com.callstackincubator.appduct*` -- three unrelated concepts, and the Kotlin package is load-bearing: `appduct doctor`'s Android detection keys on that dex package name, so renaming it would silently change what the release gate inspects. Historical task docs keep the old coordinates; they are dated records. The publish itself uses no third-party plugin. Sonatype ships no official Gradle plugin for the Central Portal, and the community ones largely automate what publishing.gradle now does directly: stage a signed, checksummed repository layout on disk, which `publish-maven` zips and POSTs to the Portal API. Publishing to a *file* repository is what produces that layout, checksums included, without hand-rolling any of it. Signing is wired only when APPDUCT_SIGNING_KEY is set, so test.yaml's `publishToMavenLocal` keeps working unkeyed. Because Central rejects any unsigned file, the job asserts every staged artifact has a .asc before zipping rather than discovering it inside Central's validator. Uploads as USER_MANAGED: Central validates, then waits for a human to press Publish. A Central version can never be replaced or deleted, so the last irreversible step stays deliberate for now. Verified end to end with a throwaway GPG key (since destroyed): both modules stage, all 10 artifacts signed and checksummed, `gpg --verify` accepts a signature, the POM carries every field Central requires, the zip has the right layout with maven-metadata.xml excluded, and the unsigned-artifact assertion correctly fails an unkeyed build. Also confirmed `publishToMavenLocal` still succeeds with no key present.
#60 moved only the Maven coordinates, leaving the Kotlin package, the AGP namespace, the manifest meta-data keys, and the native playground's application id on com.callstackincubator.appduct. Two namespaces in one project is a standing invitation to reach for the wrong one, so this moves the rest. The reason the split was tempting to leave alone is also the reason it was dangerous: the knobs are technically independent, but one coupling is invisible. `appduct doctor`'s Android detection keys on the dex package name, so changing the Kotlin package silently changes what the release gate inspects. That coupling is now explicit rather than load-bearing folklore. `artifact-inspect.ts` therefore matches every Android marker against both `com.callstack.*` and the legacy `com.callstackincubator.*` spelling. `doctor` inspects artifacts it did not build -- an app compiled against Appduct <= 0.9.0 is exactly what a release gate meets -- and reporting `absent` for an app that genuinely bundles Appduct would rubber-stamp shipping it to production, the one failure this tool must never have (docs/CI.md: a broken check must fail loudly, never rubber-stamp). A new regression test covers a pre-rename artifact and fails if those legacy entries are dropped. The GitHub organisation is still `callstackincubator`, so every repository URL is untouched -- note `github.com/callstackincubator/appduct` contains the string `com/callstackincubator/appduct`, which a naive rename would have corrupted. Task documents keep the old spelling as dated records. Verified: core unit tests and core-noop assemble pass; the native playground builds Debug and Release; `doctor --assert-present` holds on the debug APK with all three signals and `--assert-absent` holds on release; lint, typecheck, and the 272 JS tests covering detection, the config plugin and the CLI all pass. The new regression test was confirmed to fail when the legacy markers are removed.
|
Added 582f49f: every namespace now reads You were right that leaving two was the bigger risk. The reason it was tempting to leave alone is also why it was dangerous: those knobs are technically independent, but one coupling is invisible — Backward compatibility (please review this bit)
One trap avoidedThe GitHub org is still Verification
Unrelated local flake worth noting: 14 daemon tests fail on my machine with |
publish-cocoapods skipped a version already on trunk; publish-maven had no equivalent, so re-running it could upload a second deployment of the same version. Two layers, because one check is not enough: 1. Already published: repo1.maven.org, OR the Portal's `published` endpoint on an explicit `"published": true`. Skip if either says so. 2. Uploaded but not yet published -- the likeliest re-run, e.g. the status poll timing out after a successful upload. That deployment is VALIDATED but unpublished, so layer 1 says "not published" and would upload again. The Portal API has no endpoint to list deployments, so the id is the only handle on it: the job now saves it as a run artifact immediately after uploading, *before* polling, and a re-run attempt restores it and resumes watching that deployment instead of uploading a duplicate. Both layers lean the same way. Only explicit evidence counts as "published", and only live states (PENDING, VALIDATING, VALIDATED, PUBLISHING, PUBLISHED) are resumed; FAILED, dropped, unparseable, or unrecognised answers all fall through to a fresh upload. A wrong skip would end a release green with nothing on Central; a wrong upload at worst leaves a duplicate for a human to drop. The upload step also now refuses any response that is not a bare deployment id, so a re-run can never try to resume junk. Verified by extracting the real `run:` blocks from the workflow and executing them under `bash -e` against a fake curl, 20 scenarios: every published/not-published/garbage/network-failure branch of the check, every deployment state plus dropped/errored/unknown for the reuse decision, and valid, newline-terminated, error, failed, and empty upload responses. actionlint is clean. Not verifiable locally: that a re-run attempt can download an artifact uploaded by an earlier attempt of the same run. This workflow already relies on that -- publish-* download `npm-packages` from `package` -- but not for an artifact written by the failing job itself.
Publishes
core/core-noopto Maven Central, and moves them to the namespace you actually own.Coordinates change
The old
com.callstackincubator.appductcould never have been published. Central verifies a namespace against the reversed domain, andcallstackincubator.comis not a registered domain at all (whoisreturnsNo match).callstack.comis, and is the verified namespace on the account.Only the Maven coordinates move. The Kotlin package and the AGP namespace stay
com.callstackincubator.appduct*. Those are three unrelated concepts, and the Kotlin package is load-bearing:appduct doctor's Android detection keys on thecom.callstackincubator.appductdex package, so renaming it would silently change what the release gate inspects. Historical task docs keep the old coordinates — they are dated records.No third-party plugin
Sonatype ships no official Gradle plugin for the Central Portal, and the community alternatives mostly automate what the new
publishing.gradledoes in ~40 lines. Keeping it first-party keeps the Android build classpath to AGP and Kotlin, matching the pinning rationale already in the rootbuild.gradle.The trick is publishing to a file repository rather than a remote one. The Portal takes a single zipped bundle, not Maven-protocol uploads, and a file repository produces exactly the layout that bundle needs — full group path plus the
.md5/.sha1checksums Central requires on every file, written by Gradle rather than by hand. Both modules stage into one directory so a single zip covers the release;maven-metadata.xmlis excluded because Central generates its own.Two safety properties worth reviewing
Signing is conditional. The
signingplugin is wired only whenAPPDUCT_SIGNING_KEYis present, sotest.yaml'spublishToMavenLocalkeeps working on an unkeyed machine. That leniency could otherwise let the job upload an unsigned bundle and fail deep inside Central's validator — sopublish-mavenasserts every staged artifact has a.ascbeside it before zipping.The final Publish is manual. Upload uses
publishingType=USER_MANAGED: Central validates, then waits for a human to press Publish in the Portal UI. A Central version can never be replaced or deleted, so the last irreversible step stays deliberate until this path has proven itself. Switching toAUTOMATIClater is a one-word change. The job polls the status endpoint and fails onFAILED, so a rejected bundle still breaks the release loudly.Verification
Run end to end locally with a throwaway GPG key, since destroyed:
com/callstack/appduct/...layoutgpg --verifyaccepts a generated signaturewithJavadocJar()); Central rejects a deployment without onemaven-metadata.xmlpublishToMavenLocalstill succeeds with no key present — the constraint that keepstest.yamlgreenactionlintclean;check:linkspassesNot verified: the actual Portal upload, which needs the real token and cannot be rehearsed without consuming version 0.9.0 — Central is immutable.
Setup needed before a release
A
maven-centralenvironment with four secrets:CENTRAL_TOKEN_USERNAME/CENTRAL_TOKEN_PASSWORD— Portal user tokenAPPDUCT_SIGNING_KEY/APPDUCT_SIGNING_PASSWORD— armored private key, used in memory and never written to the runner's diskThe GPG walkthrough is in the PR discussion.