Skip to content

build(sdk): make datahub-sdk publishable to Maven Central, with locally signed, CI-verified releases - #148

Merged
olavgg merged 5 commits into
mainfrom
build/java-sdk-central-publishing
Sep 30, 2026
Merged

olavgg merged 5 commits into
mainfrom
build/java-sdk-central-publishing

Conversation

@JosteinGj

@JosteinGj JosteinGj commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Why

We want to publish the Java SDK (ai.intellistream:datahub-sdk) and the wire-contract types it is built on (ai.intellistream:datahub-api-model) to Maven Central, the same way the Rust and Python SDKs go to crates.io and PyPI. Nothing has ever been published under ai.intellistream, so 0.3.0 will be the first release.

main could not produce a correct release. The Central tooling from the earlier 0.2.0 prep only ever existed on the old-lineage branch release/java-sdk-0.2.0, which shares no history with main, so it never landed here. What main would have published was wrong in ways Central makes permanent:

  • Wrong coordinate. The SDK published as datahub-java-sdk, while every doc tells users to depend on datahub-sdk.
  • The Spring Boot BOM leaked into both POMs. Both modules apply the dependency-management plugin only for build-time versions, but it copied the whole spring-boot-dependencies import into the published <dependencyManagement>. These artifacts are meant to be framework-free, and that import would have forced Spring's version choices on every consumer.
  • No LICENSE in the jars. The modules are Apache-2.0 while the repo is AGPL, so the jar has to say so itself.
  • An early-access dependency. zero-allocation-hashing:0.27ea0 was an api dependency of api-model.
  • No way to build a Central bundle.

A Central release can never be replaced or deleted, so these had to be fixed before the first upload rather than after.

What changes

Packaging (62fe1a5e)

  • Ports the maven-central-conventions plugin into buildSrc. It holds the POM metadata Central requires, sources and javadoc jars, META-INF/LICENSE, signing, and a local staging repository. Both module build files collapse onto it. The old branch's hardcoded Gitea registry is dropped; the optional -PmavenPublishUrl remote from main is kept.
  • Adds the root centralBundle task. It stages both modules into one Maven layout, zips the deployment Central's Portal takes, and refuses to build if any file is unsigned.
  • Sets the SDK's artifactId to datahub-sdk, and stops the Spring Boot BOM from being written into the POMs.
  • Moves zero-allocation-hashing to the stable 2026.0, pinned once as zeroAllocHashingVersion for every module. The ids it produces are persisted, so every module has to hash identically. IdGeneratorHashStabilityTest and ExternalIdHashingConventionTest pin golden values and pass unchanged.
  • Fills in the Apache-2.0 copyright line (Intellistream AS) in both modules' LICENSE.
  • Doc fixes found along the way: three SDK README snippets that did not compile or threw at runtime (getById returns DataWrapper<NodeModel>, create takes a relations list, and Map.of rejects null values), stale 0.1.0 version references, and a datahub-api-model README that still described Jackson 2 and a "future SDK".

Release workflow (3557013b, reworked in 4f5bdf15, tags in the latest commit)
Each release is signed on the release manager's own machine with their personal key, and CI never holds a signing key. The release manager:

  1. on the minor version's release branch release/vX.Y, sets both versions in gradle.properties to X.Y.Z;
  2. runs ./gradlew centralBundle -PsigningUseGpgCommand=true on that commit, which signs through their local gpg;
  3. runs gh release create vX.Y.Z --target release/vX.Y build/central/datahub-central-X.Y.Z.zip.

Publishing that GitHub Release triggers .github/workflows/java-sdk-release.yml, which is modelled on the Rust/Python SDK's release.yml:

  1. Versions and branch: the tag must be vX.Y.Z, match both apiModelVersion and javaSdkVersion, and point at a commit on release/vX.Y, the release-branch convention from ci: run checks on release/vX.Y branches #145.
  2. Build and test both modules. build.yml does not run on releases, so this is the only test run a release gets.
  3. Verify the attached bundle with scripts/verify-central-bundle.sh:
    • it holds exactly the expected files for both modules at X.Y.Z, with valid checksums;
    • every jar, POM and module file has a good signature from a key whose primary fingerprint is in datahub-java-sdk/RELEASE_SIGNERS, fetched from the public keyservers as Central does. Revoked and expired keys are refused, even though gpg itself exits 0 for them;
    • every file is byte-identical to what the tagged commit builds. A valid signature alone only proves a release manager signed something. The build is reproducible: artifacts built locally with Red Hat JDK 25 and in CI with Temurin 25 were compared byte for byte.
  4. Publish, in the release environment after approval: uploads the bundle verify checked, taken from the run's artifact rather than the release asset so a swapped asset cannot reach Central. It uses publishingType=AUTOMATIC and waits for PUBLISHED, failing with Central's errors if validation fails.

A pull request that touches the version or the release machinery rehearses steps 1–3, signing with a throwaway key made in the job, as the Rust repo does.

