Bundle nix outputs, images, and binaries as GitHub Releases
Design record. Citations name paths in
compass= RigelBuild/compass atc25ce94f(this repo, main at authoring). Line numbers drift as code evolves; resolve against that revision. This is a public repo: the internal CD monorepo that consumes these artifacts is referred to by role, never by name, path, PR, or quoted source.
Status: Active — the three load-bearing forks (OQ-1/2/3) were ruled by Matt (2026-08-22); freezes on merge as the contract T1–T5 execute against. Tracking: RIG-1746 (whole-product semver + GitHub Releases). RIG-2103 is retargeted onto T4 (OQ-2); the OQ-1 desktop deferral is RIG-2477.
Problem / Intent
Section titled “Problem / Intent”CI/CD publishes container images to GHCR and an internal config-publish job
go installs a pinned compass CLI from source on every run — but nothing
produces a durable, versioned bundle of what a build actually shipped. This
record designs a GitHub Releases lane: one Release per qualifying build,
carrying the compiled binaries (compass CLI per-arch), the container image
identity (GHCR ref + digest), and the nix build output identity (store paths +
hashes), so consumers pin and download a verified artifact instead of
rebuilding from source.
The record lives in the compass repo because Fork 1 below concludes compass
owns the surface: GitHub Releases attach to a GitHub repo, and the public
RigelBuild/compass repo is where the release artifacts’ sources live, where
the existing GHA publish lane runs, and where an in-repo GITHUB_TOKEN can
create Releases with zero new credentials. The internal CD monorepo is
Woodpecker-driven and would need a cross-repo PAT to write Releases anywhere
(§Fork 1).
Approach
Section titled “Approach”Four design forks, each with options, a recommendation, and tradeoffs.
Fork 1: which repo + which surface owns Release creation
Section titled “Fork 1: which repo + which surface owns Release creation”Options:
- (a) compass repo, GitHub Actions — a new
.github/workflows/release.ymlbeside the existing publish lane. - (b) the internal CD monorepo, Woodpecker CD — a push→main CD job like its image-publish lane, writing Releases on some GitHub repo via a PAT.
- (c) hybrid — compass GHA creates the Release; the internal CD appends its own assets to it.
Recommendation: (a) compass GHA. Grounds:
-
The artifacts are compass-built. The compass CLI is
go/cmd/compassin the compass Go module — after the RIG-1930/RIG-2025 module-path rename (compass PR #458, in review at authoring; a Global Constraint below),compass go/go.mod:13on that branch reads:module github.com/RigelBuild/compass/go(main at
c25ce94fstill carries the pre-renamegithub.com/sealedsecurity/compass/go; the remaining in-flight half is the internal config-publish job’s install target, still on the old path — §Fork 2). The agent image builds fromcompass agent-image/via the vendored devenv/nix2container forks —compass agent-image/publish.sh:51:BUILD_OUT="$(nix run path:../forks/devenv#devenv -- container build agent)"A Release must attach to the repo whose commits it versions; releasing compass artifacts on the internal monorepo would version them against the wrong history.
-
The GHA precedent already exists and is the right shape. The compass repo deliberately runs artifact publishing as a separate least-privilege GHA workflow, not a CI-gate step —
compass .github/workflows/publish-agent-image.yml:3-9:# WHY A SEPARATE WORKFLOW, NOT A STEP IN THE CI GATE — a deliberate, principled# exception to this repo's ONE-JOB doctrine (see ci.yml's header):## - Least privilege. A GHCR push needs `packages: write`; the gate job runs# `contents: read` only.A Release workflow is the same pattern with
contents: writeinstead ofpackages: write. -
Auth is free in-repo. GHA’s ephemeral
GITHUB_TOKENcan create Releases and upload assets in its own repo with apermissions: contents: writegrant — no standing secret. The internal Woodpecker CD authenticates GHCR pushes with a provisionedGHCR_TOKEN— a packages-scoped credential; writing Releases from Woodpecker would need a new repo-scoped PAT to provision, rotate, and fork-gate (§Fork 4). -
The public repo is the consumption point.
RigelBuild/compassis public, so Release assets download anonymously — which is exactly what the internal config-publish consumer needs (§Fork 3). The internal monorepo is not the public surface.
Tradeoffs accepted: (b) would co-locate the Release cut with the internal CD’s other push→main GHCR legs, keeping one CD brain; but none of those legs produce compass artifacts, and the cross-repo PAT + Woodpecker↔GitHub seam outweighs the co-location. (c) hybrid is deferred as non-load-bearing: today no internally-built artifact belongs in a compass Release (the internal CD’s images are CI step images, not product artifacts). If that changes, the internal CD can append assets to an existing Release with a narrowly-scoped PAT without reopening this record.
Fork 2: what artifacts, and how each is produced
Section titled “Fork 2: what artifacts, and how each is produced”Three artifact classes. The principle: attach what consumers download; reference what registries already serve durably.
(i) Binaries — attach, per-arch.
- Which:
compass(the CLI — the one binary with a known external consumer: the internal config-publish job pins it, §Fork 3), pluscompass-serverandcompass-runner(the deployable daemons —compass go/cmd/also carriescompass-stack,compass-app,compass-gen-cert,compass-mint-runner-token,compass-postgres; those are dev/desktop tooling built on demand and are excluded from v1 to keep the asset set meaning something — an OQ records the boundary). - Arch matrix:
linux-amd64for all three binaries (the dogfood/CD target — the agent-image record froze linux/amd64 as the dogfood platform,compass docs/designs/platform/compass-agent-image-publish.md:54-67), plusdarwin-arm64for the CLI only (Matt’s macOS dev machines run the CLI against remote/dogfood stacks; the daemons deploy on Linux, and shipping darwin daemon builds would assert a support surface nothing consumes). Go cross-compiles the darwin CLI from the same ubuntu runner (GOOS=darwin GOARCH=arm64 CGO_ENABLED=0; the CLI is pure-Go — CGO is needed only by the pgtest suites,compass .github/workflows/ci.yml:263setsCGO_ENABLED: '1'for tests, not for the CLI build). Asset set: 3 × linux-amd64 + 1 × darwin-arm64 = 4 binaries. - How: plain
go buildin the release workflow, namedcompass_<version>_<os>-<arch>+ a singleSHA256SUMSasset. No goreleaser: the repo’s convention is zero-extra-tooling glue (compass agent-image/publish.sh:5-11argues exactly this for bash-over-frameworks), and goreleaser’s tag-driven model fights the per-build lane in Fork 3.
(ii) Container image — reference by digest, do NOT attach a tarball.
The agent image is already published durably to GHCR with an immutable
per-commit tag — compass .github/workflows/publish-agent-image.yml:33:
podman pull ghcr.io/rigelbuild/compass-agent:git-<sha12>and the publish enforces tag immutability + digest coherence
(publish-agent-image.yml:143-178, the verify step). A docker save/skopeo copy dir: tarball would duplicate a multi-GB closure GitHub caps and GHCR
already serves, and would create a second image identity to keep coherent.
Instead the Release body records ghcr.io/rigelbuild/compass-agent@sha256:…
(the config digest the publish verify step already computes,
publish-agent-image.yml:173) — a durable pointer to an immutable artifact.
Tradeoff: if GHCR retention ever deletes the package, the Release’s image
pointer dangles; accepted because the package is public + repo-linked and
nothing in the fleet deletes it, and a tarball’s cost is paid on every release
for a disaster that has a rebuild path (publish.sh re-mints any sha via
workflow_dispatch, publish-agent-image.yml:69).
(iii) Nix build outputs — attach the identity manifest, not the closure.
The repo’s nix outputs are the agent-image nix2container spec and the
toolchain derivations (compass devenv.nix:29-36 — go from go-overlay,
bun/node/moon vendored as derivations). A full closure export
(nix-store --export / nix copy to a file) of the agent image is the same
multi-GB payload rejected in (ii), and CI already declares binary caches as
the closure-serving mechanism (compass .github/workflows/ci.yml:165-168,
the pinned substituters). Attach instead a nix-outputs.json manifest:
per built derivation, the store path + narHash from
nix path-info --json (the image spec path publish.sh:52 captures, plus the
toolchain langs set ci.yml:183-184 already evaluates). That makes every
Release a verifiable statement of which nix outputs the build produced —
reproducible byte-for-byte from the tagged commit — without shipping the
bytes twice. Tradeoff: a consumer without nix or cache access cannot
materialize the closure from the Release alone; accepted — no such consumer
exists (the runtime consumer pulls the GHCR image; developers have nix), and
an OQ records closure-attachment as a possible later add for offline/archival
use.
Fork 3: versioning + trigger
Section titled “Fork 3: versioning + trigger”Options:
- (a) Release per push→main, tagged by commit (
build-<sha12>or CalVer+sha). - (b) Release per semver tag push (
vX.Y.Z), manual cut. - (c) Two lanes: an automatic prerelease per qualifying main push
(tag
build-<sha12>,prerelease: true) + a semver release onv*tag push (RIG-1746’s whole-product semver), both from the same workflow.
Recommendation: (c). The directive asks for “a durable, versioned Release
per build” — that is lane one. But RIG-1746 already reserves whole-product
semver as its own deferred concern (RIG-1746: “Whole-product semantic
versioning + GitHub Releases is DEFERRED to its own product-versioning
record”), and the agent-image record explicitly left the tag/release trigger
open for it (compass docs/designs/platform/compass-agent-image-publish.md: 185-186: “No tag/release trigger yet; that arrives with the GA
release-version tag”). Lane two implements that reserved slot: the same
workflow, triggered on push: tags: ['v*'], cuts a non-prerelease Release and
additionally pushes the :vX.Y.Z image tag via the positional-args seam the
agent-image record forward-planned (publish.sh:38-39: “A GA add is one more
positional arg (./publish.sh git-<sha> v<semver> latest)”) — invoked as
./publish.sh git-<sha12> v<semver>, deliberately WITHOUT latest.
publish-agent-image.yml serializes every :latest move under its own
concurrency group (publish-agent-image.yml:76-82: “Publishes SERIALIZE — an
in-flight :latest move must never be half-superseded”) and hard-fails its
verify when :git-<sha>/:latest digests diverge
(publish-agent-image.yml:171-178). A release-lane invocation moving :latest
under a different concurrency group (release) would break that
serialization: two workflows could interleave :latest moves, and the image
lane’s verify would red on a divergence the release lane caused. Alternatives
weighed: (a) share one concurrency group across both workflows — GitHub
concurrency groups do span workflows, but that serializes every release cut
behind every image build (and vice versa) and couples the two lanes’ latencies
for no gain; (b) drop latest from the release invocation — :latest is the
per-push image lane’s pointer and a semver cut needs only :vX.Y.Z; (c)
workflow_dispatch the image workflow to add the tag — indirection plus a
second run for one manifest write. (b) chosen: the semver lane mints
:vX.Y.Z only; :latest stays owned, moved, and verified exclusively by
publish-agent-image.yml. What vX.Y.Z means across the product (cadence,
changelog, what bumps major) stays RIG-1746’s record — this lane only gives it
the mechanical rail.
- Per-build tag scheme:
build-<sha12>(12-hex short sha, matching the image tag derivationpublish.sh:43). Git-sha, not date: it is the identity every existing pin already speaks (the pinned-rev constant, the:git-<sha12>image tag), and it is collision-free where CalVer needs a disambiguator. - Qualifying push: a
paths:filter over the binary-affecting inputs only —go/**,tools/toolchain/versions/go.nix, and the workflow file — NOT unioned with the image lane’s closure paths (OQ-3, ruled). It reuses theon.push.pathsnative-scoping technique the image publish uses (publish-agent-image.yml:44-68) but not that lane’s path set: an image-only sha (agent-image/**,forks/**,bun.lock) republishes the image with its GHCR per-sha identity but mints no Release, and a docs-only push mints nothing. Unioning the two path sets would mint binary-identical Releases on image-only shas — the exact outcome OQ-3 rejects. Prereleases are published, not draft: a draft is invisible to the anonymous asset-download consumer, and the per-build lane has no human step. - Semver releases: draft-then-publish is unnecessary too — the tag push is
the human act (only Matt pushes
v*tags). That is a convention, not an enforcement, so Fork 4 adds two hard controls on the tag lane. - Consumer cutover (the RIG-2103 overlap): the internal config-publish job
currently provisions the CLI by compiling it (
go installof the pinned rev), which requires a Go toolchain in the CD step image (the exact RIG-2025 regression: the job runs inci-base, which has none) and go-get module resolution (the exact module-path regression). Ordering is explicit: the in-flight RIG-2025 fixes (route the job toci-go; rename the module path, compass PR #458 + the internal job’s install-target repoint) land NOW and are independent of this record — T4 is a LATER revert of that routing once a Release exists to download, never a substitute for the P0 fix. With this lane, the job instead downloadshttps://github.com/RigelBuild/compass/releases/download/build-<sha12>/compass_<…>_linux-amd64and verifies againstSHA256SUMS— anonymous (public repo), toolchain-free, and still pinned by the same pinned-rev constant. No Release covers the current pina61d0cafretroactively, so T4’s landing path is a pin advance: move the pinned-rev constant to the first released sha as its own reviewed change, then cut the sourcing over. RIG-2103 proposed a nix derivation for the same problem; the Release asset is the lighter answer and T4 below proposes retargeting RIG-2103 to this cutover rather than running both. Matt ruled RETARGET (2026-08-22, OQ-2); RIG-2103’s Linear scope was updated to match.
Fork 4: auth + fork-safety
Section titled “Fork 4: auth + fork-safety”Options: (a) the workflow’s own GITHUB_TOKEN with contents: write;
(b) a releases-scoped fine-grained PAT; (c) reuse/widen the internal CD’s GHCR_TOKEN.
Recommendation: (a). Same-repo Release creation is exactly what
GITHUB_TOKEN covers; the agent-image record already weighed and rejected a
PAT for the analogous GHCR case (compass docs/designs/platform/compass-agent-image-publish.md:245-247: “a PAT/deploy
token — needed only for cross-repo or cross-org pushes, which this is not; a
standing secret to rotate for zero benefit”). (c) is a category error:
GHCR_TOKEN is the internal Woodpecker CD’s packages credential, provisioned
for image pushes — it never touches this GHA surface, and widening it to
contents: write would raise the blast radius of the internal CD’s
most-injected secret for nothing.
Fork-safety: the workflow triggers only on push: branches: [main],
push: tags: ['v*'], and workflow_dispatch — no pull_request event, so
fork-PR code never executes with the write token (the identical posture to
publish-agent-image.yml:44-69, which has no PR trigger for the same reason).
The dispatch lane carries the same main-ref guard the image publish uses
(publish-agent-image.yml:88-91: “guard so a dispatch from a feature branch
can never mint a :git-<sha> for unmerged code”). Workflow-level
permissions: block grants contents: write and nothing else; the CI gate
keeps its contents: read (compass .github/workflows/ci.yml:90-91).
Tag-lane hardening (the v* injection hole). A push: tags: ['v*']
trigger fires at whatever commit the tag names, wherever it lives — “agents
never push tags” (Global Constraint 5) is a convention, not an enforcement,
and the push-guard gates agent pushes, not every credential that could mint a
tag. Two hard controls close it:
- A GitHub tag ruleset on
RigelBuild/compassrestrictingv*tag creation to Matt (repo-admin bypass only) — the platform-level control, so a leaked or over-scoped credential cannot create a release-triggering tag. - An in-workflow ancestry guard on the tag lane: fail unless the tagged
commit is on main —
git merge-base --is-ancestor "$GITHUB_SHA" origin/main— mirroring the image workflow’s dispatch main-ref guard (publish-agent-image.yml:88-91), so even a ruleset mis-configuration cannot cut a non-prerelease Release (and a:vX.Y.Zimage tag) from unmerged code.
Alternatives considered
Section titled “Alternatives considered”- The internal Woodpecker CD as the release surface — rejected in Fork 1: wrong repo for the versioned history, needs a new cross-repo PAT, and the public download point is compass.
- Image tarball as a Release asset — rejected in Fork 2(ii): duplicates a registry-served multi-GB artifact and creates a second identity to keep coherent; the digest pointer is the durable record.
- Full nix closure export as an asset — rejected in Fork 2(iii): same size argument; the narHash manifest gives verifiability, caches give materialization.
- goreleaser — rejected in Fork 2(i): tag-only model fights the per-build prerelease lane; the repo convention is dependency-free glue scripts.
- Semver-only releases (no per-build lane) — rejected in Fork 3: fails the directive’s “Release per build” and leaves the config-publish consumer with no per-rev asset to pin.
- Signed provenance for v1 (attestations / cosign) — weighed, deferred.
SHA256SUMSuploaded from the same origin as the binaries is transfer-integrity, not provenance: a compromised workflow forges both. GitHub artifact attestations (permissions: attestations: write+gh attestation verify) or cosign signing would bind assets to the building workflow identity. Rejected for v1 because the only consumer is our own sha-pinned CD (T4), which already trusts the repo it pins; recorded as OQ-6 so a future external consumer triggers the add, not a redesign.
Global Constraints
Section titled “Global Constraints”- Module path rename is a prerequisite. The compass-side rename
github.com/sealedsecurity/compass/go→github.com/RigelBuild/compass/go(RIG-1930 / RIG-2025) lands via compass PR #458 (in review at authoring); the internal job’s install-target repoint is its in-flight other half. Every task here lands after the rename and uses theRigelBuildpath exclusively; nothing in this design may reintroduce asealedsecurity/compassreference. - Release names are
build-<sha12>(prerelease) andvX.Y.Z(release); binary assets arecompass_<tag>_<os>-<arch>— threelinux-amd64binaries + onedarwin-arm64CLI (Fork 2(i)) — plus oneSHA256SUMSper Release. - No PR-event trigger, ever, on any workflow holding
contents: write— the E1 fork posture of every publish lane in both repos. - The tag pair ordering rule extends: immutable identities first
(Release +
:git-<sha>), moving pointers last (:vX.Y.Zimage tag on the semver lane) — perpublish.sh:37-39.:latestis exclusively the image lane’s pointer; the release lane never moves it (Fork 3). v*tag creation is enforced, not assumed. A GitHub tag ruleset restrictsv*creation to Matt, and the tag lane hard-fails on a non-main-ancestor sha (Fork 4’s two controls). “Agents never push tags” remains the convention those controls back. The per-build lane needs no tag-push actor (GHA creates the tag via the Release API at the built sha).- Go version floor tracks
tools/toolchain/versions/go.nix(the same pin CI resolves,ci.yml:170-195); the release build must use the pinned toolchain, not asetup-godrift.
T1 — Release workflow: per-build prerelease lane.
New compass .github/workflows/release.yml: trigger push: branches: [main]
with a paths: filter over go/**, tools/toolchain/versions/go.nix, and the
workflow itself; permissions: {contents: write}; concurrency group
release, cancel-in-progress: false (serialize, like
publish-agent-image.yml:80-82). Steps: install nix (the pinned
cachix/install-nix-action + substituter block copied from ci.yml:150-168),
resolve the pinned Go toolchain, build the four binaries (three ×
linux-amd64 + the CLI × darwin-arm64, CGO_ENABLED=0 for the darwin
cross-build), generate SHA256SUMS, gh release create build-<sha12> --prerelease with the assets and a generated body carrying the image digest +
nix-outputs manifest (T2).
Interfaces: consumes HEAD sha + go/cmd/{compass,compass-server,compass-runner};
produces Release build-<sha12> with 6 assets (4 binaries + SHA256SUMS +
nix-outputs.json). Acceptance: a main push touching go/** yields a
downloadable, checksum-verifying compass_build-<sha12>_linux-amd64.
T2 — Release body + nix-outputs manifest generator.
A small script (bash or bun, matching publish.sh conventions) that emits:
(a) the GHCR image ref + config digest for the sha (querying the tag with the
fork skopeo exactly as publish-agent-image.yml:160-173 does; “image not yet
published for this sha” degrades to a recorded absence, not a failure — the
image lane is paths-filtered independently); (b) nix-outputs.json from
nix path-info --json over the toolchain langs set and, when built, the
agent-image spec path.
Interfaces: consumes git rev-parse HEAD, GHCR (read, anonymous),
tools/toolchain/gate-tools.nix; produces the release body markdown +
nix-outputs.json. Acceptance: unit-testable pure formatting; a dry-run mode
prints both without any GitHub write.
T3 — Semver lane + image release-tag.
Extend release.yml with push: tags: ['v*']: same build steps, Release is
non-prerelease named vX.Y.Z, guarded by the Fork 4 ancestry check
(git merge-base --is-ancestor "$GITHUB_SHA" origin/main — fail otherwise),
and additionally invokes ./publish.sh git-<sha12> v<semver> (the
forward-planned positional add, publish.sh:38-39; deliberately WITHOUT
latest — Fork 3’s concurrency resolution) so the GHCR image gains the
matching :vX.Y.Z tag. Requires packages: write on this lane only
(job-level permissions split). Includes provisioning the v* tag ruleset
(Fork 4 control 1) — an IaC change if the github Pulumi stack manages this
repo, else a documented one-time setting.
Interfaces: consumes the pushed tag ref; produces Release vX.Y.Z + GHCR
:vX.Y.Z. Acceptance: pushing a v0.0.1-test tag on a throwaway ref cuts a
coherent Release + image tag (then deleted); a tag on a non-main sha fails the
ancestry guard; documented in the workflow header.
T4 — Consumer cutover: the internal config-publish job downloads the Release asset.
Sequenced strictly AFTER the in-flight RIG-2025 fixes (ci-go routing + module
rename), which land now and independently — T4 is the later revert of that
routing, not its substitute. In the internal CD monorepo, two reviewed steps:
(1) pin advance — move the pinned-rev constant to the first released sha (no
Release covers the current pin a61d0caf retroactively); (2) replace the
go install provisioning with a download-and-verify of
releases/download/build-<sha12>/compass_…_linux-amd64 + SHA256SUMS check,
then drop the Go-toolchain requirement from the CD job (reverting the RIG-2025
ci-go routing). Keep the single pinned-rev constant. RIG-2103 is retargeted onto
this task (Matt ruled RETARGET, 2026-08-22, OQ-2); the exact file targets live
in RIG-2103.
Interfaces: consumes Release assets (anonymous HTTPS); produces the same
binDir/compass provisioning contract the CD job returns today. Acceptance:
the existing unit tests’ injected-runner seam re-covers the new argv; a CD run
on main provisions the CLI with no go in the image.
T5 — Docs + record freeze.
Update docs/architecture/build-and-ci.md (the doc the image publish workflow
already cross-references, publish-agent-image.yml:39-40) with the release
lane; append the DECISIONS.md ledger rows for the choices in Forks 1-4.
Ordering: T1 → T2 fold into one PR if small; T3 independent after T1; T4 lands in the internal CD monorepo only after the RIG-2025 fixes are green on main AND T1 has minted a Release at the advanced pin (the pin advance is T4 step 1); T5 with the record freeze.
- T1 —
release.ymlper-build prerelease lane (binaries + SHA256SUMS) - T2 — release body generator (image digest pointer + nix-outputs.json)
- T3 — semver
v*lane +:vX.Y.Zimage tag via publish.sh - T4 — internal config-publish cutover: go install → Release asset download
- T5 — docs/architecture update + DECISIONS.md ledger rows
Resolved decisions and deferred questions
Section titled “Resolved decisions and deferred questions”The three load-bearing questions were put to Matt and ruled (2026-08-22); the rulings below are the frozen contract. OQ-4/5/6 stay deferred (non-load-bearing).
- OQ-1 (load-bearing): binary set boundary. RESOLVED (Matt, 2026-08-22): DEFER
the desktop lane. v1 attaches
compass,compass-server,compass-runneronly.compass-stack/compass-app(the Wails desktop lane — platform-linked,compass devenv.nix:189-190notes the app links the system WebKit framework on macOS, so it does NOT cross-compile from an ubuntu runner) are out of scope here; their release-artifact distribution is decided in the native-packaging lane (compass docs/designs/product/compass-native-packaging/design.md, RIG-1687 umbrella). The deferral is filed as RIG-2477 (parented under RIG-1687) so it has a tracked home rather than living only as a design-record note. - OQ-2 (load-bearing): RIG-2103 disposition. RESOLVED (Matt, 2026-08-22): RETARGET. RIG-2103 (formerly “compass CLI via a nix derivation”) is retargeted onto T4’s Release-asset download — one sourcing mechanism, checksum-verified, toolchain-free; a nix derivation adds hermeticity the CD job does not need. RIG-2103 now IS this record’s T4 consumer-cutover task (its Linear scope was updated 2026-08-22 to match), gated on T1 minting a Release at the advanced pin.
- OQ-3 (load-bearing): per-build lane trigger breadth. RESOLVED (Matt,
2026-08-22): every qualifying push, binary-affecting scope. The per-build
prerelease lane triggers on every qualifying main push (cheap — binaries only,
no nix image build), filtered by the binary-affecting paths (
go/**+ the toolchain pin), NOT unioned with the image lane’s paths. An image-only sha (agent-image/**,forks/**,bun.lock) republishes the image but mints NO Release: its image identity stays GHCR-only (:git-<sha12>+ digest) and is recorded in the next qualifying Release’s body. Rationale: the Release lane versions downloadable binaries; the image already has an immutable per-sha GHCR identity, and unioning the filters would mint binary-identical Releases on image-only shas. - OQ-4 (non-load-bearing, deferred): closure attachment. Whether an
archival lane should ever attach full nix closures (
nix copy --to file://) for offline reproduction. No consumer today; caches + pinned substituters (ci.yml:165-168) serve materialization. Revisit if an air-gapped consumer appears. - OQ-5 (non-load-bearing, deferred): prerelease retention.
build-*prereleases accumulate one per qualifying push. GitHub has no Release quota pressure; a later janitor (keep last N + all semver) can be added without contract change. Deferred. - OQ-6 (non-load-bearing, deferred): signed provenance. GitHub artifact
attestations (
attestations: write+gh attestation verify) or cosign would bind assets to the building workflow identity;SHA256SUMSalone is transfer-integrity, not provenance (§Alternatives). No external consumer yet — revisit when one appears.