This document explains how to publish the pk-auth libraries to Maven Central and the browser SDK to npm.
Every release publishes the same set of 13 artifacts, all under
com.codeheadsystems and sharing one version:
pk-auth-core,pk-auth-jwt,pk-auth-admin-apipk-auth-backup-codes,pk-auth-magic-link,pk-auth-otp,pk-auth-refresh-tokenspk-auth-persistence-jdbi,pk-auth-persistence-dynamodbpk-auth-testkitpk-auth-spring-boot-starter,pk-auth-dropwizard,pk-auth-micronaut
The demo applications under examples/ are intentionally not published.
Separately, the browser SDK clients/passkeys-browser/ is published to npm
as @pk-auth/passkeys-browser. Its version tracks the server release it speaks
to (the SDK and the JVM artifacts share a version), so it is published as part of
the same release — see Publishing the browser SDK to npm
below. There is no CI workflow for the npm publish yet; it is a manual step.
These repository secrets must exist on the GitHub repo (they are shared with
hofmann-elimination under the same codeheadsystems org, so on a fresh fork
you only need to set them once at the org level):
| Secret | Purpose |
|---|---|
CENTRAL_PORTAL_USERNAME |
Sonatype Central Portal user-token username |
CENTRAL_PORTAL_PASSWORD |
Sonatype Central Portal user-token password |
GPG_PRIVATE_KEY |
Base64-encoded ASCII-armored private key used to sign artifacts |
GPG_PASSPHRASE |
Passphrase for the GPG key |
GPG_KEY_ID |
Long key id (or fingerprint) of the signing key |
To generate the base64 form of the private key locally:
gpg --armor --export-secret-keys YOUR_KEY_ID | base64 -w 0The signing key must be published to a public keyserver (e.g.
keys.openpgp.org) so Central Portal can verify signatures.
Two GitHub Actions workflows are configured:
Push a semver tag matching vMAJOR.MINOR.PATCH (or
vMAJOR.MINOR.PATCH-suffix for pre-releases) and the
Tag Release to Maven Central workflow takes over.
git switch main
git pull --ff-only
git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0settings.gradle.kts reads the exact tag at build time and uses it as the
project version, so you do not need to edit gradle.properties first.
A pre-release suffix (e.g. v0.2.0-rc.1) automatically marks the GitHub
release as a pre-release.
For a quick patch release with no special version planning, go to Actions → Manual Build Release to Maven Central → Run workflow. The workflow:
- Reads the latest
vX.Y.Ztag and increments the patch component. - Creates and pushes the new tag.
- Runs the same publish + GitHub-release flow as the tagged path.
Use the tagged path (mode A) when the release should be a minor or major bump, when you want a pre-release suffix, or when you want the version pinned in the commit history before the workflow runs.
Both workflows perform the same steps:
- Checkout with full history (
fetch-depth: 0) sogit describecan see the tag. - Validate the tag matches semver.
- Set up JDK 21 + Gradle.
- Confirm Gradle's resolved version equals the tag's version.
./gradlew clean build test --stacktrace— full build and test gate; a failure here aborts the release.- Import the GPG key and configure non-interactive signing.
./gradlew publishAggregationToCentralPortal— builds, signs, and uploads every module in a single bundle to the Central Portal.- Build per-module
-sources.jarand-javadoc.jararchives. - Create a GitHub release with all 13 module jars attached and a copy-paste
implementation(...)snippet in the body.
After the workflow succeeds:
- GitHub release appears at https://github.com/codeheadsystems/pk-auth/releases with all jars attached.
- Maven Central can take up to ~2 hours to index. Check status at
https://central.sonatype.com/publishing/deployments (Central Portal
dashboard). The deployment moves through
VALIDATING → VALIDATED → PUBLISHING → PUBLISHED. - README badges (driven by
img.shields.io/maven-central/v/...) will refresh on their own once Central serves the new version — usually within an hour of publish completing.
To sanity-check a release locally before tagging:
./gradlew clean build test # the gate the workflow uses
./gradlew publishMavenJavaPublicationToMavenLocal # write jars to ~/.m2/repository
./gradlew tasks --group publishing # confirm the aggregation task is presentLocal publish to Central Portal requires ~/.gradle/gradle.properties to
contain centralPortalUsername / centralPortalPassword and a working
signing.gnupg.keyName. Most contributors do not need this — push a tag and
let CI handle it.
- Tag/version mismatch. The
Verify version matches tagstep fails whengradle.propertiesversion doesn't match the tag. Becausesettings.gradle.ktsoverrides version from the tag, this normally only fires when the tag itself is malformed. - GPG import fails. Verify
GPG_PRIVATE_KEYis base64 of the private key (gpg --export-secret-keys, not--export), encoded withbase64 -w 0(single line, no wrap). - Central Portal rejects the bundle. Check the dashboard's deployment
detail page — the most common cause is the signing key not being on a public
keyserver. Re-upload to
keys.openpgp.organd retry. - Artifact not visible after 2 hours. Re-check the Central Portal
deployment status; if it shows
PUBLISHED, the indexer is just slow — artifacts are already downloadable via direct URL. publishAggregationToCentralPortaltask missing. Means thecom.gradleup.nmcp.settingsplugin insettings.gradle.ktsdid not load. Confirm the plugin id and version, then re-run with--refresh-dependencies.
The browser SDK (clients/passkeys-browser/, package
@pk-auth/passkeys-browser) is published to npm manually after the Maven Central
release for the same version has gone out. Its version must match the pk-auth
release version (the SDK speaks the same wire contract as that server release).
- Node ≥ 20 + npm (same toolchain the Gradle SDK build uses).
- An npm account that is a member of the
@pk-authorg/scope with publish rights. The package is public scoped (publishConfig.access = "public"is set inpackage.json, so no--access publicflag is needed). - Authenticate locally once with
npm login(or setNPM_TOKEN/~/.npmrc). Confirm withnpm whoami.
Run these from clients/passkeys-browser/. Replace X.Y.Z with the release
version you just tagged for Maven Central (without the leading v).
cd clients/passkeys-browser
# 1. Match the SDK version to the server release (no git tag — the repo is
# already tagged for the Maven release). This rewrites package.json's version.
npm version X.Y.Z --no-git-tag-version --allow-same-version
# 2. Clean install of the locked dependency tree.
npm ci
# 3. Dry run — verify the file list and resulting tarball before publishing.
# `prepublishOnly` (typecheck + test + build) runs automatically on publish;
# --dry-run exercises it without uploading.
npm publish --dry-run
# 4. Publish for real. `prepublishOnly` runs typecheck, tests, and the tsup
# build, so dist/ is regenerated from source as part of the publish.
npm publish--no-git-tag-version is important: the version bump must not create its own
git tag, because the release is already tagged (vX.Y.Z) for Maven Central. Pin
the SDK version to that same number so the two stay in lockstep.
The committed
versioninpackage.jsonis the in-development line (e.g.2.1.0-SNAPSHOT), mirroringgradle.properties.SNAPSHOTis a valid semver prerelease identifier, so it will not be published by accident — step 1 always pins it to the concrete release version first. After publishing, you may leavepackage.jsonat the published version or restore the-SNAPSHOTline on the development branch; either way, the next release re-pins it in step 1.
npm view @pk-auth/passkeys-browser version # should report X.Y.Z
npm view @pk-auth/passkeys-browser dist-tags # latest -> X.Y.ZFor a pre-release (e.g. X.Y.Z-rc.1), publish under a dist-tag so it does
not become latest:
npm version X.Y.Z-rc.1 --no-git-tag-version --allow-same-version
npm publish --tag nextLike Maven Central, npm publishes are effectively immutable (unpublish is restricted and discouraged). To recover from a bad SDK release, publish a new patch version with the fix rather than unpublishing.
Maven Central releases are immutable — a published version cannot be overwritten or deleted. To recover from a bad release, cut a new patch (or minor) release with the fix and update consumers. Do not attempt to re-publish the same version.