Skip to content

Release path: digest promotion, self-verifying publish, RC tags - #60

Merged
sagiba merged 6 commits into
mainfrom
release/publish-path
Jul 27, 2026
Merged

Release path: digest promotion, self-verifying publish, RC tags#60
sagiba merged 6 commits into
mainfrom
release/publish-path

Conversation

@sagiba

@sagiba sagiba commented Jul 27, 2026

Copy link
Copy Markdown
Member

The release path itself — everything that only runs on a v* tag. After this,
v0.1.0-rc.1 becomes possible.

Digest promotion (the structural change)

The image is now pushed by digest with no tags, smoked, and only then tagged
via imagetools create. Previously build-push-action attached X.Y.Z and
latest in the same step, so the release tags named a build nothing had ever
executed
— while dev sha- images got a full runtime smoke in ci.yml. The
release was the least-verified image we ship.

It now gets that same deep smoke (Parquet write → DuckDB read-back → resume
no-op) against the exact digest the tags will point at. imagetools create
copies no bytes; it repoints tags at the smoked digest.

Signing, attestation, and a release that verifies itself

  • SHA256SUMS and the SBOM are now signed — previously omitted, and SHA256SUMS
    is the one file whose integrity transitively covers every other asset. Signed
    last, deliberately.
  • Artifact attestations alongside cosign: assets, the image digest
    (push-to-registry), and the SBOM. Signatures say "these are the bytes that
    were signed"; attestations say "this workflow, at this commit, built it".
  • COSIGN_EXPERIMENTAL removed — a no-op since cosign 2.0 (2023).
  • Draft → verify → publish. gh release create publishes and then uploads,
    so a mid-upload failure left a live half-release and notified watchers. It now
    creates a draft, runs the exact commands the docs give users (sha256sum --check, cosign verify-blob, cosign verify, gh attestation verify), and
    only un-drafts if they pass. A signature nobody can verify is worse than no
    signature, because it invites trust — so that failure belongs in CI, not in a
    user's terminal.

Release candidates

vX.Y.Z-rc.N is now accepted. It publishes its own immutable container tag only
(no latest, no rolling X.Y) plus a GitHub pre-release; everything else runs
identically, which is the point.

Not the full SemVer pre-release grammar — -rc.N is the only form with
implemented publish semantics, and -alpha/-beta.2+build would parse as valid
while meaning nothing here.

Also adds the rolling X.Y tag for stable releases. No bare X while 0.x.

Docs

RELEASING.md gets the RC section, including that an RC is not promoted
its jar reports X.Y.Z-rc.N, so shipping those bytes as X.Y.Z would contradict
the tag. install.md gets the verification commands with the exact certificate
identity and issuer, not just flag names — an identity a user cannot construct is
a story they won't follow. Note this pins the workflow filename: renaming
release.yml changes the identity and invalidates every published instruction.

Plus the two first-run traps found while smoke-testing the public image today:
the UID-10001 write failure (recommending --user over chmod 777) and why a
short containerised run looks silent.

Verification, and its limits

Done: workflow parses; publish step order confirmed post-parse; tag selection
simulated for rc/0.x/1.x; all twelve pinned action SHAs resolved against the
API
(the two new attestation actions are v4 commits, dereferenced from tag
objects — I do not pin SHAs from memory); verifyReleaseVersion exercised across
the rc/stable/mismatch/-beta.1/-SNAPSHOT matrix; docs test green.

Not done, and cannot be before merge: none of the publish job runs without a
tag. Environment approval, GHCR push under the release token, cosign OIDC,
attestation upload, imagetools create, and gh release create are all still
unexercised. That is exactly why RC support ships here — v0.1.0-rc.1 is the
test for this PR
, and I would expect it to surface something.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for release candidates using the vX.Y.Z-rc.N format.
    • Release candidate images use dedicated RC tags without latest or rolling tags.
    • Releases now undergo digest-based smoke testing and verification before publication.
    • Added signed checksums, software bills of materials, and provenance attestations for release assets and container images.
  • Documentation

    • Expanded installation and release guidance with download, image, signature, and attestation verification instructions.
    • Added container runtime notes covering permissions and progress logging.

sagiba added 3 commits July 27, 2026 18:22
The publish job has never executed -- environment approval, GHCR push under
the release token, cosign OIDC, attestation upload, gh release create are all
untested. Tags are meant to be immutable, so a v0.1.0 that fails halfway
leaves a half-published release and a version that cannot be cleanly redone:
you would burn 0.1.0 and ship 0.1.1 as the real first release. An rc number is
free to burn; a version number is not.

