Every releasable artifact is a directory under packages/, versioned on its
own with its own CHANGELOG.md. Those changelogs are marked merge=union in
.gitattributes, so PRs that both append under ## [Unreleased] merge
without a textual conflict (GitHub's mergeability check does not run merge
drivers, so a PR may still read CONFLICTING until its branch merges main
locally and pushes). The directory name is the git tag prefix, and
the files in it decide where it publishes:
| Package | Kind | Tag | Published by |
|---|---|---|---|
ankusa, ankusa_rabbitmq, ankusa_kafka, ankusa_nats, ankusa_redis, async_api_spex | Hex (mix.exs) | <pkg>-vX.Y.Z | release.yml |
ankusa_server | Docker image jamescarr/ankusa (Dockerfile) | ankusa_server-vX.Y.Z | docker.yml |
sdk-typescript | npm package ankusa (package.json) | sdk-typescript-vX.Y.Z | release-npm.yml |
sdk-python | PyPI package ankusa (pyproject.toml) | sdk-python-vX.Y.Z | release-python.yml |
sdk-rust | crates.io crate ankusa (Cargo.toml) | sdk-rust-vX.Y.Z | release-crates.yml |
sdk-ruby | RubyGems gem ankusa-sdk (ankusa-sdk.gemspec) | sdk-ruby-vX.Y.Z | release-ruby.yml |
sdk-go | Go module github.com/jamescarr/ankusa/packages/sdk-go (go.mod) | sdk-go-vX.Y.Z, then packages/sdk-go/vX.Y.Z after CI | release-go.yml |
sdk-php | Packagist package jamescarr/ankusa (composer.json) | sdk-php-vX.Y.Z | release-php.yml |
sdk-elixir | Hex package ankusa_sdk (mix.exs) | sdk-elixir-vX.Y.Z | release.yml |
sdk-java | Maven Central io.github.jamescarr:ankusa-sdk (build.sbt) | sdk-java-vX.Y.Z | release-maven.yml |
sdk-clojure | Clojars io.github.jamescarr/ankusa-clj (deps.edn, build.clj) | sdk-clojure-vX.Y.Z | release-clojars.yml |
The npm, PyPI, and crates.io packages are all named ankusa, the RubyGems gem
is ankusa-sdk because RubyGems' ankusa belongs to an unrelated project, the
Elixir SDK publishes as ankusa_sdk because Hex's ankusa is this repo's
core, the Java SDK publishes under the io.github.jamescarr group because
that is the namespace GitHub verifies for this account, and the Clojure SDK
is ankusa-clj because the Clojure CLI resolves from Maven Central and
Clojars together, so ankusa-sdk in the same group would collide with the Java
SDK's artifact; each one's tag prefix is its directory name, not the package
name, so none can be confused with the Hex core's ankusa-vX.Y.Z or with each
other.
The same for every kind:
mise run status # version, last tag, published?, commits since
mise run release:prepare minor ankusa_nats # patch|minor|major|X.Y.Z, one or more packages
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag ankusa_nats
mise run release:watch ankusa_nats
mise run release:verify ankusa_nats
release:prepare <bump> <pkgs>… refuses a dirty tree or a HEAD that
isn't origin/main, and refuses a package whose CHANGELOG.md has nothing
under [Unreleased]. For each package it sets the version (@version in
mix.exs, [package] version in Cargo.toml, VERSION in a gem's
lib/**/version.rb, const Version in a Go module's version.go,
npm version for npm, or uv version for Python),
opens a dated ## [X.Y.Z] heading
under [Unreleased], and points the footer compare links at the new tag.
It commits all of them on release/<tag>, pushes, and opens a PR whose
body is the release notes. --no-pr stops after the local commit.release:tag <pkgs>… from the merged main runs release:preflight
first: clean tree, HEAD == origin/main, a ## [X.Y.Z] heading, the tag
free locally and on origin, the version not yet on its registry, and the
package's repo secrets present. Then it pushes one tag per package.ci.yml
via workflow_call, so a core tag can't ship something that breaks an
adapter), publishes, and cuts a GitHub release from the CHANGELOG section
(mise run release:notes <pkg> [version] prints the same text).release:watch <pkg> follows that run; release:verify <pkgs>…
confirms the version is live on its registry.Nobody runs the long drills locally to release
(AGENTS.md).
The pre-tag gate is a green ci.yml on the commit being tagged. e2e.yml (the
kind + Oban end-to-end proof, and the compose fleet drills) runs on every tag
push, and the latest nightly e2e.yml and soak.yml runs on main are read
before tagging, not rerun. Tagging ankusa checks the power-loss crash drill
(scripts/drills/flakey.sh, testing.md)
from release:preflight by reading the crash drill (power-loss cut, device-mapper) job of ci.yml's push run for HEAD: it fails when that run
is missing, red, or skipped the drill (a runner without device-mapper), and then
the drill is a manual step, run on a privileged Docker host before tagging.
Tag ankusa alone first, and wait for release:watch ankusa to finish
green before tagging any adapter. The adapters' :prod deps resolve ankusa
from Hex, not the path dep, so an adapter tag pushed before core is live
fails at MIX_ENV=prod mix deps.get. release:preflight enforces this: an
adapter's preflight fails until the core version in the tree is on Hex.
release:prepare can still bump core and adapters together in one PR.
async_api_spex comes before core: ankusa depends on it, and builds under
:prod resolve it from Hex, so a core tag pushed before async_api_spex is
live fails the same way. Tag async_api_spex alone first, wait for
release:watch async_api_spex, then continue as above;
release:preflight fails a core tag until the async_api_spex version in the
tree is on Hex.
The server image and the npm, Python, Rust, Ruby, and Go SDKs have no ordering
constraint: the image builds from this checkout, and none of the SDKs depends
on anything here. sdk-elixir is a Hex package but has no ankusa dependency
either, so release:preflight skips the core-first check for it (the check
applies only to packages whose mix.exs depends on ankusa).
Every push to main touching packages/ankusa*/**, packages/async_api_spex/**, .dockerignore, or the
docker/ci workflows publishes jamescarr/ankusa:edge and :sha-<short> for
linux/amd64 and linux/arm64. Pull requests touching the same paths build and
smoke-test the image without pushing.
The tag run builds both architectures natively, runs
packages/ankusa_server/scripts/smoke.sh
against the built image (the same script as mise run docker:smoke), and only
then publishes X.Y.Z, X.Y, latest (plus X from 1.0 on). A prerelease
version publishes its exact tag only, so nobody can pull latest and get an rc.
The same job pushes packages/ankusa_server/README.md as the Docker Hub
repository description. release:verify ankusa_server also checks latest
(for a non-prerelease) and that the tag carries both architectures.
The Helm chart (deploy/helm/ankusa)
runs jamescarr/ankusa:<appVersion> unless image.tag says otherwise: set its
appVersion in Chart.yaml to the new ankusa_server version in the release PR, and
bump the chart's own version when its templates or values changed.
Nothing the image or its pipeline pulls is a moving name. mise run lint:pins
(in mise run check and the CI lint job) fails any GitHub action not pinned
to a 40-character commit SHA, the BuildKit and SBOM-scanner images docker.yml
builds with, and any FROM or # syntax= in
packages/ankusa_server/Dockerfile
not pinned by digest. The tag or version each pin came from is the trailing
comment; bump the SHA and the comment together (the header of
.mise/tasks/lint/pins has the commands).
A push to main or a server tag then does, in docker.yml:
build pushes each architecture by digest with provenance: mode=max
(SLSA provenance) and an SPDX SBOM from the digest-pinned BuildKit Syft
scanner, both stored as attestation manifests next to the image.merge creates the tags, then refuses the index unless
docker buildx imagetools inspect … --format '{{json .SBOM}}' and .Provenance
each have linux/amd64 and linux/arm64; signs the index digest with
cosign sign --yes (keyless: the job's OIDC token, id-token: write, is the
identity); and runs smoke.sh against jamescarr/ankusa@sha256:<index>,
whose first step is cosign verify with the identity of this workflow on
main or a server tag. A failure leaves the job red and no GitHub release.mise run docker:smoke builds locally and prints cosign verify: skipped (a
local build is unsigned); SMOKE_COSIGN_IMAGE=jamescarr/ankusa:X.Y.Z mise run docker:smoke, or smoke.sh jamescarr/ankusa@sha256:…, verifies a published one
(needs cosign). How a consumer verifies is in the image's
README.
Each CI package job also uploads mix hex.audit's report as the
hex-audit-<package> artifact.
Repository secrets, under Settings → Secrets and variables → Actions.
release:preflight checks the ones the package needs before any tag exists.
HEX_API_KEY, a Hex key scoped to api:write: generate one from
the Hex.pm dashboard (Keys).DOCKERHUB_USERNAME, and DOCKERHUB_TOKEN: a Docker Hub
personal access token with Read & Write.NPM_TOKEN, an npmjs.com Automation access token (Account →
Access Tokens → Generate New Token → Automation: this type bypasses
2FA-on-publish, which a personal "Publish" token does not), scoped to the
ankusa package once it exists, or unscoped for the first publish.
release-npm.yml also supports Trusted Publishing (OIDC): a trusted
publisher is already configured on npmjs.com (GitHub Actions, repo
jamescarr/ankusa, workflow release-npm.yml, main), but it can't be
switched on yet — npm's registry rejects this repo's immutable OIDC
subject claim with a misleading "package not found"
(npm/cli#9969, open,
untriaged). The repository variable NPM_AUTH (Settings → Secrets
and variables → Actions → Variables, not Secrets) picks the path: unset,
or anything other than oidc, uses NPM_TOKEN; set to oidc to drop
the token and publish via the trusted publisher instead. Flip it once
#9969 is fixed, confirm one release through it, then delete the
NPM_TOKEN secret.CARGO_REGISTRY_TOKEN, a crates.io API token carrying the
publish-new and publish-update scopes, generated at
https://crates.io/settings/tokens (Account Settings → API Tokens). See
Rust SDK (crates.io) below for why it is a token
rather than OIDC.SDK_PHP_DEPLOY_KEY, an SSH deploy key with
write access to the read-only mirror jamescarr/ankusa-php;
PACKAGIST_USERNAME, the Packagist account name; and PACKAGIST_TOKEN, that
account's API token. Setup: see PHP SDK (Packagist)
below.SONATYPE_USERNAME and
SONATYPE_PASSWORD, a Central Portal user token's two halves, plus
PGP_SECRET (base64 of the armored secret signing key) and
PGP_PASSPHRASE. The Central Portal has no OIDC trusted publishing, so all
four are repository secrets. Setup: see
Java SDK (Maven Central) below.CLOJARS_USERNAME, the Clojars account that
owns the group, and CLOJARS_PASSWORD, a deploy token for that account (not
its password). Clojars has no OIDC trusted publishing, so both are
repository secrets. Setup: see Clojure SDK (Clojars)
below.sdk-python publishes as the ankusa project on PyPI. There is no token to
store or rotate: release-python.yml
authenticates with PyPI Trusted Publishing
(OIDC), so the only setup is on PyPI itself, and the only manual step in the
whole flow.
ankusa does not exist on PyPI yet, so register a pending trusted
publisher: the first successful publish creates the project and binds the
name to this repo. From the PyPI account that will own the package:
Sign in at https://pypi.org/account/login/. PyPI requires 2FA before you can manage publishers.
Open https://pypi.org/manage/account/publishing/ (Publishing under your account, not the project settings — there is no project yet).
Under Add a new pending publisher, fill in exactly:
| Field | Value |
|---|---|
| PyPI Project Name | ankusa |
| Owner | jamescarr |
| Repository name | ankusa |
| Workflow name | release-python.yml |
| Environment name | pypi |
Save. Nothing else on PyPI is needed: the pypi GitHub environment is
created on first use, and every later release publishes without further
PyPI changes.
The project name has to be free, and PyPI names are first-come. Check before the first tag:
curl -s -o /dev/null -w '%{http_code}\n' https://pypi.org/pypi/ankusa/json # 404 = available
If it is taken, change [project] name in
packages/sdk-python/pyproject.toml
(and this document) before tagging; release:preflight and
release:verify read that field, so nothing else changes.
Optionally add the pypi environment under Settings → Environments first if
you want to gate publishes behind a required reviewer; an environment with no
protection rules behaves like none.
Same flow as every other package (above); only the package name differs. With no ordering constraint against the Hex packages:
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-python # or patch | major | 0.2.1
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-python
mise run release:watch sdk-python # the tag run builds and publishes
mise run release:verify sdk-python # HTTP 200 for the version on PyPI
release:prepare runs uv version, which rewrites both pyproject.toml and
uv.lock (so the release PR keeps uv sync --locked passing);
release:tag's preflight confirms the version is still unpublished on PyPI
and the tag is free; the tag run builds the sdist and wheel with uv build,
publishes through the pending publisher, and cuts the GitHub release from the
CHANGELOG section.
sdk-rust publishes as the ankusa crate on crates.io, from
release-crates.yml,
authenticated with the CARGO_REGISTRY_TOKEN repository secret.
The crate name has to be free, and crates.io names are first-come. Check before the first tag:
curl -s -o /dev/null -w '%{http_code}\n' \
-A 'ankusa-release (https://github.com/jamescarr/ankusa)' \
https://crates.io/api/v1/crates/ankusa/0.1.0 # 404 = available
The -A is not optional: crates.io answers 403 to curl's own user agent.
pkg_registry_code in .mise/lib/pkg.sh (so mise run status,
release:preflight, and release:verify) sends the same one.
Then create the token at https://crates.io/settings/tokens (Account Settings
→ API Tokens) with two scopes. publish-new allows publishing a crate name
that does not exist yet, so it is needed for the first release only;
publish-update allows a new version of a crate you already own, so it covers
every release after that. Store it as the CARGO_REGISTRY_TOKEN secret.
This is a token rather than crates.io Trusted Publishing because a trusted publisher is configured on the crate's own settings page, so the crate has to exist before the OIDC flow can be used — which is what the first token-driven release creates. Switching to it afterwards is three changes, not one:
release-crates.yml.publish job id-token: write, and add a step that fetches the
short-lived token —
rust-lang/crates-io-auth-action@v1,
whose token output becomes CARGO_REGISTRY_TOKEN for the cargo publish
step.cargo from pkg_secrets in .mise/lib/pkg.sh, or
release:preflight fails with "repo secret CARGO_REGISTRY_TOKEN is not
set".If the name is taken before the first release, change [package] name in
packages/sdk-rust/Cargo.toml
(and this document) before tagging; the directory and the tag prefix stay
sdk-rust.
Same flow as every other package (above); only the package name differs. With no ordering constraint against the Hex packages:
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-rust # or patch | major | 0.1.0
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-rust
mise run release:watch sdk-rust # the tag run builds and publishes
mise run release:verify sdk-rust # HTTP 200 for the version on crates.io
release:prepare sets [package] version in Cargo.toml and refreshes
Cargo.lock with cargo update --workspace (the lock records the crate's own
version, so --locked fails without it); release:tag's preflight confirms
the version is still unpublished on crates.io and the tag is free; the tag run
publishes with cargo publish --locked on the same Rust toolchain
.mise/conf.d/sdk-rust.toml pins for check:package sdk-rust, and cuts the
GitHub release from the CHANGELOG section.
sdk-ruby publishes as the ankusa-sdk gem on RubyGems (RubyGems' ankusa
belongs to an unrelated project). There is no token to store or rotate:
release-ruby.yml
authenticates with RubyGems Trusted Publishing
(OIDC), so the only setup is on RubyGems itself, and the only manual step in the
whole flow.
ankusa-sdk does not exist on RubyGems yet, so register a pending trusted
publisher: the first successful publish creates the gem and binds the name to
this repo. From the RubyGems account that will own the gem:
Sign in at https://rubygems.org/session/new.
Open https://rubygems.org/profile/oidc/pending_trusted_publishers (Trusted publishers under your profile).
Under Pending trusted publishers, fill in exactly:
| Field | Value |
|---|---|
| Gem name | ankusa-sdk |
| Repository owner | jamescarr |
| Repository name | ankusa |
| Workflow filename | release-ruby.yml |
| Environment | rubygems |
Leave the Workflow Repository fields blank.
Save. Nothing else on RubyGems is needed: the rubygems GitHub environment
is created on first use, and every later release publishes without further
RubyGems changes.
The gem name has to be free, and RubyGems names are first-come. Check before the first tag:
curl -s -o /dev/null -w '%{http_code}\n' https://rubygems.org/api/v1/gems/ankusa-sdk.json # 404 = available
If it is taken, change spec.name in
packages/sdk-ruby/ankusa-sdk.gemspec
(and this document) before tagging; release:preflight and release:verify
read that field, so nothing else changes.
Optionally add the rubygems environment under Settings → Environments first if
you want to gate publishes behind a required reviewer; an environment with no
protection rules behaves like none.
Same flow as every other package (above); only the package name differs. With no ordering constraint against the Hex packages:
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-ruby # or patch | major | 0.2.1
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-ruby
mise run release:watch sdk-ruby # the tag run builds and publishes
mise run release:verify sdk-ruby # HTTP 200 for the version on RubyGems
release:prepare rewrites lib/ankusa/sdk/version.rb (through
.mise/lib/release.exs)
and runs bundle lock, so the release PR keeps the frozen bundle install in
CI passing; release:tag's preflight confirms the version is still unpublished
on RubyGems and the tag is free; the tag run builds the gem with gem build,
pushes it through the trusted publisher, and cuts the GitHub release from the
CHANGELOG section.
sdk-go publishes as the Go module
github.com/jamescarr/ankusa/packages/sdk-go (package name ankusa), with no
token to store: the module proxy reads the pushed git tag. A module in a
subdirectory needs a second tag, packages/sdk-go/vX.Y.Z — that is the one
go get resolves, while sdk-go-vX.Y.Z is the repo convention. release-go.yml
pushes the module tag only after the full test suite passes, because a proxy
and checksum-database version is immutable: it must never point at a commit CI
has not run on. The workflow then waits for the proxy to serve the version,
which is also what lists the module on pkg.go.dev.
Same flow as every other package (above); only the package name differs:
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-go # or patch | major | 0.1.0
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-go # pushes sdk-go-vX.Y.Z
mise run release:watch sdk-go # the tag run pushes packages/sdk-go/vX.Y.Z
mise run release:verify sdk-go # HTTP 200 for the version on the proxy
release:prepare rewrites the const Version line in
packages/sdk-go/version.go;
release:tag's preflight also refuses a run whose
packages/sdk-go/vX.Y.Z module tag already exists on origin, and
release:verify asks proxy.golang.org for the version.
Two steps treat Go differently from the other kinds. Preflight does not ask the
proxy about the version, because a request made before the module tag exists
caches a miss there for up to about 30 minutes (the mirror does not re-check on
every request) — exactly the state a release starts in. It checks the module
tag on origin instead, which is the same fact, and release-go.yml waits up
to 40 minutes for the proxy to serve the version. release:verify does ask the
proxy, but only after that wait has passed.
The repository has to be public for the proxy to fetch it. If it is private,
the module tag is still pushed, but consumers need
GOPRIVATE=github.com/jamescarr/ankusa and pkg.go.dev never lists the module.
sdk-php publishes as the jamescarr/ankusa package on
Packagist, which installs it with
composer require jamescarr/ankusa. Packagist reads composer.json at the
root of a repository, so it cannot be pointed at this monorepo —
release-php.yml
pushes the package to the read-only split mirror
jamescarr/ankusa-php (tagged
vX.Y.Z there; the monorepo tag stays sdk-php-vX.Y.Z) and then asks
Packagist to re-crawl it. Development happens in packages/sdk-php only;
anything committed to the mirror is overwritten by the next release.
Needed once, before the first release:tag sdk-php.
Create the public repository jamescarr/ankusa-php ("Read-only mirror of
packages/sdk-php in jamescarr/ankusa"). Disable Issues, Wiki and Projects:
every question about the code belongs in the main repo.
Give the release workflow write access to it:
ssh-keygen -t ed25519 -N '' -C ankusa-php-mirror -f ankusa-php-deploy
Add ankusa-php-deploy.pub as a deploy key with write access on
jamescarr/ankusa-php, then store the private half on this repo and delete
both files:
gh secret set SDK_PHP_DEPLOY_KEY < ankusa-php-deploy
rm ankusa-php-deploy ankusa-php-deploy.pub
Seed the mirror, so Packagist has something to import:
git clone git@github.com:jamescarr/ankusa-php.git /tmp/ankusa-php
cp -R packages/sdk-php/. /tmp/ankusa-php/
(cd /tmp/ankusa-php && git add -A && git commit -m 'Seed from jamescarr/ankusa' && git push origin HEAD:main)
Submit https://github.com/jamescarr/ankusa-php at
https://packagist.org/packages/submit. The package name comes from its
composer.json (jamescarr/ankusa), and Packagist verifies the repository
by reading that file.
From Packagist → Profile → API token, set the two remaining secrets:
gh secret set PACKAGIST_USERNAME # the Packagist account name
gh secret set PACKAGIST_TOKEN # the API token
Use the account's Safe API token; if the update-package call in the
workflow answers 403, use the main API token instead.
Check the name is free before the first tag (Packagist names are first-come):
curl -s -o /dev/null -w '%{http_code}\n' https://repo.packagist.org/p2/jamescarr/ankusa.json # 404 = available
If jamescarr/ankusa is taken, change name in
packages/sdk-php/composer.json
(and this document) before tagging: release:preflight, release:verify and
the publish job all read that field, so nothing else changes.
Same flow as every other package (above); only the package name differs. With no ordering constraint against the Hex packages:
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-php # or patch | major | 0.2.1
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-php
mise run release:watch sdk-php # the tag run pushes the mirror and updates Packagist
mise run release:verify sdk-php # HTTP 200 for the version on Packagist
release:prepare rewrites public const string VERSION in
src/Version.php
(Composer locks don't record the root version, so nothing else needs
rewriting); release:tag's preflight confirms the version is still
unpublished on Packagist and the tag is free; the tag run pushes the mirror,
triggers a Packagist update, polls repo.packagist.org until the version
appears, and cuts the GitHub release from the CHANGELOG section. Because
Packagist installs from the mirror's GitHub zipball, the archive carries
whatever packages/sdk-php/.gitattributes does not mark export-ignore —
tests and dev configs stay out of what users download.
sdk-elixir publishes as the ankusa_sdk package on Hex (Hex's ankusa is
this repo's core), from
packages/sdk-elixir/mix.exs,
using the same HEX_API_KEY secret as the core packages. It has no ordering
constraint against them: the SDK is a pure HTTP client and does not depend on
ankusa at all, so release:preflight does not wait for core to be live (and
its registry lookup reads the app name from mix.exs, since the Hex name and
the directory name differ).
The flow is the same as every other package (above):
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-elixir # or patch | major | 0.4.0
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-elixir
mise run release:watch sdk-elixir # the tag run builds and publishes to Hex
mise run release:verify sdk-elixir # HTTP 200 for the version on hex.pm
release:prepare rewrites @version in mix.exs; release:tag's preflight
confirms the tag is free and ankusa_sdk @version is still unpublished; the
tag run checks the tag against mix.exs, waits for the full CI suite, then
publishes with MIX_ENV=prod mix hex.publish and cuts the GitHub release from
the CHANGELOG section. mix.lock is committed so CI's
mix deps.get --check-locked gate pins CI to known versions, but it is not in
the package's files: Hex consumers resolve req ~> 0.7 and plug ~> 1.18
themselves. Because the SDK has no ankusa dependency, the publish job's
MIX_ENV=prod mix deps.get resolves everything from Hex, with no core-first
wait.
sdk-java publishes as io.github.jamescarr:ankusa-sdk on Maven Central, from
packages/sdk-java/build.sbt,
whose organization, name and version are the coordinates — pkg_version
and pkg_registry_code read them with sed and
release:prepare rewrites version, exactly as they do for the other kinds.
The jar is built on JDK 25 but is Java 17 bytecode and Java 17 API
(javacOptions pins --release 17), carries
Automatic-Module-Name: io.github.jamescarr.ankusa in its manifest — sbt has no
JPMS descriptor support, so there is no module-info.java — and has no
_3 suffix or Scala dependency (crossPaths := false,
autoScalaLibrary := false), which mise run check:package sdk-java asserts
along with the staged bundle's shape.
Publishing goes through the Central
Portal, using sbt 2's built-in staging:
publishSigned stages the jar, sources, javadoc, POM and CycloneDX SBOM, each
with an armored .asc beside them, in
packages/sdk-java/target/sona-staging, and sonaRelease uploads that bundle
and waits for the portal to validate it. mise run check:package sdk-java runs
the unsigned publish and then checks the staged bundle, so the shape the
release signs is the shape CI gated on — including the SBOM, which sbt-sbom
attaches as the cyclonedx classifier artifact without any build.sbt setting.
Needed once, before the first release:tag sdk-java, all by the repository
owner:
Sign in to https://central.sonatype.com/ with GitHub. That verifies
io.github.jamescarr for the account, so no DNS TXT record is needed for
this group id.
Generate a user token (Account → Generate User Token) and store its halves as
the SONATYPE_USERNAME and SONATYPE_PASSWORD repository secrets. sbt 2
reads both names out of the environment and appends them as a credential for
central.sonatype.com; there is nothing to write to ~/.sbt.
Create a PGP key pair, publish its public half to
keyserver.ubuntu.com, and store the base64 of the armored secret key as
PGP_SECRET and its passphrase as PGP_PASSPHRASE:
gpg --quick-generate-key 'jamescarr <james.r.carr@gmail.com>' rsa4096 sign never
gpg --keyserver keyserver.ubuntu.com --send-keys <key id>
gpg --export-secret-keys --armor <key id> | base64 | tr -d '\n' | gh secret set PGP_SECRET
The flow is the same as every other package (above):
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-java # or patch | major | 0.3.0
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-java
mise run release:watch sdk-java # the tag run signs, uploads, and waits for repo1
mise run release:verify sdk-java # HTTP 200 for the POM on repo1.maven.org
release:verify reads the POM at
https://repo1.maven.org/maven2/io/github/jamescarr/ankusa-sdk/<version>/ankusa-sdk-<version>.pom,
so it confirms the portal's publish reached the CDN, not just that the portal
accepted the bundle. The release workflow runs the same check in a loop for up
to an hour: the portal answers PUBLISHED before repo1 serves the POM.
sonaRelease cannot be exercised locally without the real token, so
release-maven.yml is only exercised by an actual tag.
sdk-clojure publishes as io.github.jamescarr/ankusa-clj on Clojars, from
packages/sdk-clojure/build.clj,
whose column-0 (def lib '...) and (def version "...") forms are the
coordinates — pkg_version and pkg_registry_code read them with sed and
release:prepare rewrites version, exactly as they do for the other kinds.
The artifact is ankusa-clj, not ankusa-sdk: the Clojure CLI resolves from
Maven Central and Clojars together, and Central already has
io.github.jamescarr:ankusa-sdk for the Java SDK.
The jar is built by tools.build (clojure -T:build jar) and holds only
ankusa/sdk/** sources plus META-INF; its POM lists org.clojure/clojure and
org.clojure/data.json and no build or test tool. mise run check:package sdk-clojure builds it and asserts both, so the shape the release uploads is the
shape CI gated on. The format, lint, and test tools are deps.edn
aliases (cljfmt, clj-kondo, the Cognitect test runner); mise has no entries for
them.
Publishing is clojure -T:build deploy, which uploads the jar jar already
built with deps-deploy and never rebuilds, so the jar the workflow attested is
the jar Clojars serves.
Needed once, before the first release:tag sdk-clojure, all by the repository
owner:
io.github.jamescarr group through
GitHub (https://clojars.org/verify/group). That is what lets the account
deploy under the group.CLOJARS_USERNAME repository secret and the token as
CLOJARS_PASSWORD. deps-deploy reads both names out of the environment.The flow is the same as every other package (above):
mise run status # version, last tag, published?, commits since
mise run release:prepare minor sdk-clojure # or patch | major | 0.3.0
# review and merge the PR it opens, then:
git switch main && git pull
mise run release:tag sdk-clojure
mise run release:watch sdk-clojure # the tag run builds, uploads, and waits for Clojars
mise run release:verify sdk-clojure # HTTP 200 for the POM on repo.clojars.org
release:verify reads the POM at
https://repo.clojars.org/io/github/jamescarr/ankusa-clj/<version>/ankusa-clj-<version>.pom.
clojure -T:build deploy cannot be exercised locally without the real token,
so release-clojars.yml is only exercised by an actual tag.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |