A release is fully automated in CI: pushing a release tag builds the artifacts, GPG-signs them via the Scribe service, and uploads them to the Central Publisher Portal. Nothing needs to be run on a developer’s machine.
Repository secrets (one-time setup). The publish job authenticates to the Central Publisher
Portal with a User token (generated at https://central.sonatype.com/account by an account that has
publish permission in Yubico’s com.yubico namespace). Store the token as GitHub Actions repository
secrets:
SONATYPE_USERNAME=<token username> SONATYPE_PASSWORD=<token password>
Release version. Set the desired library version (without the -SNAPSHOT suffix) in the top-level
> build.gradle.kts
example:
val libraryVersion = "3.0.2"
libraryVersion is the single source of truth: every subproject takes its version from it, and the
printVersion task reports it to CI.
|
Note
|
The version must not end with -SNAPSHOT for a release build. The build job checks this
and fails before building. |
Create a release/X.Y.Z branch off the latest main. The release is cut from this branch, not
from main.
Update NEWS with the changes for the version being released.
Set libraryVersion in the top-level build.gradle.kts to the release version, dropping the
-SNAPSHOT suffix, and commit it together with the NEWS update.
Tag the release commit X.Y.Z-rc.1 on the release branch and push the tag. This triggers
release.yml, described in the next section.
Iterate if needed. If a candidate is bad, fix it on the release branch and push -rc.2,
-rc.3, … Leave libraryVersion alone; only the candidate number changes.
Publish the good deployment at https://central.sonatype.com/publishing/deployments and drop the superseded ones.
Tag the released commit X.Y.Z. This is the git release marker only — plain X.Y.Z tags do
not trigger the workflow, so it cannot start a second release.
Merge the release branch back into main.
Bump libraryVersion on main to the next -SNAPSHOT (e.g. 3.0.3-SNAPSHOT) and commit, so
main is never left on a release version.
Push a release-candidate tag — the release version plus an -rc.<n> suffix, e.g. 3.0.2-rc.1 for
version 3.0.2 — to trigger the release.yml workflow. Only -rc.<n> tags trigger it.
|
Important
|
The published version is taken from build.gradle.kts, not from the tag. Tagging
3.0.2-rc.1 publishes version 3.0.2; the -rc.<n> suffix only identifies the attempt. |
The build job enforces this: it compares the tag’s base version against printVersion and fails
before building if they differ. Tagging 3.0.2-rc.1 while build.gradle.kts still says
3.0.1 — or still carries a -SNAPSHOT suffix — stops the release immediately instead of
silently republishing the wrong version.
The pipeline runs three jobs in sequence:
build — builds all modules and publishes unsigned artifacts to the local Maven repository.
sign — sends the artifacts to the Scribe service for GPG signing and republishes the signed
Maven repository (with .asc signature files) as the maven-repo-signed artifact.
publish — downloads maven-repo-signed and runs publishToCentralPortal, which uploads the
signed tree to the Central Publisher Portal.
The upload is created as a USER_MANAGED deployment: the portal validates it but does not release
it automatically. Finish the release by verifying and pressing Publish at
https://central.sonatype.com/publishing/deployments.
If a candidate is not good, fix it and push the next tag (3.0.2-rc.2, 3.0.2-rc.3, …) without
touching the version in build.gradle.kts. Each candidate uploads as its own deployment, labelled
with the tag it was built from. Drop the superseded deployments in the portal and publish the one
you are happy with. Once a version is published to Central it cannot be re-uploaded, so re-tagging
after a successful publish requires a new version.
The sign job enumerates the artifacts (*.jar, *.aar, *.pom, *.module) under the downloaded
maven-repo/ tree, generates the Scribe config from that file list, and submits it. Scribe returns a
detached binary signature (.sig) for each file, preserving its repository-relative path. The job
then converts every .sig to an armored .asc and writes it next to its artifact, recovering the
target from the signature’s full relative path (not just its filename) so artifacts that share a
name across modules cannot be mismatched. The publish job uploads the whole tree, .asc files
included.
The artifacts are uploaded as a single deployment "bundle" (a zip of the signed Maven repository
tree). The whole tree — including the .asc signatures produced by Scribe and freshly generated
md5/sha1/sha256/sha512 checksums — is uploaded verbatim, so the signatures are preserved.
This replaces the
maven-publish plugin’s upload, which ignored the externally produced signatures.
If you need to publish outside CI (e.g. re-uploading a signed build), put your Central Portal User
token in ~/.gradle/gradle.properties:
sonatype.username=<token username> sonatype.password=<token password>
Then download the maven-repo-signed artifact from the GitHub Actions run and extract it into your
local Maven repository:
rm -rf ~/.m2/repository/com/yubico/yubikit/ unzip maven-repo-signed.zip -d ~/.m2/repository/com/yubico/yubikit/
Upload the signed artifacts to the Central Publisher Portal:
./gradlew publishToCentralPortal
The task reads from ~/.m2/repository by default. Override the source repository, deployment name,
or publishing type with Gradle properties if needed:
./gradlew publishToCentralPortal \
-PcentralPortal.repoDir=/path/to/maven/repo \
-PcentralPortal.deploymentName="yubikit-android 3.0.2" \
-PcentralPortal.publishingType=USER_MANAGED
publishingType defaults to USER_MANAGED (validated but not released until you press Publish in
the portal). Use AUTOMATIC to publish immediately after validation passes.
If the Scribe service is unavailable and you must sign a release yourself, set the
localSigning.gpgKey property to your GPG key id when publishing to the local Maven repository. The
signing plugin then signs the artifacts with your local gpg, producing the .asc files that
publishToCentralPortal uploads:
./gradlew publishToMavenLocal -PlocalSigning.gpgKey=0x1234ABCD ./gradlew publishToCentralPortal
An empty value (-PlocalSigning.gpgKey) signs with gpg’s default key. Without the property,
signing is left to Scribe and nothing changes.
|
Note
|
The Central Portal only accepts a signature whose matching public key is discoverable on a public keyserver (e.g. keys.openpgp.org). Publish your public key there before using this fallback for a real release. |
As a dry run, publish to your local Maven repository ($HOME/.m2/):
./gradlew publishToMavenLocal