This document describes how to cut a release, what artifacts are produced, and how to verify them.
-
Ensure the
mainbranch is in a releasable state and tests pass. -
Trigger the Release workflow manually — either via the GitHub Actions UI ("Run workflow" → enter version without
vprefix) or viagh:gh workflow run release.yml -f version=1.2.3
All jobs check out
main, so the dispatch ref is irrelevant. -
The workflow:
- Builds GraalVM native binaries for all supported platforms in parallel.
- Packages each binary into a platform-appropriate archive.
- Publishes the
libandclimodules to Maven Central asorg.virtuslab:cellar-lib_3:<version>andorg.virtuslab:cellar-cli_3:<version>. - Generates a SHA256 checksum file.
- Signs the checksum file using cosign keyless (OIDC).
- Updates
flake.nixwith the new version and SRI hashes, commits tomain. - Creates and pushes a
v<version>git tag. - Publishes a GitHub Release with all artifacts attached.
The Maven publish waits for the native-binary and JAR builds to succeed before uploading — a Sonatype release is immutable, so we must not publish a version whose corresponding GitHub Release assets (referenced by the coursier app descriptor) failed to build. If Sonatype then rejects the upload (e.g. duplicate version, namespace mismatch, signature failure), no GitHub Release is created and no tag is pushed.
No container images are built or published by this release flow.
For unstable, GitHub-only builds of in-progress work there is a separate Snapshot workflow, triggered manually:
gh workflow run snapshot.yml # snapshot the current default branch
gh workflow run snapshot.yml --ref my-feature # snapshot a specific branchUnlike the release flow, it builds from the dispatched ref (not pinned to main), so feature branches can be snapshotted. It:
- Builds the native binaries + assembly JAR, stamping the version as
0.1.0-SNAPSHOT-<short-sha>. - Publishes them to a fresh
snapshot-<short-sha>-<run-number>prerelease. - Attaches a
checksums.txt. - Prunes any older
snapshot/snapshot-*releases (and their tags), so exactly one snapshot survives.
What it deliberately does not do: publish to Maven Central, sign with cosign, rewrite flake.nix, commit to main, or push a version tag.
Each run uses a unique tag because immutable releases are enabled on the repo: an immutable release cannot be mutated (so target_commitish and assets can't be overwritten on a reused tag), and a tag name that once belonged to an immutable release can never be reused. A single rolling snapshot tag is therefore impossible; <run-number> keeps the tag unique even across re-snapshots of the same commit. Deleting an immutable release and its tag is allowed, which is how pruning works.
The release is marked prerelease and make_latest: false, so it never displaces the real "Latest release". Snapshots are not resolved by cs install cellar, which reads Maven metadata.
Because tags are no longer static, download URLs change every run. Consumers resolve the newest snapshot via the GitHub API — see README.md.
| Platform | Runner | Archive |
|---|---|---|
| Linux x86_64 | ubuntu-latest |
cellar-<version>-linux-x86_64.tar.gz |
| Linux aarch64 | ubuntu-24.04-arm |
cellar-<version>-linux-aarch64.tar.gz |
| macOS arm64 | macos-latest |
cellar-<version>-macos-arm64.tar.gz |
Windows — not currently supported. The Mill build uses a Unix shell launcher (
./mill). Windows support can be added when amill.batlauncher is available in the repository.
Each GitHub Release contains:
| File | Description |
|---|---|
cellar-<version>-<os>-<arch>.tar.gz |
Archive containing the cellar binary and README.md |
checksums.txt |
SHA256 checksums for all archives |
checksums.txt.bundle |
Sigstore bundle for the checksum file |
Two modules are published per release:
| Coordinate | Contents |
|---|---|
org.virtuslab:cellar-lib_3:<version> |
The dependency-API library — symbol resolver, formatters, Maven coordinate parsing, etc. |
org.virtuslab:cellar-cli_3:<version> |
The CLI driver (cellar.cli.CellarApp and friends). Regular Scala library JAR — does not include the bundled JRE blob used by the GraalVM native image. |
cellar-lib is what coursier resolves for cs install cellar — the coursier/apps descriptor reads maven-metadata.xml for cellar-lib_3 to determine the latest version, then downloads the matching native binary from this repo's GitHub Release.
The -javadoc.jar of every published module is empty by design (see CellarPublishModule in build.mill): Central requires the artifact to exist, but nobody consumes cellar as a library, and scaladoc generation has failed a release before by choking on a dependency's TASTy — a failure mode we can neither fix nor usefully gate on. Nothing builds scaladoc any more; ./mill <module>.scalaDocGenerated still works if you ever want it locally.
Both modules are independently usable as Scala 3 dependencies:
mvn"org.virtuslab::cellar-lib:<version>"
mvn"org.virtuslab::cellar-cli:<version>"The publish-maven job is gated on a GitHub Environment named maven-central. Without it the job will fail to start and no secrets are exposed. Configure the environment under Settings → Environments → New environment → maven-central:
- Required reviewers: at least one trusted maintainer. Each release run pauses at the publish step until a reviewer approves in the Actions UI.
- Deployment branches and tags: "Selected branches" → only
main. Prevents a workflow_dispatch from a feature branch from accessing the secrets at all. - Environment secrets (NOT repo-level secrets):
| Secret | Purpose |
|---|---|
SONATYPE_USERNAME |
Central Portal user-token name (generated at central.sonatype.com) |
SONATYPE_PASSWORD |
Central Portal user-token value |
PGP_SECRET |
Base64-encoded ASCII-armored PGP private key (artifact signatures) |
PGP_PASSPHRASE |
Passphrase for the PGP key |
Why on the environment, not the repo: a workflow on a non-main branch can read any repo-level secret if it's modified to echo it; environment secrets are only injected when the environment's branch rule is satisfied AND the reviewer approves.
Mill reads these via MILL_SONATYPE_USERNAME / MILL_SONATYPE_PASSWORD / MILL_PGP_SECRET_BASE64 / MILL_PGP_PASSPHRASE environment variables — the workflow's env: block is the bridge.
The PGP signing here is unrelated to the cosign signing of checksums.txt — Sonatype Central mandates .asc signatures on every uploaded artifact (.jar, .pom, -sources.jar, -javadoc.jar), while cosign covers the GitHub Release assets.
Download the archive and checksums.txt, then:
sha256sum --check --ignore-missing checksums.txtOn macOS:
shasum -a 256 --check --ignore-missing checksums.txtThe checksums.txt file is signed using cosign with GitHub OIDC (keyless signing). No private key is stored in the repository.
Install cosign (instructions), then verify:
cosign verify-blob \
--bundle checksums.txt.bundle \
--certificate-identity-regexp "https://github.com/VirtusLab/cellar/.github/workflows/release.yml@refs/tags/v" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txtA successful verification prints Verified OK.