Tags are plain vX.Y.Z, like the Rust and Python SDKs. Every published GitHub Release in this repo runs the workflow, so vX.Y.Z tags and releases belong to the Java SDK. If the platform ever cuts GitHub Releases of its own, it needs a different tag scheme, or this trigger needs narrowing.

RELEASE_SIGNERS controls who may sign, so adding a release manager is a reviewed change to it. It lists primary fingerprints, so a signer can rotate subkeys without a change. It is also the list consumers check a downloaded artifact against.

Signing fixes (b66abe25, 4f5bdf15)

  • Subkey selection. With a primary key that only certifies, Gradle's in-memory signer signed with the primary key and produced signatures nothing could verify. -PsigningKeyId / SIGNING_KEY_ID (the subkey's last 8 hex digits) selects the subkey. It was tested with Ed25519 and RSA keys. The rehearsal uses this in-memory path; releases use local gpg.
  • gpg executable. Local gpg signing now calls gpg, not Gradle's default gpg2, which only some installs provide.

Before the first release

Not code, and not done by this PR:

  • The GitHub release environment does not exist yet. It needs to be restricted to v* tags with a required reviewer, and hold the secrets CENTRAL_TOKEN_USER and CENTRAL_TOKEN_PASSWORD. There are no signing secrets. Central has no OIDC trusted publishing, so a token is unavoidable.
  • The ai.intellistream namespace has to be verified on central.sonatype.com, with a Portal token.
  • The first signer's key (906FF6D8…26F4BAB8) is on keys.openpgp.org and keyserver.ubuntu.com with its signing subkey, and is listed in RELEASE_SIGNERS.

Docs

  • datahub-sdk-docs: the install snippets should point at ai.intellistream:datahub-sdk:0.3.0, but only once it is actually on Central. The stale branch docs/java-sdk-0.2.0-maven-central needs retargeting to 0.3.0.
  • datahub-docs (operators): no change.

Verification

  • ./gradlew build is green across all modules.
  • A signed centralBundle built with a throwaway key contains, for both modules, signed jar, sources and javadoc jars, POM and module file; every signature verifies.
  • The staged POMs have no <dependencyManagement> Spring import, name datahub-sdk, and pin zero-allocation-hashing at 2026.0. Both jars carry META-INF/LICENSE.
  • The version check was run locally against good and bad tags. The release-branch rule was tested in a scratch repo: a tag on release/v0.3 passes; a tag on a main-only commit, a commit missing from release/v0.4, and a missing release/v0.5 are all refused.
  • The release manager's path was tested end to end with a throwaway key: local gpg signing via -PsigningUseGpgCommand=true, then verify-central-bundle.sh passes.
  • The verifier refuses all eight bad bundles tried: signer not listed, wrong version, missing .asc, stray maven-metadata.xml, a changed jar that was properly re-signed, a bad checksum, a revoked subkey, and a wrong file name.
  • The workflow's rehearsal verify steps were run locally as written.
  • The earlier version of the workflow ran green on this PR; the reworked one runs on this push.

Not yet exercised: signing with the release manager's real key, and the upload itself.

🤖 Generated with Claude Code

JosteinGj and others added 3 commits September 30, 2026 10:21
Port the Central tooling from the 0.2.0 prep: the maven-central-conventions
plugin and the root centralBundle task. The SDK now publishes as datahub-sdk
(not datahub-java-sdk), the POMs no longer import the Spring Boot BOM, and
the jars carry their LICENSE. zero-allocation-hashing moves off the 0.27ea0
early-access build to 2026.0, pinned once for every module. The id hashes
are unchanged.

Also fix README snippets that did not compile and stale version references.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
Mirrors the Rust/Python SDK release: pushing java-sdk-vX.Y.Z checks the tag
against both versions, builds and tests, then signs, uploads and publishes
from the release environment. Pull requests that touch the release machinery
rehearse it with a throwaway signing key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
Gradle's in-memory signer used the primary key, so a key whose primary only
certifies produced signatures nothing could verify. -PsigningKeyId /
SIGNING_KEY_ID selects the signing subkey.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
…loads

The release manager builds and signs the bundle on their own machine and
attaches it to a java-sdk-vX.Y.Z GitHub Release. CI no longer holds a signing
key. scripts/verify-central-bundle.sh checks the bundle holds exactly the
expected files, that every file is signed by a key in RELEASE_SIGNERS, and
that each is byte-identical to what the tagged commit builds. The workflow
then uploads the verified bundle. Local gpg signing defaults to `gpg`, since
not every install provides `gpg2`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
@JosteinGj JosteinGj changed the title build(sdk): make datahub-sdk publishable to Maven Central, with a tag-driven release workflow build(sdk): make datahub-sdk publishable to Maven Central, with locally signed, CI-verified releases Sep 30, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
@olavgg
olavgg merged commit 903e1e0 into main Sep 30, 2026
13 checks passed
@olavgg
olavgg deleted the build/java-sdk-central-publishing branch September 30, 2026 18:28
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