From 6ff5ce5411f8ce0b7f8f413688b1a7be94da9886 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:22:59 +0000 Subject: [PATCH 1/8] ci(rust-ci): call cicd-suite's strict Rust gate and add the Creusot gate Replace the thin caller of hyperpolymath/standards' rust-ci-reusable.yml with a caller of hyperpolymath/cicd-suite's rust-ci.yml at cicd-suite main (9a947fa, cicd-suite#54). The job id and the check contexts `rust-ci / Cargo check + clippy + fmt` and `rust-ci / Cargo test` are unchanged; `rust-ci / Rust CI verdict` is the one context to require from now on, since it turns a skipped mandatory job into red instead of a silent pass. The strict gate found two real defects, both fixed here rather than weakened: * `cargo doc` with warnings denied: three doc comments in plasma-parser/src/zone/boundary.rs wrote `[project]`, `[[zone]]` and `[family]` as bare brackets, which rustdoc reads as broken intra-doc links. They are code spans now. * The declared `rust-version = "1.74"` was false: a dependency needs edition 2024, so the workspace does not build before 1.85 (verified: `cargo +1.78 check --locked` fails on `edition2024`, `cargo +1.85 check --locked` passes). The MSRV job builds the declared version, so the declaration now says 1.85. Also added: creusot-gate.yml (the Rust deductive-verification twin of the SPARK gate; a notice on this tree, which carries no Creusot obligations) and track-estate-audit.yml (weekly PR that moves every cicd-suite pin to cicd-suite HEAD, so a fix in the gate reaches this repository as a reviewable PR). Verified locally on rustc 1.97: fmt, clippy -D warnings, doctests, tests and `cargo doc --document-private-items` with RUSTDOCFLAGS=-D warnings all pass; MSRV 1.85 check passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/creusot-gate.yml | 33 ++++++++ .github/workflows/rust-ci.yml | 40 +++++++-- .github/workflows/track-estate-audit.yml | 103 +++++++++++++++++++++++ Cargo.toml | 2 +- plasma-parser/src/zone/boundary.rs | 6 +- 5 files changed, 173 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/creusot-gate.yml create mode 100644 .github/workflows/track-estate-audit.yml diff --git a/.github/workflows/creusot-gate.yml b/.github/workflows/creusot-gate.yml new file mode 100644 index 0000000..d07d7c5 --- /dev/null +++ b/.github/workflows/creusot-gate.yml @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MPL-2.0 +# Creusot Gate — thin caller of the estate's Rust deductive-verification gate +# in hyperpolymath/cicd-suite (the Rust twin of the SPARK gate). It detects +# Creusot obligations and claims, fails a "verified with Creusot" claim that +# has no contracts behind it, and — when obligations exist — installs Creusot +# at a pinned release, proves a planted false contract to prove the prover +# can say no, and runs `cargo creusot prove` in every crate with contracts. +# +# Require `creusot / Creusot verdict`. On a tree without obligations the +# verdict is a notice ("nothing was proved"), not assurance. +# +# Full-SHA pin, DE-ONBOARDED from actions.lock, no `secrets:` — see +# rust-ci.yml in this directory for why. +name: Creusot Gate + +on: + push: + branches: [main, master] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + creusot: + uses: hyperpolymath/cicd-suite/.github/workflows/creusot-gate.yml@9a947faad4f7f6640148980dd892b39f216fd091 # cicd-suite main + # Inputs (optional): + # creusot_version: "0.14.0" # paired with rust_nightly: "nightly-2026-08-03" + # enforce_hollow: true # a creusot-std dependency must annotate something diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 3f42a5d..03282e1 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,18 +1,44 @@ # SPDX-License-Identifier: MPL-2.0 -# Rust CI — thin wrapper calling the shared estate reusable in -# hyperpolymath/standards. Configure once, propagate everywhere. -# See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards. +# Rust CI — thin caller of the estate's strict Rust gate, which lives in +# hyperpolymath/cicd-suite as a reusable workflow. Do not copy its jobs in +# here: a copy freezes at one cicd-suite commit and never receives a fix. +# +# The job id MUST stay `rust-ci`: the contexts `rust-ci / Cargo check + +# clippy + fmt` and `rust-ci / Cargo test` are what branch rules already +# require, and `rust-ci / Rust CI verdict` is the one context to require +# from now on (red if any gate failed OR was skipped while expected). +# +# The pin is a full SHA, advanced by track-estate-audit.yml (a PR whose own +# run of this gate is the check). Never `@main` or a tag: the account +# enforces sha_pinning_required and a moving ref would change the gate in +# every repository at once. +# +# ⚠ DE-ONBOARDED from actions.lock, deliberately, like the estate-audit +# caller: an onboarded cross-repo reusable startup-fails with zero jobs +# under sha_pinning_required. cicd-suite/scripts/deonboard-workflow.sh +# reverses a re-onboarding. +# +# No `secrets:` line: the reusable consumes none. name: Rust CI + on: push: branches: [main, master] pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: actions: read contents: read -# Pin must be at or past the standards commit that passes `toolchain: stable` -# to the SHA-pinned dtolnay/rust-toolchain. d135b05b predates that and fails -# every run with "'toolchain' is a required input" (#105). + jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@95de3dfc3fcf09a1cf3325ab42723e3d81682b45 + uses: hyperpolymath/cicd-suite/.github/workflows/rust-ci.yml@9a947faad4f7f6640148980dd892b39f216fd091 # cicd-suite main + # Inputs (all optional; defaults are the strict ones): + # working_directory: crates/cli # sub-crate / monorepo + # toolchain: "1.85.0" # default "auto": rust-toolchain.toml, else stable + # enable_coverage: true # with coverage_floor: "70" + # zig_version: "0.15.2" # crates with a Zig-native build diff --git a/.github/workflows/track-estate-audit.yml b/.github/workflows/track-estate-audit.yml new file mode 100644 index 0000000..87bae80 --- /dev/null +++ b/.github/workflows/track-estate-audit.yml @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: MPL-2.0 +name: Track latest cicd-suite gates + +# Weekly: is any cicd-suite reusable this repository calls behind +# cicd-suite's main? If so, open a PR that moves every such pin — the estate +# audit, Rust CI, the Creusot, SPARK and miniKanren gates — to cicd-suite +# HEAD. The PR's own gate runs are the check: merge it when they are green. +# Nothing reaches main unreviewed. +# +# The file keeps its original name (track-estate-audit.yml) so repositories +# that already carry it receive this version in place. +# +# ⚠ NO `uses:` ANYWHERE, DELIBERATELY: actions.lock is keyed by workflow path, +# and this must run even when the lock is stale. +# +# Token: AUTOMATION_TOKEN, a fine-grained token on this repository with +# Contents, Pull requests and Workflows set to read/write. In practice it is +# required: the bump edits files under .github/workflows/, which +# GITHUB_TOKEN may not write, and a PR opened by GITHUB_TOKEN does not trigger +# the gate runs that validate it. + +on: + schedule: + - cron: "23 7 * * 1" # weekly, Monday + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: track-estate-audit + cancel-in-progress: false + +jobs: + bump: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN || secrets.GITHUB_TOKEN }} + BRANCH: ci/track-estate-audit + BOT_EMAIL: 41898583+github-actions[bot]@users.noreply.github.com + steps: + - name: Move every cicd-suite pin to HEAD and open a PR + run: | + set -euo pipefail + gh auth setup-git + base=$(gh repo view "$GITHUB_REPOSITORY" --json defaultBranchRef --jq .defaultBranchRef.name) + gh repo clone "$GITHUB_REPOSITORY" repo -- --quiet --depth 1 --branch "$base" + cd repo + + # Every caller of a cicd-suite reusable, with the SHA it pins. + mapfile -t callers < <(grep -lE 'uses:[[:space:]]*hyperpolymath/cicd-suite/\.github/workflows/[A-Za-z0-9_.-]+\.yml@[0-9a-f]{40}' .github/workflows/*.yml 2>/dev/null || true) + if [ "${#callers[@]}" -eq 0 ]; then + echo "::error::no workflow here calls a cicd-suite reusable at a full SHA." + exit 1 + fi + head=$(git ls-remote https://github.com/hyperpolymath/cicd-suite.git HEAD | cut -f1) + changed=0 + for caller in "${callers[@]}"; do + while IFS= read -r pinned; do + [ -n "$pinned" ] || continue + if [ "$pinned" = "$head" ]; then + echo "$caller: already at cicd-suite HEAD ${head:0:7}" + continue + fi + echo "$caller: ${pinned:0:7} -> ${head:0:7}" + sed -i "s/$pinned/$head/g" "$caller" + changed=1 + done < <(grep -oE 'cicd-suite/\.github/workflows/[A-Za-z0-9_.-]+\.yml@[0-9a-f]{40}' "$caller" | sed 's/.*@//' | sort -u) + done + if [ "$changed" -eq 0 ]; then + echo "Every cicd-suite pin is at HEAD." + exit 0 + fi + + # The bot branch is rebuilt from the default branch each run, which + # would discard anything else on it. If someone has pushed to it + # (say, a fix the newer gate needed), leave it for review. + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then + foreign=$(gh api "repos/$GITHUB_REPOSITORY/compare/$base...$BRANCH" \ + --jq "[.commits[] | select(.commit.author.email != \"$BOT_EMAIL\")] | length") + if [ "$foreign" != 0 ]; then + echo "::warning::$BRANCH carries $foreign commit(s) not made by this workflow; leaving it alone. Merge its PR or delete the branch, then re-run." + exit 0 + fi + fi + + git switch --quiet -c "$BRANCH" + git -c user.name='github-actions[bot]' -c user.email="$BOT_EMAIL" \ + commit --quiet -am "ci(cicd-suite): track cicd-suite ${head:0:7}" + if ! git push --quiet --force origin "$BRANCH"; then + echo "::error::Push refused: writing under .github/workflows/ needs the AUTOMATION_TOKEN secret described above." + exit 1 + fi + body="Moves every cicd-suite reusable this repository calls to cicd-suite \`${head:0:7}\`: https://github.com/hyperpolymath/cicd-suite/commit/$head. This PR's own gate runs are the check. Opened by \`track-estate-audit.yml\`." + if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then + gh pr edit "$BRANCH" --title "ci(cicd-suite): track cicd-suite ${head:0:7}" --body "$body" + else + gh pr create --base "$base" --head "$BRANCH" \ + --title "ci(cicd-suite): track cicd-suite ${head:0:7}" --body "$body" + fi diff --git a/Cargo.toml b/Cargo.toml index d95ee4f..892d7fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [".", "plasma-parser", "plasma-engine"] name = "palimpsest-plasma" version = "0.6.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.85" authors = ["Jonathan D.A. Jewell "] description = "Deterministic, typed policy engine — define machine-readable policies and check repositories against them" license = "MPL-2.0" diff --git a/plasma-parser/src/zone/boundary.rs b/plasma-parser/src/zone/boundary.rs index 48dfa71..69edf6c 100644 --- a/plasma-parser/src/zone/boundary.rs +++ b/plasma-parser/src/zone/boundary.rs @@ -36,7 +36,7 @@ struct PlasmaToml { family: Option, } -/// The [project] section of .plasma.toml. +/// The `[project]` section of .plasma.toml. #[derive(Debug, serde::Deserialize)] struct ProjectSection { #[allow(dead_code)] @@ -45,7 +45,7 @@ struct ProjectSection { default_license: Option, } -/// A [[zone]] entry in .plasma.toml. +/// A `[[zone]]` entry in .plasma.toml. #[derive(Debug, serde::Deserialize)] struct ZoneSection { name: String, @@ -56,7 +56,7 @@ struct ZoneSection { copyright: Option, } -/// The optional [family] section for palimpsest provision overrides. +/// The optional `[family]` section for palimpsest provision overrides. #[derive(Debug, serde::Deserialize)] #[allow(dead_code)] struct FamilySection { From 216d366ea818a340c076c4d1d3bbf9ec32431d0c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:25:05 +0000 Subject: [PATCH 2/8] ci: harden the cicd-suite pin tracker (timeout, egress-blocked runner) Takes cicd-suite's templates/track-estate-audit.yml verbatim: the `bump` job now has `timeout-minutes: 20` and a step-security/harden-runner step at a full SHA with egress limited to api.github.com and github.com. These are the three Hypatia alerts on this file (missing_timeout_minutes, WH006, RE001). This repository carries no actions.lock, so the new `uses:` needs no lock record here. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/track-estate-audit.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-estate-audit.yml b/.github/workflows/track-estate-audit.yml index 87bae80..3cdecdc 100644 --- a/.github/workflows/track-estate-audit.yml +++ b/.github/workflows/track-estate-audit.yml @@ -10,8 +10,13 @@ name: Track latest cicd-suite gates # The file keeps its original name (track-estate-audit.yml) so repositories # that already carry it receive this version in place. # -# ⚠ NO `uses:` ANYWHERE, DELIBERATELY: actions.lock is keyed by workflow path, -# and this must run even when the lock is stale. +# The only `uses:` here is step-security/harden-runner (hypatia RE001: a job +# that holds a token monitors its egress), pinned at a full SHA. Everything +# else is plain `run:`, so a stale cicd-suite pin can never stop the workflow +# that moves cicd-suite pins. A repository that carries +# .github/workflows/actions.lock must hold a record for this workflow +# (`gh actions-lock`), or its first run dies at Set up job with "invalid +# lockfile pin". # # Token: AUTOMATION_TOKEN, a fine-grained token on this repository with # Contents, Pull requests and Workflows set to read/write. In practice it is @@ -34,6 +39,9 @@ concurrency: jobs: bump: runs-on: ubuntu-latest + # A hung `gh` call must not hold a runner for the default six hours + # (hypatia WF013 / WH006). The whole job is a clone, a sed and a PR. + timeout-minutes: 20 permissions: contents: write pull-requests: write @@ -42,6 +50,15 @@ jobs: BRANCH: ci/track-estate-audit BOT_EMAIL: 41898583+github-actions[bot]@users.noreply.github.com steps: + - name: Harden the runner + uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 + with: + # The job clones, reads cicd-suite HEAD and pushes one branch: it + # talks to GitHub only, so block (RE002) rather than audit. + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 - name: Move every cicd-suite pin to HEAD and open a PR run: | set -euo pipefail From e2d5c6cbd48277a6e2dc592e1d0a1b0e06d943e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:39:28 +0000 Subject: [PATCH 3/8] ci(tracker): refuse an unresolved cicd-suite HEAD before rewriting pins Takes cicd-suite's template verbatim: the tracker exits 1 unless `git ls-remote` returned a full 40-hex SHA, so an empty lookup can no longer strip every pin (CodeRabbit on this PR). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/track-estate-audit.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/track-estate-audit.yml b/.github/workflows/track-estate-audit.yml index 3cdecdc..09e7464 100644 --- a/.github/workflows/track-estate-audit.yml +++ b/.github/workflows/track-estate-audit.yml @@ -74,6 +74,13 @@ jobs: exit 1 fi head=$(git ls-remote https://github.com/hyperpolymath/cicd-suite.git HEAD | cut -f1) + # A lookup that succeeds but returns no HEAD leaves $head empty, and + # the sed below would then strip every pin. Refuse anything that is + # not a full SHA. + if ! [[ "$head" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::could not resolve cicd-suite HEAD (got '${head}'); no pin was touched." + exit 1 + fi changed=0 for caller in "${callers[@]}"; do while IFS= read -r pinned; do From 9ebb374d804ae909b02a363a4e597eefe91d2986 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 21:43:28 +0000 Subject: [PATCH 4/8] ci(cicd-suite): re-pin rust-ci and creusot-gate callers to cicd-suite main bf7dd95 cicd-suite#57 (the allowed-owner / Alire-by-hash / theatre re-pin follow-up) merged as 202cb8d; main is now bf7dd95 and neither rust-ci.yml nor creusot-gate.yml changed after that merge. Both callers move from the pre-merge branch commit 9a947fa to the main commit so the pins resolve on cicd-suite's default branch, as track-estate-audit.yml expects. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/creusot-gate.yml | 2 +- .github/workflows/rust-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/creusot-gate.yml b/.github/workflows/creusot-gate.yml index d07d7c5..045c48b 100644 --- a/.github/workflows/creusot-gate.yml +++ b/.github/workflows/creusot-gate.yml @@ -27,7 +27,7 @@ permissions: jobs: creusot: - uses: hyperpolymath/cicd-suite/.github/workflows/creusot-gate.yml@9a947faad4f7f6640148980dd892b39f216fd091 # cicd-suite main + uses: hyperpolymath/cicd-suite/.github/workflows/creusot-gate.yml@bf7dd95cf49733019343ff485c2f45c6b261839c # cicd-suite main # Inputs (optional): # creusot_version: "0.14.0" # paired with rust_nightly: "nightly-2026-08-03" # enforce_hollow: true # a creusot-std dependency must annotate something diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 03282e1..7ee8ebc 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -36,7 +36,7 @@ permissions: jobs: rust-ci: - uses: hyperpolymath/cicd-suite/.github/workflows/rust-ci.yml@9a947faad4f7f6640148980dd892b39f216fd091 # cicd-suite main + uses: hyperpolymath/cicd-suite/.github/workflows/rust-ci.yml@bf7dd95cf49733019343ff485c2f45c6b261839c # cicd-suite main # Inputs (all optional; defaults are the strict ones): # working_directory: crates/cli # sub-crate / monorepo # toolchain: "1.85.0" # default "auto": rust-toolchain.toml, else stable From 8f3bd1c49356173becb02fb63f94414f6acec1e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:08:45 +0000 Subject: [PATCH 5/8] build(deps): bump anyhow 1.0.102 -> 1.0.104 (RUSTSEC-2026-0190) cicd-suite's cargo-deny job (advisories, bans, sources) failed on the re-pinned gate: RUSTSEC-2026-0190, an unsoundness in anyhow's Error::downcast_mut after Error::context, affects 1.0.102; the advisory names >=1.0.103 as the fix. `cargo update -p anyhow` moves the lock to 1.0.104; nothing else in the lock changes and the manifest's `anyhow = "1"` already admits it. `cargo check --locked --all-features` passes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ce6edc..9cc6c7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "bitflags" From 057fb40602f7c39a6d37a19449cfc43b59dd6a8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:11:17 +0000 Subject: [PATCH 6/8] ci(track-estate-audit): require AUTOMATION_TOKEN; no GITHUB_TOKEN fallback GITHUB_TOKEN cannot push a change under .github/workflows/, so the fallback only ran the whole job (clone, scan, commit) to a refused push. GH_TOKEN is the automation token alone, and the first step fails with the secret's name and the scopes it needs when it is absent. The refused-push error stays as the second line of defence. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/track-estate-audit.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/track-estate-audit.yml b/.github/workflows/track-estate-audit.yml index 09e7464..ef6a9aa 100644 --- a/.github/workflows/track-estate-audit.yml +++ b/.github/workflows/track-estate-audit.yml @@ -46,7 +46,10 @@ jobs: contents: write pull-requests: write env: - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN || secrets.GITHUB_TOKEN }} + # AUTOMATION_TOKEN only. GITHUB_TOKEN cannot push a change under + # .github/workflows/, so a fallback to it would run the whole job to a + # refused push; the first step below fails instead, naming the secret. + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} BRANCH: ci/track-estate-audit BOT_EMAIL: 41898583+github-actions[bot]@users.noreply.github.com steps: @@ -59,6 +62,14 @@ jobs: allowed-endpoints: > api.github.com:443 github.com:443 + - name: Require the automation token + env: + HAVE_TOKEN: ${{ secrets.AUTOMATION_TOKEN != '' }} + run: | + if [ "$HAVE_TOKEN" != "true" ]; then + echo "::error::AUTOMATION_TOKEN is not set. This job edits files under .github/workflows/, which GITHUB_TOKEN may not push; add the fine-grained token described at the top of this workflow (Contents, Pull requests and Workflows: read/write)." + exit 1 + fi - name: Move every cicd-suite pin to HEAD and open a PR run: | set -euo pipefail From b72657f5bd5a07237255448843779bfb761c9e0e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:16:21 +0000 Subject: [PATCH 7/8] ci(track-estate-audit): lease the bot-branch push on the tip that was inspected The foreign-commit check reads the bot branch, then a plain --force push would overwrite anything that landed on it in between. The push now carries --force-with-lease=refs/heads/: (an empty tip meaning the branch must not exist yet), so a concurrent push is refused by git and reported, never lost. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/track-estate-audit.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/track-estate-audit.yml b/.github/workflows/track-estate-audit.yml index ef6a9aa..dd91553 100644 --- a/.github/workflows/track-estate-audit.yml +++ b/.github/workflows/track-estate-audit.yml @@ -113,7 +113,11 @@ jobs: # The bot branch is rebuilt from the default branch each run, which # would discard anything else on it. If someone has pushed to it # (say, a fix the newer gate needed), leave it for review. - if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null; then + # The bot branch's tip as inspected here is the lease for the push + # below: a commit landing on the branch between this check and the + # push is refused by git ("stale info"), never overwritten. + remote_sha=$(git ls-remote --heads origin "$BRANCH" | cut -f1) + if [ -n "$remote_sha" ]; then foreign=$(gh api "repos/$GITHUB_REPOSITORY/compare/$base...$BRANCH" \ --jq "[.commits[] | select(.commit.author.email != \"$BOT_EMAIL\")] | length") if [ "$foreign" != 0 ]; then @@ -125,8 +129,9 @@ jobs: git switch --quiet -c "$BRANCH" git -c user.name='github-actions[bot]' -c user.email="$BOT_EMAIL" \ commit --quiet -am "ci(cicd-suite): track cicd-suite ${head:0:7}" - if ! git push --quiet --force origin "$BRANCH"; then - echo "::error::Push refused: writing under .github/workflows/ needs the AUTOMATION_TOKEN secret described above." + # An empty lease value means "the ref must not exist yet". + if ! git push --quiet --force-with-lease="refs/heads/$BRANCH:$remote_sha" origin "$BRANCH"; then + echo "::error::Push refused: either $BRANCH moved since it was inspected (someone pushed to it; re-run) or the token cannot write under .github/workflows/ (AUTOMATION_TOKEN, described above)." exit 1 fi body="Moves every cicd-suite reusable this repository calls to cicd-suite \`${head:0:7}\`: https://github.com/hyperpolymath/cicd-suite/commit/$head. This PR's own gate runs are the check. Opened by \`track-estate-audit.yml\`." From 5206f9efa74890a3c5419eaa069e241d5af63e6f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 22:25:27 +0000 Subject: [PATCH 8/8] ci(track-estate-audit): scope the automation token to the step that pushes GH_TOKEN was a job-level env, visible to every step including the third-party harden-runner action. Only the final shell step clones, pushes and opens the PR, so the token is defined on that step alone. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xn9nqHVbUTRLgBdedoXAc8 --- .github/workflows/track-estate-audit.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/track-estate-audit.yml b/.github/workflows/track-estate-audit.yml index dd91553..98b2775 100644 --- a/.github/workflows/track-estate-audit.yml +++ b/.github/workflows/track-estate-audit.yml @@ -46,10 +46,6 @@ jobs: contents: write pull-requests: write env: - # AUTOMATION_TOKEN only. GITHUB_TOKEN cannot push a change under - # .github/workflows/, so a fallback to it would run the whole job to a - # refused push; the first step below fails instead, naming the secret. - GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} BRANCH: ci/track-estate-audit BOT_EMAIL: 41898583+github-actions[bot]@users.noreply.github.com steps: @@ -71,6 +67,13 @@ jobs: exit 1 fi - name: Move every cicd-suite pin to HEAD and open a PR + env: + # AUTOMATION_TOKEN only, and only here: GITHUB_TOKEN cannot push a + # change under .github/workflows/ (a fallback to it would run the + # whole job to a refused push; the step above fails first, naming + # the secret), and the token is scoped to the one step that uses + # it so no earlier action — harden-runner included — can read it. + GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }} run: | set -euo pipefail gh auth setup-git