verifyReleaseVersion and just release now accept X.Y.Z and X.Y.Z-rc.N, keeping
the tag<->canonical-version equality check that makes drift impossible.

Deliberately NOT the full SemVer pre-release grammar: -rc.N is the only form
this project ships, and an exhaustive pattern would accept -alpha/-beta.2+build
and other identifiers whose publish semantics nothing implements. Build
metadata stays unsupported -- it does not affect precedence, so it cannot mean
anything useful on a release tag.

Verified across the matrix: rc tag + rc version passes; every tag/version
mismatch fails on the equality check; -beta.1 fails on grammar; a -SNAPSHOT
canonical version still fails.
…hing

Reworks the publish job so the release stops being the least-verified thing
we ship.

Digest promotion. The image is now pushed BY DIGEST with no tags, smoked, and
only then tagged via imagetools create. Previously build-push-action attached
X.Y.Z and latest in the same step, so the release tags named a build nothing
had ever executed -- while dev sha- images got a full runtime smoke in ci.yml.
The release image now gets that same deep smoke (Parquet write, DuckDB
read-back, resume no-op) against the exact digest the tags will point at.
imagetools create copies no bytes; it repoints tags at the smoked digest.

Rolling X.Y tag for stable releases. No bare X while 0.x -- a '0' tag would
imply a stability contract 0.x disclaims.

RC branching. A -rc.N tag publishes its own immutable container tag only (no
latest, no X.Y) and a GitHub pre-release. Everything else -- promotion,
smoke, signing, attestation, self-verification -- runs identically, which is
the whole point of a rehearsal.

Signing now covers SHA256SUMS and the SBOM, previously omitted. SHA256SUMS is
signed last and deliberately: it is the one file whose integrity transitively
covers every other asset. Dropped COSIGN_EXPERIMENTAL, a no-op since cosign
2.0 went keyless-GA in 2023.

Artifact attestations alongside cosign, answering the question signatures
cannot: which workflow, at which commit, produced this. Assets, the image
digest (push-to-registry), and the SBOM. Both actions pinned to verified v4
commits resolved via the API, not from memory.

Draft, verify, then publish. gh release create publishes and THEN uploads, so
a mid-upload failure left a live half-release and notified watchers. Now it
creates a draft, runs the exact verification commands the docs give users --
sha256sum --check, cosign verify-blob, cosign verify, gh attestation verify --
and only un-drafts if they pass. A signature nobody can verify is worse than
no signature, so that failure belongs here rather than in a user's terminal.
It is also what makes the rc rehearsal assert something instead of merely
executing.

Verified: workflow parses; step order correct; tag selection simulated for
rc/0.x/1.x; all twelve pinned action SHAs resolve via the API.
RELEASING.md: the versioning rule now admits X.Y.Z-rc.N (and still nothing
else); a new section explains what a release candidate does, why it exists --
a failure costs an rc number instead of an immutable version -- and that it is
NOT promoted, since its jar reports X.Y.Z-rc.N and shipping those bytes as
X.Y.Z would contradict the tag. The cut steps now describe draft -> verify ->
publish, including that a failed self-verification leaves the release a draft.

Both docs gain the verification commands, quoting the exact certificate
identity and OIDC issuer rather than just naming the flags -- an identity a
user cannot construct is a verification story they will not follow. These are
the same commands the pipeline runs against itself before un-drafting, so the
docs cannot drift from what actually passes. Note this pins the workflow
FILENAME: renaming release.yml would change the identity and invalidate every
published instruction.

install.md also documents the two first-run traps hit while smoke-testing the
public image today: the container runs as UID 10001 so a directory you own is
not writable (recommending --user over chmod 777, which also leaves output
owned by you), and a short run looks silent because progress is appended every
30s off a terminal rather than redrawn.

Verified: HeadlineDocsCommandSmokeTest, which reads docs/install.md, passes.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c52e8ec-7fc4-4933-8410-212d82d0a137

📥 Commits

Reviewing files that changed from the base of the PR and between a62c094 and 436a4ff.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • RELEASING.md
  • docs/install.md
  • justfile
  • scripts/ci/smoke-container.sh
📝 Walkthrough

Walkthrough

The release process now supports vX.Y.Z-rc.N versions, publishes and smoke-tests container images by digest before tagging, signs and attests release outputs, and verifies draft GitHub releases before undrafting them. Documentation and release commands now describe these stable and prerelease workflows.

Changes

Release pipeline

