Skip to content

feat(release): stable release security gate — protected publication chain (F16–F18, F31) - #2585

Merged
namastex888 merged 5 commits into
devfrom
wish/stable-release-security-gate
Jul 14, 2026
Merged

namastex888 merged 5 commits into
devfrom
wish/stable-release-security-gate

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

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

  • F16 — all four dispatchable entry points guarded (release.yml, build-tarballs.yml, sign-attest.yml, release-publish.yml): manual dispatch requires a real v* tag ref; release-publish.yml's standalone channel default flipped stable→dev. Guard jobs are load-bearing (needs: on every downstream job).
  • F17 — provenance binding as tested helpers: scripts/release-guard.sh binds break-glass run_id to the expected repo/workflow/status/conclusion/ref/SHA + version grammar; 18 fixtures incl. injection negatives (scripts/release-guard.test.ts). Inputs enter via env:, never interpolated.
  • F18 — pin/freeze/least-privilege: 5 third-party actions SHA-pinned (verified upstream); frozen installs in ci.yml/version.yml; permissions: contents: read on ci.yml; secrets: inherit removed from release.yml. Documented exception: SLSA generator stays @v2.1.0 (slsa-verifier derives builder identity from the semver tag; SHA-pinning breaks it).
  • F31 — transactional binary swap: stage → verify → atomic rename → rollback in install.sh + corrupt-tarball fixture in update.ts; .steal lock protocol byte-identical (digest c6d5c4bd… pinned as a regression test); 7 destructive-failure fixtures. scripts/verify-release.sh realigned to the real asset scheme (*.tar.gz + .bundle + per-tarball .intoto.jsonl) — it previously could not verify any actual release.
  • Channel-scoped approval gate: environment: ${{ inputs.channel == 'stable' && 'production' || '' }} — stable waits for the production environment's independent required reviewer (configured + evidenced in qa/github-settings-evidence-20260714.json); dev/homolog attach no environment and keep publishing untouched.

Review evidence

  • Wish plan review: FIX-FIRST → all 6 gaps fixed → SHIP (2026-07-14).
  • Adversarial execution review: SHIP — guard wiring, dev-flow trace, .steal digest recomputation, SHA-pin upstream verification, fingerprint-witness contract, gates re-run. 3 advisory non-blocking follow-ups recorded in WISH.md (MEDIUM: break-glass head_branch API-shape unproven, fails closed; LOW: sidecar rollback; LOW: first-party actions tag-pinned).
  • Full record: .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

namastex888 and others added 4 commits July 14, 2026 13:27
…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>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9afc24c9-1527-4f2d-995e-09bf4081dec2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wish/stable-release-security-gate

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.

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread scripts/release-guard.sh Outdated
Comment on lines +131 to +138
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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.

Suggested change
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"

Comment thread scripts/verify-release.sh
Comment on lines 128 to 136
(
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; }
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
(
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; }

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread install.sh
Comment on lines +526 to +527
rm -rf "${bin:?}/${base}"
mv "$entry" "${bin}/${base}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment thread scripts/release-guard.sh Outdated
Comment on lines +127 to +130
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@namastex888

Copy link
Copy Markdown
Contributor Author

Bot-comment triage (verified against code)

Finding Verdict Action
Codex P1 — caller lacks actions: read for release-publish's guard job Confirmed, merge-blocking. A called workflow cannot elevate past its caller, so every orchestrated release (dev included) would have failed at workflow init. Not catchable by PR CI (release workflows don't run on PRs). Fixed in 7a4d05a5 — actions: read granted on the caller
Codex P2 — run_id grammar checked after gh presence probe Confirmed, minor (fails closed either way: 64 vs 3). Fixed in 7a4d05a5 — input validation now precedes environment probing
Gemini HIGH — mktemp leak on early fail Real but severity inflated: one temp file on an ephemeral runner / rare break-glass run. Fixed in 7a4d05a5 via EXIT trap (${tmp:-} — the trap outlives the function local under set -u)
Codex P2 — sidecars promoted before binary commit Valid; identical to the LOW advisory already recorded by the independent execution review in WISH.md. Follow-up (documented in-code + wish)
Gemini MEDIUM — exit inside subshell Rejected: set -euo pipefail propagates the subshell's exit 5 to the script exit code, and the exit-code contract is covered by verify-release.test.ts. Stylistic only. No change

🤖 Generated with Claude Code

@namastex888
namastex888 merged commit ef6ee87 into dev Jul 14, 2026
17 checks passed
@automagik-genie
automagik-genie deleted the wish/stable-release-security-gate branch September 25, 2026 04:52
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