Skip to content

feat(publishing): migrate to Maven Central under uk.co.appoly.droid - #111

Merged
projectdelta6 merged 9 commits into
mainfrom
feature/maven-central-publishing
Aug 26, 2026
Merged

projectdelta6 merged 9 commits into
mainfrom
feature/maven-central-publishing

Conversation

@projectdelta6

Copy link
Copy Markdown
Collaborator

Draft: do not merge. The build side is complete and locally verified, but no artifact has ever been uploaded to Maven Central — see Blocking below. Nothing here is proven until a real publish succeeds.

Why

JitPack rewrites the Gradle Module Metadata it serves, and that single behaviour caused every publishing problem this library has had. Both available shapes were tried against real JitPack builds and both failed:

Version Shape Result
1.8.1 Sources variant published File entry stripped; Gradle requests a name that 404s and gives up silently
1.8.2 No module metadata Sources work, but POMs pin platform artifacts — consumers get duplicate classes and cannot build
1.8.3 Metadata restored, sources by POM classifier Builds fixed. Android Studio binds AAR sources from the variant, so 25 of 26 modules decompile
1.8.4-alpha01 Sources variant restored Stripped again, and worse: ~25 non-existent files requested per sync, 30s timeout each

The two reachable states on JitPack are "no AAR source navigation" and "no AAR source navigation, plus a pile of doomed requests every sync". Maven Central serves exactly what is uploaded, so the workarounds are deleted rather than ported.

Breaking: coordinates change

Before After
com.github.appoly.AppolyDroid-Toolbox:BaseRepo uk.co.appoly.droid:baserepo
com.github.appoly.AppolyDroid-Toolbox:S3Uploader-Multipart uk.co.appoly.droid:s3uploader-multipart
com.github.appoly.AppolyDroid-Toolbox:AppolyDroid-Toolbox-bom uk.co.appoly.droid:bom
maven { url = "https://jitpack.io" } Removable unless something else needs it

Group now matches the package namespace, which has always been uk.co.appoly.droid. Artifact IDs lowercased per Maven convention. Hence 1.9.0, not a patch. All four known consumers are in-house, so a clean cut is simpler than dual-publishing; 1.8.3 stays resolvable on JitPack indefinitely.

What changed

  • com.vanniktech.maven.publish 0.37.0 replaces 26 hand-written publishing {} blocks. Shared POM metadata and coordinates live in the root build; a module declares only its name and description. Central rejects an incomplete POM — a missing developers block is a hard rejection — so the required fields sit centrally where a new module cannot omit them.
  • Deleted: the hand-rolled sources jar, the module-metadata juggling, and the BOM's -Pversion handling. All three existed only to survive JitPack. The plugin produces sources and javadoc itself, so the AAR sources problem ceases to exist rather than being worked around.
  • scripts/publish.sh — full release ceremony modelled on FlexiLogger's, with one deliberate difference: credential loading is in the script, reading the shared vault, rather than in a personal shell function. FlexiLogger's split makes releasing single-user; this way anyone with vault access can release.
  • .github/workflows/release.yml — tag-triggered, running the same gates as CI. Consequence worth stating: if CI is red, no artifact exists, where JitPack would have built one regardless.
  • publishing-check and all 26 READMEs move to the new coordinates. The README's JitPack branch-snapshot section is replaced with local installs and Central snapshots.

Verified locally

Full publish with genuine signing from the 1Password vault:

  • 26 modules published under uk.co.appoly.droid
  • every artifact signed — .asc alongside each pom, module, aar, sources and javadoc
  • every code module carries a sources variant with a correctly named -sources.jar file entry, which is precisely what JitPack strips
  • signature verifies as RSA key 3F4AD175B7176969, the last 16 of the Appoly fingerprint
  • the BOM correctly carries no sources or javadoc — a java-platform has no code, and Sonatype exempts POM-packaged artifacts
  • 650 tests green; publishing-check passes on the new coordinates
  • ./scripts/publish.sh --dry-run passes every gate and stops cleanly before publish and tagging

Not verified, and why this is a draft

No upload to Maven Central has been attempted. Everything above proves the artifacts are correct; none of it proves Central accepts them. Blocking:

  1. The uk.co.appoly DNS TXT record is not live. Verified independently from two sessions — the token is absent from the delegated Route 53 nameservers. Not propagation delay; the theory is a duplicate hosted zone, and a zone whose own NS set doesn't match the four delegated names is the wrong one.
  2. The portal user token does not exist yet. The script expects it at portal-username / portal-token on the existing 1Password item; the FlexiLogger session has agreed those names.