Layer / File(s) Summary
Version and RC contracts
build.gradle.kts, justfile, RELEASING.md
Version validation accepts stable and -rc.N forms, while release guidance defines RC container tags and promotion behavior.
Digest-based image publishing
.github/workflows/release.yml
The workflow classifies the release, pushes an image by digest, smoke-tests it, and applies computed tags afterward.
Signing, attestation, and draft release verification
.github/workflows/release.yml
Release assets and the image receive signatures and attestations; the GitHub release is created as a draft, verified, and then undrafted.
Operator and download verification guidance
RELEASING.md, docs/install.md
Release approval, checksum, signature, attestation, immutable image verification, and container runtime instructions are documented.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseWorkflow
  participant GHCR
  participant GitHubRelease
  participant Verification
  ReleaseWorkflow->>GHCR: Push image by digest
  ReleaseWorkflow->>GHCR: Smoke-test digest and apply release tags
  ReleaseWorkflow->>GitHubRelease: Create draft release with assets
  ReleaseWorkflow->>Verification: Verify checksums, signatures, and attestations
  Verification-->>ReleaseWorkflow: Verification succeeds
  ReleaseWorkflow->>GitHubRelease: Undraft release
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: digest-based release promotion, self-verifying publish flow, and RC tag support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/publish-path

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 173-178: Update the “Install DuckDB CLI” step to verify the
downloaded release asset’s authenticity before installation or execution. Pin
and validate the expected checksum or an upstream signature for the exact DuckDB
version and platform, fail the workflow on verification failure, then retain the
existing extraction and installation flow only after successful validation.
- Around line 284-314: Update the “Self-verify the published release” workflow
step to download the draft release assets into a fresh temporary directory using
the release identifier before verification. Run sha256sum, cosign verify-blob,
and gh attestation verify against the downloaded SHA256SUMS and signature files
there, and complete these checks before the release is un-drafted; leave image
verification unchanged.
- Line 150: Update the tag output in the release workflow to include the
documented major-version tag using VERSION%%.* for stable 1.x+ releases, while
preserving the existing full, minor, and latest tags and the intended 0.x
exception behavior.

In `@docs/install.md`:
- Around line 102-113: Update the fenced command blocks around the TAG/IDENTITY
verification commands and the additional blocks at the referenced locations to
include a shell language identifier, using ```sh or ```console, without changing
their command contents.
- Around line 95-97: Revise the release verification statement in
docs/install.md to distinguish the signed checksum manifest and OCI
image/SHA256SUMS attestations from per-asset provenance; do not claim every
release asset has its own SLSA attestation unless the release workflow adds and
verifies those attestations.

In `@justfile`:
- Around line 95-96: Update the comments and usage example near the VERSION
validation command to document that X.Y.Z-rc.N release candidates are supported,
removing the stale guidance that rejects all pre-releases. Keep the existing
validation behavior and error message unchanged.

In `@RELEASING.md`:
- Around line 104-123: Update the fenced code block containing the release
verification commands to declare the Bash language on its opening fence, while
leaving the commands unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b005ea3-1457-4735-b434-a8161723c791

📥 Commits

Reviewing files that changed from the base of the PR and between 066759d and a62c094.

📒 Files selected for processing (5)
  • .github/workflows/release.yml
  • RELEASING.md
  • build.gradle.kts
  • docs/install.md
  • justfile

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread docs/install.md Outdated
Comment thread docs/install.md Outdated
Comment thread justfile
Comment thread RELEASING.md Outdated
Comment on lines +104 to +123
```
TAG=v0.1.0
IDENTITY="https://github.com/varveio/swath/.github/workflows/release.yml@refs/tags/${TAG}"
ISSUER=https://token.actions.githubusercontent.com

# 1. Checksums cover every asset.
sha256sum --check SHA256SUMS

# 2. The checksum file itself is signed, so step 1 is trustworthy.
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
--certificate-identity "$IDENTITY" --certificate-oidc-issuer "$ISSUER" SHA256SUMS

# 3. The image, by digest.
cosign verify --certificate-identity "$IDENTITY" --certificate-oidc-issuer "$ISSUER" \
ghcr.io/varveio/swath@sha256:<digest>

# 4. Build provenance — which workflow, at which commit, built this.
gh attestation verify oci://ghcr.io/varveio/swath@sha256:<digest> --repo varveio/swath
gh attestation verify SHA256SUMS --repo varveio/swath
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Declare the fenced block language.

Add bash to the opening fence at Line 104 to satisfy markdownlint MD040.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@RELEASING.md` around lines 104 - 123, Update the fenced code block containing
the release verification commands to declare the Bash language on its opening
fence, while leaving the commands unchanged.

