From b2f688c392ca6389354131162f6da7401a7b27f4 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:29:43 +0100 Subject: [PATCH 1/2] fix(ci): unmask exit-masking steps; annotate fail-closed (#942) Hypatia RE005 flagged 20 workflow steps across 10 files for silently swallowing non-zero exit via `|| true` / `continue-on-error: true`. Each instance was read in context (the step itself plus what runs after it) and classified as a real mask (A, fixed) or a deliberate fail-closed design (B, left alone and annotated with an inline `# hypatia: allow` pragma). 1. affinescript-verify.yml "Checkout AffineScript compiler" -- advisory annotation added (fails OPEN, not B: best-effort checkout while BLOCKING is false; "Verify changed .affine files" surfaces an explicit ::warning:: instead of a silent green claim). 2. changelog-reusable.yml "Mode = check-only -- verify no drift" -- A: removed inert `|| true` (pipe ends in `head -60`; the following `exit 1` fails the job either way). 3. ci-pipeline.yml "Checkout the pinned Standards Deno ledger" -- B: annotated fail-closed; "Refuse Deno unless this repository is ledgered" checks for the ledger file and exits 1 with ::error:: if missing. 4. echidna-verify.yml "Type-check proofs" -- A: rewrote so a real `agda --safe` failure is detected (was previously unreachable under `tee`'s exit status plus a redundant `|| true`) and surfaced via ::warning::, kept advisory since the proof corpus is currently evicted (issue #748) and re-entry criteria aren't set. 5-14. governance-reusable.yml (ten instances): - "Check banned-language files" -- A: removed inert `|| true` on `git ls-files`-terminated captures (RES/GO/SWIFT/DART/VMOD), narrowed to `|| [ $? -eq 1 ]` on `grep`-terminated captures (PY/MAKE/JAVA) so a real grep error (exit 2) still trips `-e`. - "Check for npm/yarn artifacts" -- A: removed 4 inert `|| true` (`git ls-files`/`find`-terminated). - "Security checks" -- A: removed 3 inert `|| true` (`head`-terminated). - "Check file permissions" / "Check TODO/FIXME" / "Check for large files" -- A: dropped `continue-on-error: true` entirely (each step already always exits 0) and now emit an explicit ::warning:: instead of a silent printout. - "EditorConfig check" -- advisory annotation added (fails OPEN, not B: a follow-up step surfaces ::warning:: on failure; repos opt into blocking locally). - "Mixed content check" -- A: removed inert `|| true` (`head`-terminated). - "Checkout the pinned Standards policy helpers" -- B: annotated fail-closed; "Duplicate YAML keys in workflows" refuses to run (::error:: + exit 1) when neither script copy is present. - "Check locked or SHA-pinned actions" -- A: narrowed `|| true` to `|| [ $? -eq 1 ]` (`grep -cve`-terminated; an empty ledger file is an expected, not error, case). 15. hypatia-scan-reusable.yml "Check out standards for the SARIF baseline filter" -- advisory annotation added (fails OPEN, not B: the fallback uploads the SARIF unfiltered, which can only show more alerts). 16. readme-derive-reusable.yml "Freshness check (fail-and-tell)" -- A: narrowed `|| true` to `|| [ $? -eq 1 ]` on a bare `diff` display command; the following regen instructions + `exit 1` still fire. 17. scorecard-enforcer.yml "Check for pinned dependencies" -- A: removed inert `|| true` (`head`-terminated; already emits ::warning:: itself). 18-19. secret-scanner-reusable.yml "Check for hardcoded secrets in Rust" / "... in shell scripts" -- A: narrowed `grep`-terminated captures to `|| [ $? -eq 1 ]`, removed one inert `sed -n`-terminated `|| true`. 20. security-gate-pr-target.yml "Extract PR branch for safe checkout" -- A: the most severe finding. Removed the same-named-base-repo-branch fallback (which could let a malicious fork PR's real content go unscanned while reporting success) and converted silent tolerance of fetch/checkout failure into explicit ::error:: + exit 1. Validation: `ruby -ryaml -e 'YAML.load_file(...)'` and `yq .` both pass cleanly on all 10 files. `actionlint` exits 1 but every finding (shellcheck info/style/warning notes and pre-existing `job.workflow_sha` property errors) is confirmed pre-existing and unrelated to these edits -- zero new findings introduced. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QFphKkDVB9pUDSCD4bkz65 --- .github/workflows/affinescript-verify.yml | 6 +- .github/workflows/changelog-reusable.yml | 2 +- .github/workflows/ci-pipeline.yml | 1 + .github/workflows/echidna-verify.yml | 13 ++- .github/workflows/governance-reusable.yml | 86 ++++++++++++------- .github/workflows/hypatia-scan-reusable.yml | 6 ++ .github/workflows/readme-derive-reusable.yml | 2 +- .github/workflows/scorecard-enforcer.yml | 2 +- .github/workflows/secret-scanner-reusable.yml | 10 +-- .github/workflows/security-gate-pr-target.yml | 26 ++++-- 10 files changed, 108 insertions(+), 46 deletions(-) diff --git a/.github/workflows/affinescript-verify.yml b/.github/workflows/affinescript-verify.yml index caeea04ba..14bf63735 100644 --- a/.github/workflows/affinescript-verify.yml +++ b/.github/workflows/affinescript-verify.yml @@ -89,7 +89,11 @@ jobs: - name: Checkout AffineScript compiler if: steps.changed.outputs.any == 'true' # advisory: compiler checkout is report-only until the port backlog - # is cleared and BLOCKING flips to true. + # is cleared and BLOCKING flips to true. "Verify changed .affine + # files" below checks steps.build.outputs.ok and the checkout dir, + # and on toolchain-unavailable emits an explicit ::warning:: + + # step-summary "SKIPPED — not a pass" instead of a silent green claim. + # hypatia: allow research_extensions/RE005 -- advisory-by-design, fails OPEN not closed; see comment above. continue-on-error: true uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/changelog-reusable.yml b/.github/workflows/changelog-reusable.yml index dbadb5175..c3b238e1c 100644 --- a/.github/workflows/changelog-reusable.yml +++ b/.github/workflows/changelog-reusable.yml @@ -176,7 +176,7 @@ jobs: if ! diff -q CHANGELOG.md CHANGELOG.md.new >/dev/null; then echo "ERROR: CHANGELOG.md is out of date relative to commit history." echo "Run git-cliff locally or switch to mode=commit-back." - diff -u CHANGELOG.md CHANGELOG.md.new | head -60 || true + diff -u CHANGELOG.md CHANGELOG.md.new | head -60 exit 1 fi echo "CHANGELOG.md is up to date." diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 5def8baf9..3ebe639f9 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -615,6 +615,7 @@ jobs: sparse-checkout-cone-mode: false # Not fatal here: the next step names precisely what was missing and # then FAILS CLOSED. A fetch failure must never read as an exemption. + # hypatia: allow research_extensions/RE005 -- deliberate fail-closed: "Refuse Deno unless this repository is ledgered" below checks for the ledger file and exits 1 with an explicit ::error:: if missing, regardless of why. continue-on-error: true - name: Refuse Deno unless this repository is ledgered diff --git a/.github/workflows/echidna-verify.yml b/.github/workflows/echidna-verify.yml index dad3ff380..c4f8594d6 100644 --- a/.github/workflows/echidna-verify.yml +++ b/.github/workflows/echidna-verify.yml @@ -124,14 +124,23 @@ jobs: id: typecheck if: steps.guard.outputs.present == 'true' run: | - set -e + set -eo pipefail echo "=== Agda proofs under lol/proofs ===" find lol/proofs/theories -name '*.agda' -print cd lol/proofs + failed="" for f in $(find theories -name '*.agda'); do echo "--- checking $f ---" - agda --safe "$f" 2>&1 | tee -a ../../agda-verify.log || true + if ! agda --safe "$f" 2>&1 | tee -a ../../agda-verify.log; then + failed="$failed$f"$'\n' + fi done + if [ -n "$failed" ]; then + echo "::warning::agda --safe FAILED to type-check the following proof(s) (advisory until the evicted lol/proofs corpus's re-entry criteria are set; see issue #748):" + printf '%s' "$failed" | sed 's/^/ - /' + else + echo "All Agda proofs type-checked cleanly." + fi - name: Postulate audit if: steps.guard.outputs.present == 'true' diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 18dab2de9..7e59805d6 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -559,12 +559,12 @@ jobs: # failures because `find` crawled gitignored vendor directories. # `git ls-files` works correctly on fresh PR checkouts because # actions/checkout populates the index before running workflows. - RES_FILES=$(git ls-files '*.res' || true) - GO_FILES=$(git ls-files '*.go' || true) + RES_FILES=$(git ls-files '*.res') + GO_FILES=$(git ls-files '*.go') PY_FILES=$(git ls-files '*.py' \ - | grep -v venv | grep -v __pycache__ || true) + | grep -v venv | grep -v __pycache__ || [ $? -eq 1 ]) MAKE_FILES=$(git ls-files 'Makefile' 'Makefile.*' '*.mk' \ - | grep -v '\.github/' || true) + | grep -v '\.github/' || [ $? -eq 1 ]) # Platform-required JVM shims carve-out 2026-06-02: # Java is permitted only in Android source trees # (android/**/src/**/*.java) because Android instantiates @@ -583,12 +583,12 @@ jobs: # only remaining Java carve-out is the minimal Android platform # shim above. `.groovy` is detected so it is banned everywhere. JAVA_FILES=$(git ls-files '*.java' '*.kt' '*.kts' '*.groovy' \ - | grep -vE '(^|/)android/.*/src/.*\.java$' || true) - SWIFT_FILES=$(git ls-files '*.swift' || true) - DART_FILES=$(git ls-files '*.dart' 'pubspec.yaml' || true) + | grep -vE '(^|/)android/.*/src/.*\.java$' || [ $? -eq 1 ]) + SWIFT_FILES=$(git ls-files '*.swift') + DART_FILES=$(git ls-files '*.dart' 'pubspec.yaml') # V-lang detected by manifest (v.mod / vpkg.json); the .v extension # collides with Verilog so we never key on it. - VMOD_FILES=$(git ls-files 'v.mod' 'vpkg.json' || true) + VMOD_FILES=$(git ls-files 'v.mod' 'vpkg.json') enforce "ReScript files" "use AffineScript instead" "$RES_FILES" enforce "Go files" "use Rust/WASM instead" "$GO_FILES" @@ -612,10 +612,10 @@ jobs: # runtime impossible, not merely awkward. It blocked what the policy # mandates. Bun artifacts are now accepted; npm and yarn are unchanged. run: | - LOCK_FILES=$(git ls-files 'package-lock.json' '**/package-lock.json' 2>/dev/null || true) - YARN_FILES=$(find . -name "yarn.lock" -not -path "./.git/*" 2>/dev/null || true) - NPMRC_FILES=$(find . -name ".npmrc" -not -path "./.git/*" 2>/dev/null || true) - BUN_LOCK=$(find . \( -name "bun.lock" -o -name "bun.lockb" \) -not -path "./.git/*" 2>/dev/null || true) + LOCK_FILES=$(git ls-files 'package-lock.json' '**/package-lock.json' 2>/dev/null) + YARN_FILES=$(find . -name "yarn.lock" -not -path "./.git/*" 2>/dev/null) + NPMRC_FILES=$(find . -name ".npmrc" -not -path "./.git/*" 2>/dev/null) + BUN_LOCK=$(find . \( -name "bun.lock" -o -name "bun.lockb" \) -not -path "./.git/*" 2>/dev/null) FAILED="" if [ -n "$LOCK_FILES" ]; then echo "❌ Tracked package-lock.json detected (standards#67 — npm-avoidant)." @@ -773,17 +773,17 @@ jobs: - name: Security checks run: | FAILED=false - WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) + WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5) if [ -n "$WEAK_CRYPTO" ]; then echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" echo "$WEAK_CRYPTO" fi - HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) + HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5) if [ -n "$HTTP_URLS" ]; then echo "⚠️ HTTP URLs found. Use HTTPS:" echo "$HTTP_URLS" fi - SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true) + SECRETS=$(grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.env" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3) if [ -n "$SECRETS" ]; then echo "❌ Potential hardcoded secrets detected!" FAILED=true @@ -1010,26 +1010,50 @@ jobs: # same merge commit but is always fetchable. ref: ${{ github.sha }} - name: Check file permissions + # advisory: informational only; repos can opt into blocking locally. + # No continue-on-error needed: the step below always exits 0 itself; + # it now surfaces its finding as an explicit ::warning:: instead of a + # silent printout. run: | - find . -type f -perm /111 -name "*.sh" | head -20 - continue-on-error: true - # advisory: informational only; repos can opt into blocking locally + FOUND=$(find . -type f -perm /111 -name "*.sh" | head -20) + if [ -n "$FOUND" ]; then + echo "::warning::Executable .sh files found (informational only):" + echo "$FOUND" + else + echo "No executable .sh files found." + fi - name: Check TODO/FIXME + # advisory: informational only; repos can opt into blocking locally. run: | - echo "=== TODOs ===" - grep -rn "TODO\|FIXME\|HACK\|XXX" --include="*.rs" --include="*.res" --include="*.py" --include="*.ex" . | head -20 - continue-on-error: true - # advisory: informational only; repos can opt into blocking locally + FOUND=$(grep -rn "TODO\|FIXME\|HACK\|XXX" --include="*.rs" --include="*.res" --include="*.py" --include="*.ex" . | head -20) + if [ -n "$FOUND" ]; then + echo "::warning::TODO/FIXME/HACK/XXX markers found (informational only):" + echo "$FOUND" + else + echo "No TODO/FIXME/HACK/XXX markers found." + fi - name: Check for large files + # advisory: informational only; repos can opt into blocking locally. run: | - find . -type f -size +1M -not -path "./.git/*" | head -20 - continue-on-error: true - # advisory: informational only; repos can opt into blocking locally + FOUND=$(find . -type f -size +1M -not -path "./.git/*" | head -20) + if [ -n "$FOUND" ]; then + echo "::warning::Files larger than 1M found (informational only):" + echo "$FOUND" + else + echo "No files larger than 1M found." + fi - name: EditorConfig check + id: editorconfig_check uses: editorconfig-checker/action-editorconfig-checker@51f63319f592f97930c73d9c46184d20bd206393 # v3.0.0 - # advisory: formatting hygiene is reported from the reusable estate - # bundle; repos opt into blocking formatter checks locally when ready. + # advisory-by-design, fails OPEN not closed: formatting hygiene is + # reported from the reusable estate bundle; the follow-up step below + # emits ::warning:: explicitly when this fails instead of silently + # swallowing it. Repos opt into blocking formatter checks locally. + # hypatia: allow research_extensions/RE005 -- advisory-by-design, fails OPEN; see comment above. continue-on-error: true + - name: EditorConfig check - surface result + if: steps.editorconfig_check.outcome == 'failure' + run: echo "::warning::EditorConfig check found formatting issues (advisory only — see the 'EditorConfig check' step's own log above; repos opt into blocking locally when ready)." # Sparse-check-out standards' scripts/ for the docs gate (a reusable # workflow only auto-checks-out its own YAML, not sibling scripts). - name: Check out standards for the docs gate @@ -1199,7 +1223,7 @@ jobs: fi - name: Mixed content check run: | - MIXED=$(grep -rE 'src="http://|href="http://' --include="*.html" --include="*.htm" . 2>/dev/null | grep -vE 'localhost|127\.0\.0\.1|example\.com|lol/|node_modules/|third-party/|vendor/' | head -5 || true) + MIXED=$(grep -rE 'src="http://|href="http://' --include="*.html" --include="*.htm" . 2>/dev/null | grep -vE 'localhost|127\.0\.0\.1|example\.com|lol/|node_modules/|third-party/|vendor/' | head -5) if [ -n "$MIXED" ]; then echo "::error::Mixed content (HTTP in HTML)" echo "$MIXED" @@ -1242,6 +1266,10 @@ jobs: # very PR merges. An immutable pin removes the race entirely: the names # at this SHA are fixed. The self-lint fallback in the next step still # covers standards' own tree, where a rename lands before the bump. + # "Duplicate YAML keys in workflows" below refuses to run (::error:: + + # exit 1) when neither the fetched nor the self-hosted copy of the + # checker script is present, regardless of why the fetch failed. + # hypatia: allow research_extensions/RE005 -- deliberate fail-closed: see comment above. continue-on-error: true - name: Duplicate YAML keys in workflows @@ -1451,7 +1479,7 @@ jobs: # branches): 200 carry actions.lock, 163 do not, 5 have no workflows # directory. The ledger makes that 163 an explicit, shrinking debt # instead of a cliff. - total=$(grep -cve '^[[:space:]]*$' -e '^[[:space:]]*#' "$RUNNER_TEMP/lock-allow.txt" || true) + total=$(grep -cve '^[[:space:]]*$' -e '^[[:space:]]*#' "$RUNNER_TEMP/lock-allow.txt" || [ $? -eq 1 ]) : "${total:=0}" # The ledger excuses missing-lock debt ONLY (gate exit 3: lockless, # every ref pinned, grace window closed). Exit 1 is a LIVE diff --git a/.github/workflows/hypatia-scan-reusable.yml b/.github/workflows/hypatia-scan-reusable.yml index e50b41e3b..3e1c1bba1 100644 --- a/.github/workflows/hypatia-scan-reusable.yml +++ b/.github/workflows/hypatia-scan-reusable.yml @@ -210,6 +210,12 @@ jobs: # the filter itself would produce exactly the fake gate this estate # keeps finding. Pinned to main because github.workflow_sha resolves to # the CALLER's SHA, which would 404 here. + # advisory-by-design, not fail-closed: this checkout fails OPEN. + # "Filter SARIF through the baseline before upload" below falls back + # to uploading the SARIF UNFILTERED when the fetched filter script is + # unavailable — an unfiltered upload can only show more alerts, never + # hide real ones. + # hypatia: allow research_extensions/RE005 -- advisory-by-design, fails OPEN; see comment above. continue-on-error: true uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/readme-derive-reusable.yml b/.github/workflows/readme-derive-reusable.yml index 5df5361cd..f3899bc0b 100644 --- a/.github/workflows/readme-derive-reusable.yml +++ b/.github/workflows/readme-derive-reusable.yml @@ -240,7 +240,7 @@ jobs: elif ! diff -q <(norm "$RUNNER_TEMP/derived.md") <(norm "$DERIVED_PATH") >/dev/null; then echo "::error::'$DERIVED_PATH' is STALE relative to '$CANONICAL'. Regenerate and commit it." echo "----- diff (committed vs regenerated, normalised) -----" - diff <(norm "$DERIVED_PATH") <(norm "$RUNNER_TEMP/derived.md") || true + diff <(norm "$DERIVED_PATH") <(norm "$RUNNER_TEMP/derived.md") || [ $? -eq 1 ] SHOW_CMD=1 else echo "✓ $DERIVED_PATH is up to date with $CANONICAL" diff --git a/.github/workflows/scorecard-enforcer.yml b/.github/workflows/scorecard-enforcer.yml index 4ee284168..e9e5caf46 100644 --- a/.github/workflows/scorecard-enforcer.yml +++ b/.github/workflows/scorecard-enforcer.yml @@ -80,7 +80,7 @@ jobs: - name: Check for pinned dependencies run: | # Check workflows for unpinned actions - unpinned=$(grep -r "uses:.*@v[0-9]" .github/workflows/*.yml 2>/dev/null | grep -v "#" | head -5 || true) + unpinned=$(grep -r "uses:.*@v[0-9]" .github/workflows/*.yml 2>/dev/null | grep -v "#" | head -5) if [ -n "$unpinned" ]; then echo "::warning::Found unpinned actions:" echo "$unpinned" diff --git a/.github/workflows/secret-scanner-reusable.yml b/.github/workflows/secret-scanner-reusable.yml index dae222508..0ce41aa0b 100644 --- a/.github/workflows/secret-scanner-reusable.yml +++ b/.github/workflows/secret-scanner-reusable.yml @@ -628,15 +628,15 @@ jobs: | grep -vE "$COMMENT" \ | grep -vE "$ENV_RHS" \ | grep -vE "$URL_RHS" \ - | grep -vE "$PRAGMA" || true)" + | grep -vE "$PRAGMA" || [ $? -eq 1 ])" [ -n "$hits" ] || continue # ./src was the only path the previous version scanned. Hits there # keep blocking exactly as before — no regression. Hits outside it # are newly visible, so they warn until the cutoff rather than # reddening repos that were never actually being scanned. - old_scope="$(printf '%s\n' "$hits" | grep -E '^\./src/' || true)" - new_scope="$(printf '%s\n' "$hits" | grep -vE '^\./src/' || true)" + old_scope="$(printf '%s\n' "$hits" | grep -E '^\./src/' || [ $? -eq 1 ])" + new_scope="$(printf '%s\n' "$hits" | grep -vE '^\./src/' || [ $? -eq 1 ])" if [ -n "$old_scope" ]; then printf '%s\n' "$old_scope" @@ -776,7 +776,7 @@ jobs: # Exemption 6: inline pragma on the immediately preceding line. if [[ "$lineno" -gt 1 ]]; then - prev_line=$(sed -n "$((lineno - 1))p" "$filepath" 2>/dev/null || true) + prev_line=$(sed -n "$((lineno - 1))p" "$filepath" 2>/dev/null) if echo "$prev_line" | grep -qE "$PRAGMA_RE"; then echo " [skip] $filepath:$lineno — pragma on preceding line" continue @@ -789,7 +789,7 @@ jobs: done < <(grep -rnE --include='*.sh' --include='*.bash' \ --exclude-dir='.git' --exclude-dir='node_modules' --exclude-dir='target' \ - "$pattern" . 2>/dev/null || true) + "$pattern" . 2>/dev/null || [ $? -eq 1 ]) done if [ $found -eq 1 ]; then diff --git a/.github/workflows/security-gate-pr-target.yml b/.github/workflows/security-gate-pr-target.yml index c4d9f95a5..84a28bca3 100644 --- a/.github/workflows/security-gate-pr-target.yml +++ b/.github/workflows/security-gate-pr-target.yml @@ -92,14 +92,28 @@ jobs: echo "Checking out PR branch from fork: $FORK_REPO/$PR_BRANCH" - # Add the fork as a remote temporarily - git remote add pr-fork "https://github.com/$FORK_REPO.git" 2>/dev/null || true + # Add the fork as a remote. A fresh checkout should never already + # have this remote; if this fails we want to know, not guess. + git remote add pr-fork "https://github.com/$FORK_REPO.git" - # Fetch the PR branch - git fetch pr-fork -- "$PR_BRANCH" 2>/dev/null || true + # Fetch the PR branch. Do NOT swallow failure here: the scans below + # only inspect what is actually on disk, so a silently-tolerated + # fetch failure would let them run against the base repo's own + # tree while still reporting a fork PR as scanned and clean. + if ! git fetch pr-fork -- "$PR_BRANCH"; then + echo "::error::could not fetch PR branch '$PR_BRANCH' from $FORK_REPO — refusing to run the malicious-content/file-type scans against unverified content." + echo "pr_checked_out=false" >> "$GITHUB_OUTPUT" + exit 1 + fi - # Checkout the PR branch - git checkout -f "pr-fork/$PR_BRANCH" 2>/dev/null || git checkout -f "$PR_BRANCH" 2>/dev/null || true + # Checkout the fetched PR branch. No fallback to a same-named + # branch in the base repo: that would silently scan the wrong + # (trusted, non-fork) tree while still reporting success. + if ! git checkout -f "pr-fork/$PR_BRANCH"; then + echo "::error::could not check out fetched branch 'pr-fork/$PR_BRANCH' — refusing to run the malicious-content/file-type scans against unverified content." + echo "pr_checked_out=false" >> "$GITHUB_OUTPUT" + exit 1 + fi echo "pr_checked_out=true" >> "$GITHUB_OUTPUT" From 8e0f96b1b7e7456f21f3c9bb3eebeb5b9e4bce0b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:35:24 +0100 Subject: [PATCH 2/2] chore(ci): annotate last two fail-closed RE005 sites (#942) The Nickel import scan's `|| true` absorbs grep's exit 1 on a file with no imports; the ledger checkout's continue-on-error feeds a Verdict step that treats unreadable == empty == blocked. Local scan: RE005 22 -> 0, total findings 150 -> 128. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QFphKkDVB9pUDSCD4bkz65 --- .github/workflows/ci-pipeline.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 3ebe639f9..237988754 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -524,6 +524,7 @@ jobs: # Comments are stripped first: `_base.ncl` documents its own usage # in a `#` comment containing a literal `import "../_base.ncl"`, and # matching that would skip a file that typechecks perfectly well. + # hypatia: allow research_extensions/RE005 -- `|| true` absorbs grep's exit 1 on a file with no imports; an empty import list is the correct answer there, and the typecheck below still gates. done < <(sed 's/#.*//' "$f" | grep -oE 'import[[:space:]]+"[^"]+"' | sed -E 's/.*"([^"]+)".*/\1/' || true) if [ -n "$MISSING" ]; then @@ -1038,6 +1039,7 @@ jobs: .machine_readable/pipeline-allow.txt sparse-checkout-cone-mode: false # Not fatal: the next step names what was missing and then FAILS CLOSED. + # hypatia: allow research_extensions/RE005 -- deliberate fail-closed: the Verdict step below treats an unreadable ledger exactly like an empty one, i.e. blocked. continue-on-error: true - name: Verdict — block unless this repository is ledgered