Stage fork releases for the cache signer (RIG-5698)
This record lives under woodpecker, the fork lane’s surface, and covers all four forks: woodpecker, oh-my-pi, jj-vine and nix2container.
Open Questions
Section titled “Open Questions”The questions are independent. The design assumes each recommendation.
- Where the producer code lives (Matt).
- (a) A copy in each fork. A fix is four PRs that each review code.
- (b) A reusable workflow in one fork. The signer matches
job_workflow_ref, which carries the called ref, against acallee, so every SHA bump also changes the trust policy. - (c) A new public repo,
RigelBuild/nix-cache-producer, with composite actions pinned by SHA. These run in the caller’s job, sojob_workflow_refstays the fork’s workflow and the drafted trust entries need nocallee. A fix is one producer PR plus four SHA-bump PRs. Its minter makes a ninth holder of the automation App key. - Recommendation: (c), with a fork-owned pin check (§ Where it runs). Under (a), T1 is dropped and T2–T5 are copied into each fork.
- Upload path for large NARs (Matt; the signer owner, RIG-5407). The
rigel.buildzone is on Cloudflare Pro: a proxied body over 100 MB gets 413 (Cloudflare), and the origin must answer within about 100 seconds (Cloudflare). The omp NAR is about 251 MB. The signer sits behind a Cloudflare Tunnel, and a tunnel hostname must be proxied, so a DNS-only host needs a new public LoadBalancer, a certificate in the pod, and no WAF in front of the upload routes.- (A) The whole signer host is DNS-only, on that LoadBalancer.
- (B) Chunked PUTs of 64 MiB parts, assembled in the signer’s spool file.
narHashis still checked over the whole NAR. - (C) The attestation call returns presigned R2 PUT URLs. It removes the size, deadline and slot limits, but reopens RIG-5699’s rejected temporary-credential option.
- (D) A second DNS-only host for the staging routes only, with A’s costs.
- Recommendation: (B). It keeps the tunnel and WAF, and the signer owner agrees.
- A non-reproducible rerun blocks a release (the signer owner; Matt if the
gate rule changes). A rerun that rebuilds differently stores a second
attestation for the system, and the publisher rejects the release forever.
- (A) The publisher ignores attestations whose staging never completed.
- (B) Recovery is a new
-rigel.N+1release. - (C) The producer withdraws its uncompleted attestation by
DELETE. - (D) A rerun uploads the first run’s saved closure. Heavy for omp.
- Recommendation: (A), with (B) until it lands.
- Substituting from Rigel’s public cache (Matt). A release rebuilds
unchanged paths Rigel already published. A non-identical rebuild conflicts
with the published narHash, and the gate quarantines it.
- (A) Also substitute from Rigel’s public cache, with its key.
- (B) cache.nixos.org only; T5 builds each fork twice and compares.
- Recommendation: (A). Unchanged paths come back as the signer’s own bytes.
Decisions
Section titled “Decisions”Matt ruled on RIG-5699 (2026-10-18). D1 and D2 record the ruling.
- D1 (DL-583) — A fork stages its closure through the signer’s OIDC-authenticated upload endpoint. It holds no signing key, no bucket credential and no other long-lived cache secret.
- D2 (DL-584) — The digest test is split. The producer tests golden vectors copied from the signer. A signer-side gate test on a fork-shaped fixture catches drift.
Problem / Intent
Section titled “Problem / Intent”The cache signer publishes a fork release only after the fork has attested it
and staged its full closure (parent RIG-4790). Nothing produces either today:
each fork’s release-tag.yaml mints a vX.Y.Z-rigel.N tag on a push to
rigel-release and stops. The producer builds the manifest roots per system,
attests their runtime closure, and stages it unsigned through the signer. The
signer publishes when the consumer’s bump PR moves the fork’s pin.
Approach
Section titled “Approach”Where it runs
Section titled “Where it runs”The signer accepts a staging token only for event_name push, ref
refs/heads/rigel-release, and workflow_ref and job_workflow_ref naming
the fork’s release-tag.yaml on that branch. So the producer runs in that
workflow, after mint, which is unchanged:
resolveneedsmintand runs only onpush. A fork-owned first step reads the workflow atgithub.sha, requires one producer SHA across the produceruses:, and requires it on the producer’smain. The action then outputs the onevX.Y.Z-rigel.Nremote tag atgithub.sha, or nothing. Two such tags fail.stageneedsresolve, runs only on its tag, and has one matrix leg per system, withfail-fast: falseandtimeout-minutes: 180.
resolve reads the remote because no mint script writes an output, and
“already exists; nothing to mint” never checks the tag’s target. Forks merge
into rigel-release only by merge commit or squash, so the tag lands on
github.sha, as the signer requires.
stage has exactly contents: read and id-token: write. Neither job has an
environment, which keeps the App key out and fixes the token’s sub. The
signer stores sub with each attestation and answers 409 to a repeat under
another sub, so adding an environment later breaks reruns. Standard
GitHub-hosted runners are free for public repositories
(GitHub).
| Fork | x86_64-linux (ubuntu-24.04) |
aarch64-linux (ubuntu-24.04-arm) |
aarch64-darwin (macos-15) |
|---|---|---|---|
| woodpecker | woodpecker-agent |
woodpecker-server, woodpecker-agent |
woodpecker-agent |
| oh-my-pi | omp |
— | omp |
| jj-vine | jj-vine |
— | jj-vine |
| nix2container | nix2container-bin, skopeo-nix2container |
nix2container-bin, skopeo-nix2container |
— |
Each name is the root packages.<system>.<name>: nine legs, one attestation
each. The expected outPaths, evaluated at the revs consumers lock on
2026-10-19, are T5’s acceptance fixture:
| Fork rev | Root | outPath |
|---|---|---|
woodpecker dfc97d50662e08a37798ff80d77434c761065694 |
aarch64-linux.woodpecker-server |
/nix/store/advdr1xdz9g795f11r2gjs0h442jpq82-woodpecker-server-3.17.0-rigel.14 |
aarch64-linux.woodpecker-agent |
/nix/store/cjfprja0a1yjhhnkmisvqsj1rqn9ihf5-woodpecker-agent-3.17.0-rigel.14 |
|
x86_64-linux.woodpecker-agent |
/nix/store/aij1xq4s5jq1c93rcd861khd4rgl3ckz-woodpecker-agent-3.17.0-rigel.14 |
|
aarch64-darwin.woodpecker-agent |
/nix/store/3w33nnmxx9dccli9nz1cg0i68pnpvjp2-woodpecker-agent-3.17.0-rigel.14 |
|
oh-my-pi 4e506a0cf0639874ca75464f70c26e58acdeb237 |
x86_64-linux.omp |
/nix/store/nvwzlbncf4rnas0vbd5v598r6h6vqs7n-omp-18.9.2-rigel.1 |
aarch64-darwin.omp |
/nix/store/qnvb58k2zzd282a8afrygp1ldm5j1s7w-omp-18.9.2-rigel.1 |
|
jj-vine 6a8b21b5ddb0f84e4a83a9993b9d3446fb500456 |
x86_64-linux.jj-vine |
/nix/store/8m6jdb2qh14g280f7rrjwmmr87a6hq2z-jj-vine-0.5.4-rigel.1 |
aarch64-darwin.jj-vine |
/nix/store/xmhb78w1m5wzgyfdl4m639rvr4jspkw5-jj-vine-0.5.4-rigel.1 |
|
nix2container 7663114b3c271f6119478fb7cfe79c42347d29ae |
x86_64-linux.nix2container-bin |
/nix/store/f1blj11ghlkf32mczynhbl21jamcxs77-nix2container-1.0.0 |
x86_64-linux.skopeo-nix2container |
/nix/store/8pq413af13q376xkjib2kkf1vl26zgmd-skopeo-1.21.0 |
|
aarch64-linux.nix2container-bin |
/nix/store/xp3qffdck2gz17c91q3i098dyr280xbw-nix2container-1.0.0 |
|
aarch64-linux.skopeo-nix2container |
/nix/store/rjjr630yy6xvqis8lay3p2jwry82bm7s-skopeo-1.21.0 |
Build and capture
Section titled “Build and capture”- Source.
github:<repo>/<sha>, the fetcher type consumers lock. A checkout can change the source path andsourceInfo, and so the outPaths. - Nix settings. The installer’s
extra_nix_configsetsaccept-flake-config = falseand the substituters of Open Question 4, and the leg asserts them withnix config show. No third-party cache’s bytes are republished. - Build.
nix build --no-link --json, after checkingbuiltins.currentSystem. A root’soutPathis itsoutoutput. - Closure.
nix path-info --recursive --json --json-format 2over the roots (Nix 2.33 and later). The producer joins basenames withstoreDir, converts SRI tosha256:<nix32>, and keeps self-references. The result is exactly the paths the signer walks from the roots. - NAR.
nix-store --dumpis read in 64 MiB parts. Each part is held in memory until the signer accepts it, so a leg holds at most 128 MiB.
Signer calls
Section titled “Signer calls”Only POST /v1/attestations exists today. Everything else below is
proposed, owned by the signer (RIG-5407), which publishes the contract:
schemas, statuses, idempotency, token checks, limits, deadlines and
acceptance tests.
Each call carries a fresh OIDC token. Before the first call, the leg decodes
its token and checks repository, sha, workflow_ref, job_workflow_ref,
event_name and ref against its release context, naming any mismatch.
| Step | Call | Notes |
|---|---|---|
| 1 | POST /v1/attestations |
The attestation with its local digest. The returned digest must equal it. |
| 2 | PUT /v1/staging/<digest>/nar/<nix32>.nar/parts/<n> |
From n = 0, each part exactly 64 MiB except a shorter last one. A NAR of 64 MiB or less is part 0 alone. |
| 3 | POST /v1/staging/<digest>/nar/<nix32>.nar/complete |
Body {"parts": N}. The signer checks every part, the total size, and narHash over the whole NAR. |
| 4 | POST /v1/staging/<digest>/complete |
Empty body. The signer writes the narinfos. |
| Answer | Action |
|---|---|
| 200, 201 | Success. |
| 429, any 5xx, or a transport error | Retry in process: wait Retry-After if given, else back off from 1 s, doubling to 60 s. |
| Any other 4xx | Fail the leg. |
A part re-PUT answers 200 for the same bytes and 409 for different ones. A
NAR complete with a part missing answers 400. A retry resends the same
part, so it is safer than a rerun. Retries stop after 30 minutes per call and
90 per leg. A part’s deadline is 2 minutes plus 1 second per MiB, plus 60
seconds. A leg has two NARs in flight, one part each, because legs share the
signer’s four body slots. Also assumed: the part route checks the token at
the headers, and a NAR complete answers within 100 seconds.
Recovery is “Re-run failed jobs”, within 30 days; never rerun a completed leg. A release with no attestation posts no status on the consumer’s bump PR, so the signer owner is asked to post a failure status in that case.
Canonical digest
Section titled “Canonical digest”The digest is the lowercase hex SHA-256 of one UTF-8 JSON text. The interface between producer and signer is the golden vectors.
- Normalize. Each
narHashbecomessha256:<nix32>, with the alphabet0123456789abcdfghijklmnpqrsvwxyz. A missingreferencesbecomes[]. Sort eachreferenceslist, theclosurebystorePath, and therootsbyattr, all by UTF-16 code units (JavaScript’s default string order). - Exclude
digest. - Key order. Top level:
closure, event, observedTip, ref, releaseRef, releaseSha, repo, roots, system, tag, tagTarget, workflow. Closure entry:narHash, narSize, references, storePath. Root:attr, outPath. - Encode with
JSON.stringifyand no whitespace."and\are escaped,\b \f \n \r \tuse short forms, other characters below U+0020 become lowercase\u00xx, and everything else is raw, including<,>,&, U+2028 and U+2029. Invalid UTF-8 becomes U+FFFD.
| Vector | Digest |
|---|---|
| main | 56e8daaddc90e26fc78ff78eed931d21efee4be787beaea5ff395d328ce76a61 |
| empty | 8224f1853e23597c289636641f6c51cb08893e128ae7e79013ab1272aac6b584 |
| unicode and control escapes | 8e8c175aa796d4a5d28334e6383db9c34e5dcee6f1e783731910c797604a37a3 |
| UTF-16 order | bfeff84fa061297c07306505a701bddc0d0a159ca178e287a31deef3939292f9 |
| fork (woodpecker aarch64-linux) | pending |
The four listed digests reproduce, with these rules, from the signer’s tests
today. They live in test/vectors/attestation-digest.json. The fork vector is
pending: it comes from the signer-side fixture test (D2), and T5 copies it
once that test is on the signer’s main. It tests the encoding, not the
binding. To change the encoding, the signer owner posts new vectors, the
producer releases, and forks move pins.
Code and runtime
Section titled “Code and runtime”Bun TypeScript with no runtime dependencies, run from source, as three
composite actions: resolve, stage and check. Bun is the house script
language, oh-my-pi runs it, and JSON.stringify gives the canonical escaping.
Go would need a hand-written encoder, and the signer’s is not public.
Each fork’s check job runs the check action on every PR to
rigel-release. It recomputes the vectors and checks the job shape. This
catches mistakes; the fork-owned pin step is the trust control. Each stage
leg also recomputes the vectors before its first call.
The producer’s bun test covers each module, with the signer client against
a Bun.serve fake. The dry run builds, captures and digests, checks every NAR
against path-info, fails if a root has an output besides out, and never
mints a token. A workflow_dispatch workflow runs it on the real runners.
Rollout
Section titled “Rollout”Fork PRs open now as drafts on rigel-release and stay draft until:
- the signer’s staging contract, part routes included, is deployed (RIG-5407), with its host and audience posted;
- the policy file lists the fork (RIG-5274);
- the signer’s capacity proof has passed;
- for jj-vine and nix2container, the consuming flake uses the fork’s outputs.
Merging is the enable step. The PR also bumps the version, so its merge cuts
the first staged release. Earlier releases, such as woodpecker’s
v3.17.0-rigel.14, are never staged: a dispatch token fails the push
claim.
Order: jj-vine (small, Linux and macOS), nix2container, woodpecker, oh-my-pi (the largest NARs).
Rejected
Section titled “Rejected”- Tag-push trigger. Wrong
event_nameandref. - A separate producer workflow. The trust policy names
release-tag.yaml. - Mint scripts writing outputs. Four changes, and “already exists” still skips the target.
- A pin check inside the action. Pinned code would vouch for itself.
Global Constraints
Section titled “Global Constraints”RigelBuild/nix-cache-producer, branchmain: strict TypeScript, noany, no runtime dependencies.- Nix:
cachix/install-nix-action@448cde739abec1eaf0646249e3d25bb8d9bc7702 # v31.11.2(Nix 2.35.3; 2.33+ required). Bun:oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0,bun-version: 1.4.2. - Every
uses:inrelease-tag.yamlis a 40-hex SHA with a# vX.Y.Ztrailer, as the woodpecker and jj-vine contract tests require. - Permissions,
environmentand runners as in § Where it runs. - Signer URL, audience, cache URL and cache key are inputs with no defaults.
- Public repos name no private path, source or PR.
T1 creates the repo. T2–T4 are independent modules. T5 wires the actions,
runs the dry runs and cuts v0.1.0. T6–T9 follow T5. Tests come first.
T1. Producer repo
Section titled “T1. Producer repo”One IaC PR, owned by the fork lane: the public repo with Actions on, a main
ruleset requiring review and the test check, a refs/tags/v* tag ruleset,
and the release minter on main. The IaC test pinning the App key’s holders
goes from eight to nine. After apply, push the README-only root commit.
T2. Canonical digest
Section titled “T2. Canonical digest”Files: package.json, tsconfig.json, .github/workflows/test.yaml (job
test), src/attestation.ts, src/nix32.ts, src/digest.ts,
test/vectors/attestation-digest.json, test/digest.test.ts,
test/nix32.test.ts.
Interfaces:
export interface RootClaim { attr: string; outPath: string }export interface PathInfo { storePath: string; narHash: string; // sha256:<nix32> once normalized narSize: number; references: string[]; // full store paths}export interface Attestation { repo: string; tag: string; releaseRef: string; observedTip: string; tagTarget: string; releaseSha: string; workflow: string; event: string; ref: string; system: string; roots: RootClaim[]; closure: PathInfo[]; digest?: string;}
// src/nix32.tsexport function toNix32(bytes: Uint8Array): string;// Accepts SRI, sha256:<hex> and sha256:<nix32>; throws on anything else.export function normalizeNarHash(narHash: string): string;
// src/digest.tsexport function canonicalJson(attestation: Attestation): string;export function canonicalDigest(attestation: Attestation): string; // lowercase hexexport function checkVectors(): void; // throws, naming the first failing vector{ "copiedFrom": "the signer's digest tests, 2026-10-18", "vectors": [{ "name": "main", "attestation": {}, "digest": "56e8…6a61" }]}Tests: the four signer vectors, and the fork vector once copied (T5); SRI,
hex and nix32 spellings normalize alike; input order and a present digest
do not change the digest.
T3. Build and capture
Section titled “T3. Build and capture”Files: src/nix.ts, test/nix.test.ts, test/fixtures/path-info-v2.json,
test/fixtures/flake/flake.nix.
Interfaces:
export async function currentSystem(): Promise<string>;// Fails unless `nix config show` has the expected settings.export async function checkNixConfig(substituters: string[]): Promise<void>;// nix build --no-link --json; each attr with its out output.export async function buildRoots(flakeRef: string, attrs: string[]): Promise<RootClaim[]>;// Every output name of each attr, for the dry run's out-only check.export async function rootOutputs(flakeRef: string, attrs: string[]): Promise<Record<string, string[]>>;// Throws unless version is 2; joins basenames with storeDir; normalizes narHash.export function parsePathInfoV2(json: unknown): PathInfo[];export async function closureOf(outPaths: string[]): Promise<PathInfo[]>;export function narKey(info: PathInfo): string; // "nar/<nix32>.nar"// nix-store --dump; done rejects on a non-zero exit.export interface NarStream { body: ReadableStream<Uint8Array>; done: Promise<void> }export function dumpNar(storePath: string): NarStream;Tests: parsePathInfoV2 on a recorded fixture keeps self-references, joins
storeDir, converts SRI, and rejects version 1. In CI, a two-derivation
fixture flake with no nixpkgs is built and captured, and each NAR’s SHA-256 and
size match path-info.
T4. Signer client
Section titled “T4. Signer client”Files: src/oidc.ts, src/signer.ts, test/fake-signer.ts,
test/signer.test.ts.
Interfaces:
// src/oidc.ts: one token per call from ACTIONS_ID_TOKEN_REQUEST_URL and _TOKEN.export async function mintToken(audience: string): Promise<string>;// The payload's string claims; the signer checks the signature.export function tokenClaims(token: string): Record<string, string>;
// src/signer.tsexport interface SignerConfig { url: string; audience: string; token: (audience: string) => Promise<string>; perCallMs: number; // 30 min perLegMs: number; // 90 min, shared by every call in the leg}// Retry-After when given, else 1 s doubling to 60 s.export function backoffMs(attempt: number, retryAfter: string | null): number;export const PART_BYTES: number; // 64 MiB// 120 s + 1 s per MiB of the part + 60 s.export function partDeadlineMs(partBytes: number): number;// Returns the signer's digest; throws if it differs from attestation.digest.export async function postAttestation(cfg: SignerConfig, attestation: Attestation): Promise<string>;// Reads one dump in PART_BYTES parts and PUTs each, retrying a part under// the status policy with the part held in memory, then POSTs the NAR's// complete. Fails if done rejects or the bytes read differ from narSize.export async function putNar(cfg: SignerConfig, digest: string, info: PathInfo, open: () => NarStream): Promise<void>;// Distinct keys, `concurrency` (default 2) in flight; the first failure aborts the rest.export async function uploadAll(cfg: SignerConfig, digest: string, closure: PathInfo[], open: (info: PathInfo) => NarStream, concurrency?: number): Promise<void>;export async function complete(cfg: SignerConfig, digest: string): Promise<void>;Each request sets AbortSignal.timeout and timeout: false, because Bun’s
timeout is an idle timer, not a deadline. When the signer’s contract lands,
the fake is updated to it before T6.
Tests, against a Bun.serve fake: 201, then 200 on a repeat; 503 with
Retry-After: 1, then 201; 500 and a dropped connection, then 201; 400 and
404 fail with no retry; the per-call and per-leg budgets end retries; a
three-part NAR with a short last part; a NAR under 64 MiB as part 0; one
part retried alone; 409 on conflicting part bytes; a NAR complete failing
on a missing part; at most two NARs in flight; a new token per request;
Content-Length equals the part size; the deadline at 0, 1 MiB and 64 MiB.
T5. Actions, dry run and first release
Section titled “T5. Actions, dry run and first release”Files: src/release.ts, src/workflow-check.ts, src/main.ts, their tests,
resolve/action.yml, stage/action.yml, check/action.yml,
.github/workflows/dry-run.yaml, .github/workflows/release-tag.yaml,
scripts/release-tag.ts, VERSION.
Interfaces:
export interface ReleaseContext { repo: string; sha: string; tag: string; workflow: string; event: string; ref: string;}// Throws unless event is push, ref is refs/heads/rigel-release, and workflow is// <repo>/.github/workflows/release-tag.yaml@refs/heads/rigel-release.export function releaseContext(env: Record<string, string | undefined>, tag: string): ReleaseContext;// Throws naming the first of repository, sha, workflow_ref, job_workflow_ref,// event_name, ref that differs from ctx.export function checkClaims(claims: Record<string, string>, ctx: ReleaseContext): void;// observedTip, tagTarget and releaseSha are ctx.sha; releaseRef is ctx.ref; sets digest.export function buildAttestation(ctx: ReleaseContext, system: string, roots: RootClaim[], closure: PathInfo[]): Attestation;// git ls-remote --tags output; one vX.Y.Z-rigel.N tag at sha, or null; two throw.export function tagAt(lsRemote: string, sha: string): string | null;
// src/workflow-check.ts: one message per broken rule; empty means pass.export function checkReleaseWorkflow(yaml: string): string[];bun src/main.ts resolvebun src/main.ts stage --system S --roots 'A B' --tag T --signer-url U --audience A \ --cache-url C --cache-key Kbun src/main.ts stage --dry-run --repo R --sha SHA --system S --roots 'A B'bun src/main.ts check --workflow .github/workflows/release-tag.yamlresolve writes tag= to GITHUB_OUTPUT. The stage action installs Bun
and Nix, then runs one leg. dry-run.yaml writes the digest, root outPaths
and largest NARs to the job summary. release-tag.yaml mints v<VERSION> on
a push to main that changes VERSION.
A fork’s new jobs (woodpecker’s aarch64-linux leg shown):
resolve: needs: mint if: github.event_name == 'push' runs-on: ubuntu-24.04 permissions: contents: read outputs: tag: ${{ steps.resolve.outputs.tag }} steps: - name: Require producer pin on main env: GH_TOKEN: ${{ github.token }} run: | gh api "repos/$GITHUB_REPOSITORY/contents/.github/workflows/release-tag.yaml?ref=$GITHUB_SHA" \ -H 'Accept: application/vnd.github.raw' > release-tag.yaml pin="$(grep -oE 'nix-cache-producer/[a-z]+@[0-9a-f]{40}' release-tag.yaml | cut -d@ -f2 | sort -u)" [ -n "$pin" ] && [ "$(printf '%s\n' "$pin" | wc -l)" -eq 1 ] || { echo '::error::expected one producer SHA' >&2; exit 1; } status="$(gh api "repos/RigelBuild/nix-cache-producer/compare/main...$pin" --jq .status)" case "$status" in identical|behind) ;; *) echo "::error::producer pin $pin is not on main ($status)" >&2; exit 1 ;; esac - id: resolve uses: RigelBuild/nix-cache-producer/resolve@<T5 sha> # v0.1.0
stage: needs: resolve if: needs.resolve.outputs.tag != '' runs-on: ${{ matrix.runner }} timeout-minutes: 180 permissions: contents: read id-token: write strategy: fail-fast: false matrix: include: - system: aarch64-linux runner: ubuntu-24.04-arm roots: packages.aarch64-linux.woodpecker-server packages.aarch64-linux.woodpecker-agent steps: - uses: RigelBuild/nix-cache-producer/stage@<T5 sha> # v0.1.0 with: system: ${{ matrix.system }} roots: ${{ matrix.roots }} tag: ${{ needs.resolve.outputs.tag }} signer-url: <posted staging host> audience: <posted audience> cache-url: <Rigel public cache URL> cache-key: <Rigel public cache key>Done when:
- the signer-side fork fixture test is on the signer’s
main, and the fork vector is copied from there; bun testpasses in the producer’s CI;- the nine dry runs pass at the fixture revs in § Where it runs, and every root outPath equals the fixture;
- each fork’s largest NAR is posted on RIG-5698;
v0.1.0is minted.
T6–T9. Fork PRs
Section titled “T6–T9. Fork PRs”One draft PR per fork on rigel-release: T6 jj-vine, T7 nix2container, T8
woodpecker, T9 oh-my-pi. Each PR adds resolve and stage with the fork’s
rows, and adds the check action to the check job. It keeps the fork’s
script tests green. In oh-my-pi, it also adds release-tag.yaml to the
pull_request paths, so check runs on a workflow change, and pins the two
actions/checkout@v4 steps. Done when the fork’s CI passes and the draft
links RIG-5698.
- Post the two signer assumptions, the missing-attestation status ask, and Open Questions 2 and 3 to the signer owner on RIG-5407.
- Signer owner: publish the staging contract and its acceptance tests; align T4’s fake with it.
- File Open Questions 1 and 4 for Matt.
- T1: IaC PR, apply, root commit.
- T2: vector tests first; see them fail; then nix32 and the digest.
- T3: fixture tests first; then
src/nix.ts. - T4: fake-signer tests first; then the client.
- T5: CLI, actions and workflows; fork vector; nine dry runs against the
outPath fixture; largest NARs on RIG-5698; mint
v0.1.0. - T6–T9: four draft PRs pinned to
v0.1.0. - Per fork, once § Rollout holds: set the posted values, bump the version, promote and merge. The release’s legs pass, and its consumer bump PR passes the signer gate.