Source: Linters/SAST tools

sagiba added 3 commits July 27, 2026 18:32
The deep runtime smoke was copy-pasted into both publish paths -- same bucket,
same DuckDB read-back, same resume-no-op assertion -- differing only in the
work directory and how the image is named. That is the wrong shape for this
check in particular: its whole purpose is that the release image gets the SAME
verification as dev images, and two copies drift silently.

scripts/ci/smoke-container.sh <image-ref> [work-dir] is now called from
ci.yml's docker-publish and release.yml's publish, matching the existing
prepare/verify-container-promotion.sh pattern, and runs locally against any
image reference.

It refuses to reuse an existing work directory rather than pre-deleting one.
The smoke deliberately does NOT pass --user, because exercising the image's
own default identity (UID 10001) is part of what it verifies -- but that means
outputs land in 10001-owned subdirectories the invoking user usually cannot
unlink, so an 'rm -rf' here would fail on the second local run. Found by
hitting exactly that while testing; it now exits 2 with the elevation hint.

Verified against the live public image: 7 objects listed, 7 read back via
DuckDB, resume no-op left the count unchanged, exit 0. Negative paths: wrong
arity exits 2, an existing work directory exits 2, an unresolvable image exits
non-zero rather than passing vacuously. bash -n and shellcheck clean.
A release publish now depends on listing a bucket we do not control. The
default target is a CMAS example case for the SMOKE emissions model -- a
third-party research-consortium bucket. If it is ever removed, renamed, or
flipped to requester-pays, the release path breaks at the worst moment: after
environment approval, with the image already pushed by digest and its tags not
yet applied.

SWATH_SMOKE_BUCKET lets an operator repoint it with a repository variable and
a re-tag, instead of a code change, a PR and a merge while a half-published
release waits. Same reasoning as PUBLIC_RELEASE_ENABLED being a runtime switch.
Both workflows pass vars.SWATH_SMOKE_BUCKET through; unset leaves today's
behaviour exactly as it was.

Also corrects the inherited comment describing the bucket as 'a small, stable
public AWS Open Data prefix'. It is public, small and us-east-1, but I could
not confirm it belongs to the Registry of Open Data on AWS, and the
distinction matters now that a release depends on it. Notes too that 'SMOKE'
in the bucket name is Sparse Matrix Operator Kernel Emissions, not a smoke
test -- the name is a coincidence.

Verified all three paths against the live image: unset -> 7 objects (default),
overridden -> 2 objects (a different bucket), empty string -> falls back to
the default. That last case is what the workflow actually passes when the
variable is unset, so the :- form is required.
…e major tag

Seven review findings, all valid.

Verify DuckDB before executing it. The install curl'd a binary and sudo
install'd it with no integrity check, in a job holding contents:write,
packages:write, id-token:write and attestations:write -- the
highest-privilege foothold in the pipeline, able to sign and publish. Now
checksum-pinned, at all three call sites (release.yml plus both in ci.yml).

Verify what GitHub STORED, not what we uploaded. The self-verification ran
sha256sum and cosign verify-blob against release-assets/ in the workspace,
re-confirming files already known good and missing the failure it exists to
catch -- a truncated or mis-uploaded asset. It now downloads the draft's
assets into a fresh directory and verifies those.

Implement the bare major tag for 1.x+. The comment promised '{{major}} at
1.0' but no code emitted it, so at 1.0 the behaviour would silently not
appear. Now conditional on the major not being 0, which is the exception the
comment described.

Correct the attestation claim in install.md. It said every asset 'carries a
SLSA build-provenance attestation'; the SBOM is signed but not separately
attested, since SHA256SUMS covers it. Reworded to state exactly what is
attested.

Also: the justfile's release comment still said stable-only after this branch
taught it -rc.N, and the fenced blocks this PR added lacked language
identifiers.

Not addressed, deliberately: unlabelled fences PREDATING this PR (5 in
install.md, 11 in packaging-and-docker.md). Mechanical and unrelated to the
release path; fixing them here would bury this diff.

Verified: workflows parse; justfile parses; shellcheck clean; docs test green;
DuckDB checksum computed from the pinned v1.1.3 asset.
@sagiba
sagiba merged commit 7619d5b into main Jul 27, 2026
5 checks passed
@sagiba
sagiba deleted the release/publish-path branch July 28, 2026 12:47
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.

1 participant