The signing key itself is done, org-owned, and published to keyserver.ubuntu.com.

Review notes

  • The 26 module descriptions are permanent. They go to Maven Central and cannot be corrected in place — releases are immutable. Drafted from CLAUDE.md's module list; worth a read.
  • Immutability changes the workflow. The alpha-retag loop used throughout 1.8.3 stops working. The publish script refuses to run if the version's tag already exists, and says so before the build rather than after a failed upload.
  • A known flake: javaDocReleaseGeneration (Dokka) has aborted a run twice and succeeded unchanged on retry. Not reproducible on demand, so reported as observed rather than diagnosed. It fails before any upload, so it costs a wasted run rather than a partial release.
  • 81d5aac bumps FlexiLogger to 2.1.4 (not mine). Confirmed resolvable and green — and it resolves to flexilogger-android, which is variant-aware resolution working as intended.

🤖 Generated with Claude Code

projectdelta6 and others added 8 commits August 24, 2026 14:59
JitPack rewrites the Gradle Module Metadata it serves, and that single
behaviour caused every publishing problem this library has had. Two shapes
were tried against real JitPack builds and both failed: a sources variant has
its file entry stripped so Android Studio binds a 404, and no variant at all
leaves it nothing to bind. 1.8.2 avoided both by publishing POM-only and broke
consumer builds instead. Central serves exactly what is uploaded, so the
workarounds are deleted rather than ported.

Coordinates change: com.github.appoly.AppolyDroid-Toolbox:BaseRepo becomes
uk.co.appoly.droid:baserepo, and AppolyDroid-Toolbox-bom becomes bom. Group
now matches the package namespace, which has always been uk.co.appoly.droid.
Breaking for consumers, hence 1.9.0 rather than a patch.

- com.vanniktech.maven.publish 0.37.0 replaces 26 hand-written publishing
  blocks. Shared POM metadata and coordinates live in the root build; a module
  declares only its name and description. Central rejects an incomplete POM,
  and a missing developers block is a hard rejection, so the required fields
  are set centrally where a new module cannot omit them.
- Deleted: the hand-rolled sources jar, the module-metadata juggling, and the
  BOM's -Pversion handling. All three existed only to survive JitPack.
- The plugin produces sources and javadoc jars itself, so withSourcesJar and
  the AAR sources problem disappear rather than being worked around.
- publishing-check, the release workflow and all 26 READMEs move to the new
  coordinates. The README's JitPack branch-snapshot section is replaced with
  local installs and Central snapshots, including a warning that a local
  install carries the same version string as the real release.
- scripts/publish.sh reads credentials from the shared 1Password vault, so
  anyone with vault access can release. Deliberately not a personal shell
  function, which is single-user by construction.

Verified locally with genuine signing from the vault: 26 modules published,
every artifact carries a .asc, every code module carries sources and javadoc
variants with correctly named file entries, and the BOM correctly carries
neither since a java-platform has no code. Signature checks out against key
3F4AD175B7176969. 650 tests green; publishing-check passes on the new
coordinates.

Not yet possible: the actual upload. The uk.co.appoly namespace is unverified
because the DNS TXT record is not live, and the portal token does not exist
yet. Nothing here is merged until a real Central publish has been proven.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the minimal credential shim with the same release ceremony
FlexiLogger uses: gates before the prompt, an explicit confirmation, and
tagging only after a successful upload.

One deliberate difference. FlexiLogger splits this in two — a `flexipublish`
function in a personal ~/.zshrc supplies credentials, and ./publish.sh
consumes them. That split makes releasing single-user: the credentials live in
one person's shell profile and one person's private vault, so nobody else can
publish. Here the credential loading is in the script itself, reading the
SHARED Appoly vault, so anyone with vault access can release and there is
nothing to install but the 1Password CLI.

Three modes:
  (none)      gates, confirm, publish to Central, tag, push the tag
  --dry-run   every gate, no publish and no tag
  --local     signed install to ~/.m2 for testing a branch in a consuming app

Adapted rather than copied:
- Version comes from BuildConfig.kt, not the version catalog.
- Release branch is main, and tags are bare (1.9.0), matching this repo's
  existing tags rather than FlexiLogger's v-prefix.
- Gates include the R8 consumer keep rules and the published-metadata check,
  which FlexiLogger has no equivalent of.
- Refuses to run if a tag for the version already exists. Central releases are
  immutable, so a repeat publish cannot succeed and the clearest place to say
  so is before the build rather than after the upload fails.
- The confirmation prints all 26 coordinates and states the immutability
  explicitly. This repo has spent the week retagging alphas freely; that stops
  working here, and the prompt is where that needs to land.
