feat(release): stable release security gate — protected publication chain (F16–F18, F31) - #2585
Conversation
…es + least-privilege CI Close inherited findings F16/F17/F18 and the channel-scoped environment deliverable of the stable-release-security-gate wish, entirely in repo code. F16/F17 — every dispatchable entry point (release.yml, build-tarballs.yml, sign-attest.yml, release-publish.yml) now gates on scripts/release-guard.sh: a manual workflow_dispatch must target a protected v<version> tag, and a break-glass run_id is bound to a SUCCESSFUL upstream run on the same repo, workflow, tag ref, and head SHA before anything is signed or published. The guard is extracted into a shell helper with colocated bun:test fixtures (scripts/release-guard.test.ts, 18 cases) because CI has no workflow simulator. HARD INVARIANT preserved: the dev release path dispatches release.yml on the freshly-pushed tag, so it always satisfies the tag guard; orchestrated workflow_call sub-runs pass no run_id and inherit the tag ref. Deliverable 6 — release-publish.yml's publish job declares `environment: production` ONLY when channel == 'stable'; dev/homolog resolve to an empty environment string, so dev keeps publishing with no approval gate. release-publish.yml's standalone dispatch channel default flips from stable to dev (defensive). F18 — SHA-pin all third-party actions (setup-bun, cosign-installer, slsa-verifier installer, ggshield-action, attest-build-provenance); freeze the last unfrozen `bun install`s in ci.yml + version.yml; add a least-privilege top-level `permissions: contents: read` to ci.yml; and drop blanket `secrets: inherit` from release.yml (called workflows use only the auto-provided GITHUB_TOKEN). The SLSA generator reusable stays pinned to its `@v2.1.0` semver tag with a documented exception — slsa-verifier derives the trusted builder identity from that tag and the build fails if it is pinned to a commit SHA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gression guard Close F31a in repo code. install.sh's extract_and_link no longer untars directly over the live tree. It extracts to a same-filesystem staging dir, verifies the staged binary runs and reports the expected version (rejecting a corrupt or wrong-version artifact), moves sidecars into place, backs the live binary up to bin/.previous, then commits with a single rename-over-live of the `genie` binary — the old executable is runnable up to that instant, the new one immediately after, and a crash mid-rename can never yield a partial file. A failed post-swap verification rolls back to the backup. The durable .steal lifecycle-lock recovery protocol (F42/F45–F47/F50) is left byte-for-byte unchanged; scripts/install-swap.test.ts pins the SHA-256 of the seven protected functions as a regression guard and exercises the swap against destructive-failure fixtures: happy path, first install, corrupt artifact (no binary), corrupt tarball, version mismatch, kill mid-swap (old binary stays runnable), and failed provenance (download_and_verify refuses). update.ts's atomic swap is already transactional; a corrupt-tarball fixture is added to extractTarball to complete its destructive-failure coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Close F31b. Releases ship per-platform `genie-<v>-<platform>.tar.gz` plus a
cosign sigstore `*.tar.gz.bundle` and a per-tarball SLSA `*.tar.gz.intoto.jsonl`
(sign-attest.yml / release-publish.yml). The verifier previously expected the
non-existent `*.tgz` + detached `.sig`/`.cert` + a single
`provenance.intoto.jsonl`, so it could not verify any real release.
It now downloads `*.tar.gz{,.bundle,.intoto.jsonl}`, verifies each tarball with
`cosign verify-blob --bundle` (identity + issuer pinned, matching install.sh)
and `slsa-verifier verify-artifact` against the per-tarball provenance, and adds
a best-effort GitHub-native `gh attestation verify` cross-check. The canonical
signing-identity pin block (witnessed by check-fingerprint-pinning.sh) is
preserved verbatim. scripts/verify-release.test.ts covers the exit-code
contract (verified / cosign-fail / slsa-fail / missing-material / misuse).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, G2 settings evidence Plan review FIX-FIRST→SHIP and adversarial execution review SHIP recorded in WISH.md; Group 2 external state (production environment with independent required reviewers + prevent-self-review + no admin bypass, v-tags-immutable ruleset) captured as qa/github-settings-evidence-20260714.json; INDEX entry moved to Poured. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements the stable-release-security-gate plan to harden the stable release pipeline. Key changes include adding transactional binary promotion and rollback mechanisms to install.sh, introducing scripts/release-guard.sh to validate dispatch tags and run provenance, and updating scripts/verify-release.sh to support the new release asset verification scheme. Comprehensive unit tests and fixtures have also been added to verify these behaviors. The review feedback highlights two issues: a potential temporary file leak in scripts/release-guard.sh if the script exits early, and an ineffective exit call inside a subshell in scripts/verify-release.sh that should be handled in the parent context.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| local tmp | ||
| tmp="$(mktemp)" | ||
| if ! gh api "repos/${expected_repo}/actions/runs/${run_id}" >"$tmp" 2>/dev/null; then | ||
| rm -f "$tmp" | ||
| fail "could not fetch upstream run ${run_id} from ${expected_repo} (bad run_id or insufficient token scope)" | ||
| fi | ||
| check_run_provenance "$tmp" | ||
| rm -f "$tmp" |
There was a problem hiding this comment.
The temporary file created by mktemp is not cleaned up if the script exits early due to set -e or an explicit call to fail (which exits with status 3). To prevent leaking temporary files in the system's temp directory, use a trap to ensure cleanup on exit.
| local tmp | |
| tmp="$(mktemp)" | |
| if ! gh api "repos/${expected_repo}/actions/runs/${run_id}" >"$tmp" 2>/dev/null; then | |
| rm -f "$tmp" | |
| fail "could not fetch upstream run ${run_id} from ${expected_repo} (bad run_id or insufficient token scope)" | |
| fi | |
| check_run_provenance "$tmp" | |
| rm -f "$tmp" | |
| local tmp | |
| tmp="$(mktemp)" | |
| trap 'rm -f "$tmp"' EXIT | |
| if ! gh api "repos/${expected_repo}/actions/runs/${run_id}" >"$tmp" 2>/dev/null; then | |
| fail "could not fetch upstream run ${run_id} from ${expected_repo} (bad run_id or insufficient token scope)" | |
| fi | |
| check_run_provenance "$tmp" |
| ( | ||
| cd "${workdir}" | ||
| gh release download "${tag}" \ | ||
| --repo "${REPO}" \ | ||
| --pattern '*.tgz' \ | ||
| --pattern '*.sig' \ | ||
| --pattern '*.cert' \ | ||
| --pattern 'provenance.intoto.jsonl' \ | ||
| --pattern '*.tar.gz' \ | ||
| --pattern '*.tar.gz.bundle' \ | ||
| --pattern '*.tar.gz.intoto.jsonl' \ | ||
| || { echo "error: release assets missing — exit 5" >&2; exit 5; } | ||
| ) |
There was a problem hiding this comment.
The exit command inside the subshell (...) only terminates the subshell itself, not the parent script. While set -e is active and will cause the parent script to exit when the subshell fails, relying on set -e for explicit error handling and exit codes can be fragile. It is more robust to handle the subshell's failure explicitly in the parent context.
| ( | |
| cd "${workdir}" | |
| gh release download "${tag}" \ | |
| --repo "${REPO}" \ | |
| --pattern '*.tgz' \ | |
| --pattern '*.sig' \ | |
| --pattern '*.cert' \ | |
| --pattern 'provenance.intoto.jsonl' \ | |
| --pattern '*.tar.gz' \ | |
| --pattern '*.tar.gz.bundle' \ | |
| --pattern '*.tar.gz.intoto.jsonl' \ | |
| || { echo "error: release assets missing — exit 5" >&2; exit 5; } | |
| ) | |
| ( | |
| cd "${workdir}" | |
| gh release download "${tag}" \ | |
| --repo "${REPO}" \ | |
| --pattern '*.tar.gz' \ | |
| --pattern '*.tar.gz.bundle' \ | |
| --pattern '*.tar.gz.intoto.jsonl' | |
| ) || { echo "error: release assets missing — exit 5" >&2; exit 5; } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3a42fab63
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: read | ||
| actions: read # gh api reads the upstream sign-attest run record |
There was a problem hiding this comment.
Grant actions:read to the reusable publish caller
In the release.yml → release-publish.yml orchestrated path, the caller job still grants only contents: write/id-token: write, but this new guard job asks the called workflow for actions: read. GitHub documents that reusable-workflow GITHUB_TOKEN permissions can only be downgraded, not elevated (https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations), so tag/dispatch releases through release.yml can fail workflow initialization before publishing; add actions: read to the caller's publish permissions or avoid requesting it on the workflow_call path.
Useful? React with 👍 / 👎.
| rm -rf "${bin:?}/${base}" | ||
| mv "$entry" "${bin}/${base}" |
There was a problem hiding this comment.
Preserve sidecars until the binary commit succeeds
If install/update is interrupted after these sidecars are removed/moved but before the later mv of genie, the live binary remains the old version while VERSION, plugins, skills, and marketplaces have already been replaced (and the old sidecars were deleted). The injected before-promote failure path exercises exactly this window, so rollback leaves a mixed install rather than the previous release; stage or back up sidecars and commit them only after the binary promotion succeeds.
Useful? React with 👍 / 👎.
| command -v gh >/dev/null 2>&1 || misuse "gh CLI is required for guard-run-provenance" | ||
| local expected_repo="${EXPECTED_REPO:-}" | ||
| [[ -n "$expected_repo" ]] || misuse "guard-run-provenance needs EXPECTED_REPO" | ||
| [[ "$run_id" =~ ^[0-9]+$ ]] || fail "run_id '${run_id}' is not a numeric run id" |
There was a problem hiding this comment.
Validate run_id before requiring gh
On developer or CI images that do not preinstall gh, malformed RUN_ID values exit 64 at the prerequisite check before reaching the numeric guard, so the new scripts/release-guard.test.ts fixture for RUN_ID=not-a-run fails instead of returning the intended fail-closed exit 3. Move the ^[0-9]+$ validation ahead of command -v gh so bad input is rejected without depending on the GitHub CLI being present.
Useful? React with 👍 / 👎.
Codex P1 on PR #2585: release-publish.yml's guard job requests actions:read but the orchestrated caller granted only contents:write/id-token:write — a called workflow cannot elevate past its caller, so every release.yml-driven release (dev included) would fail at workflow initialization. Grant the permission on the caller. Also: validate RUN_ID grammar before probing for gh (fail-closed exit 3 even without the CLI installed) and clean the provenance temp file via EXIT trap (${tmp:-} because the trap outlives the function local under set -u). Bot-comment triage recorded in WISH.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bot-comment triage (verified against code)
🤖 Generated with Claude Code |
Closes out the repo-code side (Group 1) of wish
stable-release-security-gate— the blocking disposition for inherited Ultra findings F16 (SEC1, CRITICAL), F17 (SEC2), F18 (SEC3), F31 (QA6) that gate stable promotion.What changed
release.yml,build-tarballs.yml,sign-attest.yml,release-publish.yml): manual dispatch requires a realv*tag ref;release-publish.yml's standalone channel default flippedstable→dev. Guard jobs are load-bearing (needs:on every downstream job).scripts/release-guard.shbinds break-glassrun_idto the expected repo/workflow/status/conclusion/ref/SHA + version grammar; 18 fixtures incl. injection negatives (scripts/release-guard.test.ts). Inputs enter viaenv:, never interpolated.ci.yml/version.yml;permissions: contents: readonci.yml;secrets: inheritremoved fromrelease.yml. Documented exception: SLSA generator stays@v2.1.0(slsa-verifier derives builder identity from the semver tag; SHA-pinning breaks it).install.sh+ corrupt-tarball fixture inupdate.ts;.steallock protocol byte-identical (digestc6d5c4bd…pinned as a regression test); 7 destructive-failure fixtures.scripts/verify-release.shrealigned to the real asset scheme (*.tar.gz+.bundle+ per-tarball.intoto.jsonl) — it previously could not verify any actual release.environment: ${{ inputs.channel == 'stable' && 'production' || '' }}— stable waits for theproductionenvironment's independent required reviewer (configured + evidenced inqa/github-settings-evidence-20260714.json); dev/homolog attach no environment and keep publishing untouched.Review evidence
.stealdigest recomputation, SHA-pin upstream verification, fingerprint-witness contract, gates re-run. 3 advisory non-blocking follow-ups recorded in WISH.md (MEDIUM: break-glasshead_branchAPI-shape unproven, fails closed; LOW: sidecar rollback; LOW: first-party actions tag-pinned)..genie/wishes/stable-release-security-gate/WISH.md.Invariant
Continuous dev releases are unaffected: guards pass automatically for the tag-dispatched orchestrated flow, and the approval environment attaches to the stable channel only.
🤖 Generated with Claude Code