- --local warns that a local install carries the same version string as the
  real release, so leaving mavenLocal() in place means resolving your own
  artifacts while believing you are testing the published one.

Fixed while testing: --dry-run originally skipped credential loading, but its
gate sequence runs publishToMavenLocal for the metadata check, which fails
without a signing key. Signing is now read in every mode; only the upload
token is release-only.

Verified: --help, argument rejection, --local (26 modules signed into ~/.m2)
and a full --dry-run through every gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dle 9.7.1

Pull-request CI failed at `:bom:signMavenPublication` with "No configured
signatory". The job runs publishToMavenLocal to feed the variant-resolution
gate, but holds no PGP key — deliberately, since a pull-request build must not
carry the release signing key — while signAllPublications() was unconditional.
The gate therefore died before the check it exists to run.

Signing is now enabled only when a key is actually present. The gate reads
module metadata and POMs and never looks at a signature, so an unsigned local
publish serves it perfectly well. A task-graph guard refuses any Maven Central
upload without a key, so the relaxation cannot leak into a real release: it
fails loudly at configuration time rather than uploading artifacts Central
would reject, in a release that cannot be undone.

scripts/publish.sh keeps loading signing credentials in every mode. Its comment
said signing was required for the metadata gate, which is no longer true; the
real reason is that a dry run must still exercise the step a release cannot
survive failing, and --local exists to test the signed artifacts.

Also syncs the toolchain bump: tasks.wrapper still pinned gradleVersion to
9.7.0, which would have re-downgraded the wrapper on the next `./gradlew
wrapper`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…se CI

The plan lived only as an artifact, where it drifted out of date and could not be
reviewed alongside the code it describes. It moves to
docs/maven-central-migration.md, updated to what has actually landed: the namespace
is verified, the publishing configuration and publish script are in, and the only
work left is cutting 1.9.0 and migrating the four in-house consumers.

Deletes .github/workflows/release.yml. Releases are run manually and locally for
now, so a tag-triggered publish job is not the chosen path — and with no Maven
Central secrets in the repo it could never have published anyway. Left in place it
would have reported a red release run for a release that succeeded on a developer's
machine.

CLAUDE.md said a version tag ran the same publish in CI, which is no longer true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Listing the vault item's field labels confirms what phase 1 left open: the signing
fields are all present, but portal-username and portal-token were never added.

publish.sh reads those two only in release mode, so --dry-run and --local work
today and a real release fails immediately at the credential step, before any gate
runs. That is the last thing standing between this branch and 1.9.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The portal credentials were read only in release mode, so a dry run rehearsed every
gate except the two fields most likely to be wrong — a missing or misnamed vault
field would first surface on the immutable run it exists to protect. Reading them in
dry-run mode costs nothing and uploads nothing. --local still skips them; it needs
no upload credentials at all.

Also records that portal-username and portal-token are now in the vault, so the plan
no longer lists the release as blocked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A dry run failed with a bare "Metaspace" error partway through publishing. Cause:
publishing drives Dokka across all 26 modules in one daemon, which needs well over
the 1 GiB metaspace a typical personal ~/.gradle/gradle.properties sets. User-level
properties take precedence over the repo's, so the project cannot set this itself —
meaning a release succeeded or failed depending on whose machine ran it. GRADLE_OPTS
beats both files, so the script now pins its own.

Also guards `./gradlew publishToMavenLocal` in the metadata gate, the only unchecked
Gradle call in that section. set -e already aborted correctly; this just names what
broke instead of leaving a raw Gradle failure.

With both in, `--dry-run` passes end to end: all five vault fields read, gates green,
26 modules signed into ~/.m2, variant resolution clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@projectdelta6
projectdelta6 marked this pull request as ready for review August 26, 2026 12:59
@projectdelta6 projectdelta6 self-assigned this Aug 26, 2026
Nothing resolves from jitpack.io any more. The one plausible candidate is
com.github.skydoves:sandwich, whose group name suggests otherwise, but it
publishes to Maven Central — so the repository entry was only widening the set of
hosts the build trusts. Verified by resolving :app's releaseRuntimeClasspath with
--refresh-dependencies after removal: 1199 components, nothing unresolved.

jitpack.yml is likewise vestigial. JitPack builds a tag from that tag's own copy of
the file, so removing it here cannot affect anyone still resolving 1.8.3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@projectdelta6
projectdelta6 merged commit e400b22 into main Aug 26, 2026
1 check passed
@projectdelta6
projectdelta6 deleted the feature/maven-central-publishing branch August 26, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants