Skip to content

feat(scripts): SHA-pin tooling Suggested-tier follow-up (#176 review)#177

Merged
williaby merged 5 commits into
mainfrom
claude/sha-pin-suggested-followup-176
May 26, 2026
Merged

feat(scripts): SHA-pin tooling Suggested-tier follow-up (#176 review)#177
williaby merged 5 commits into
mainfrom
claude/sha-pin-suggested-followup-176

Conversation

@williaby
Copy link
Copy Markdown
Collaborator

Summary

Addresses the 9 Suggested-tier findings from the /pr-review of #176 that were deferred from the Important-tier scope. All changes are mechanical hardening or test additions; no breaking changes to the default flow.

Builds directly on the merged v1.4.2 (PR #176).

Findings addressed

Added

  • S-7 STRICT_AUDIT mode + S-1 REPO_LIMIT env override (scripts/fleet-audit-sha-pins.sh): the audit script now supports STRICT_AUDIT=1 to fail closed (exit 2) when the run is incomplete (per-repo repo,error row or REPO_LIMIT saturation). REPO_LIMIT is now env-overridable (REPO_LIMIT=\"\${REPO_LIMIT:-1000}\") so tests can exercise the saturation WARN without a 1000-repo stub. Default flow remains report-only (exit 0).

Fixed

  • S-6 annotated-tag null guard (scripts/update-pinned-actions.sh): resolve_tag_sha now validates that .object.type and .object.sha are non-empty and not the literal string \"null\" (which jq -r emits for missing fields). The previous code accepted \"null\" as a successful resolution and produced patterns like actions/checkout@null that the caller wrote into the workflow file. Guard fires after both the composite first-call and the annotated-tag second-call.
  • S-9 sed-delimiter documentation (scripts/update-pinned-actions.sh): the safe_tag escape block's inline comment now explicitly names the three replacement-side specials (\\, &, |) and notes that the | escape is tied to the sed delimiter choice. The pattern-side escape lives in escape_sed_pat (added in PR fix(scripts): SHA-pin tooling robustness follow-up (#175 review) #176), already documented.

Tests

  • S-1: REPO_LIMIT saturation WARN exercised with REPO_LIMIT=3 + 3-repo stub
  • S-2: extract_branch_pins reports a comment-annotated branch ref (previously only the tag path was tested with comments)
  • S-3: --pin-tags --apply succeeds with a fixture containing only first-party refs (verifies the rc-aware extractor zero-match path under set -euo pipefail)
  • S-5: SKIP_OWNERS edge cases (single value, empty string, double internal commas)
  • S-6 test: annotated-tag dereferencing where the second-step response is missing .object.sha
  • S-7 tests: STRICT_AUDIT=1 fail-closed on saturation, fail-closed on per-repo error, and stays exit 0 on a clean run (regression guard)

Findings re-scoped after PR #176 merge

  • S-3 zero-match pipefail tolerance (originally about verifying the { ...; } || true wrapper): the || true wrappers were removed by PR fix(scripts): SHA-pin tooling robustness follow-up (#175 review) #176 in favor of explicit grep rc handling. The Suggested-tier test now verifies the new rc-aware path returns 0 cleanly when the first grep matches lines but the awk filter zeroes them all (all-first-party fixture). Same coverage intent, different mechanism.
  • S-4 legacy-path EXIT trap leak: implicitly covered by the I-11 portability fix in PR fix(scripts): SHA-pin tooling robustness follow-up (#175 review) #176 (${VAR:+\"\$VAR\"} expansion). Any existing legacy-path test exercises the trap; an explicit leak-counter test was judged net-negative because temp-file inspection is fragile across bats environments.
  • S-8 secret-scanner annotation portability: confirmed the project uses detect-secrets (and TruffleHog) per .pre-commit-config.yaml; # pragma: allowlist secret is the correct annotation. No code change needed; not in scope of this PR.

Test plan

  • pre-commit run --files <changed> passes (trailing whitespace, EOL, secrets, em-dash all clean)
  • shellcheck clean on both modified scripts (only the pre-existing SC2001 style note on line 250 remains, unchanged by this branch)
  • Manual reproducer for the 4 most fragile new tests: REPO_LIMIT WARN, STRICT_AUDIT exit 2 on saturation, SKIP_OWNERS="", SKIP_OWNERS=',,actions,,' all behave as asserted
  • CI green (Bats Tests on Ubuntu, SonarCloud, etc.) -- pending push
  • No new SonarCloud findings on the diff

Notes

  • Total new test surface: 9 cases (4 in tests/fleet-audit-sha-pins.bats, 3 in tests/update-pinned-actions.bats, plus the 2 STRICT_AUDIT diagnostic variants). Full suite is now 52 -> 61.
  • All 4 commits are SSH-signed.

🤖 Generated with Claude Code

williaby and others added 4 commits May 25, 2026 19:57
- REPO_LIMIT now reads from environment with default 1000:
  `REPO_LIMIT="${REPO_LIMIT:-1000}"`. Allows tests to exercise the
  saturation WARN with a small stub-friendly value and gives operators
  an escape hatch for fleets that grow beyond the default.
- New STRICT_AUDIT=1 mode: track an audit_incomplete flag during the
  run. When STRICT_AUDIT=1 is set in the environment and any repo
  emitted a `repo,error` row OR REPO_LIMIT saturated, exit 2 with a
  diagnostic stderr line. The default flow remains report-only (exit 0)
  so existing CI consumers do not break; CI gates that need fail-closed
  semantics opt in by setting STRICT_AUDIT=1.
- Header comment updated with both env vars under a new
  "Environment overrides" block.

Addresses Suggested findings S-7 (STRICT_AUDIT fail-closed mode) and
S-1 partial (env override for REPO_LIMIT) from the /pr-review of #176.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the GitHub API returns a tag-ref response missing `.object.type` or
`.object.sha`, jq -r emits the literal string "null". The previous code
accepted that as a successful resolution and produced patterns like
`actions/checkout@null` which the caller then wrote into the workflow
file (replacing the original tag-pinned ref with a broken SHA pin).

After both the initial composite fetch and the second annotated-tag
dereferencing call, validate that the returned fields are non-empty and
not the literal "null". Return empty (the existing failure convention)
so the caller marks the action SKIPPED rather than writing junk.

Also clarify the sed-delimiter assumption in the safe_tag escape block:
the comment now explicitly names the three replacement-side specials
(\, &, |) and notes that the | escape is tied to the sed delimiter
choice; if the delimiter ever changes, this set must be updated.

Addresses Suggested findings S-6 (annotated-tag .object.sha guard)
and S-9 (sed-delimiter documentation) from the /pr-review of #176.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds nine new bats cases guarding the production fixes in this branch
and the residual coverage gaps the /pr-review of #176 surfaced:

fleet-audit-sha-pins.bats:
- "audit warns when gh repo list saturates REPO_LIMIT": exercises the
  saturation WARN with REPO_LIMIT=3 + a 3-repo stub
- "STRICT_AUDIT=1 exits non-zero on REPO_LIMIT saturation": verifies
  the new fail-closed exit-2 path on saturation
- "STRICT_AUDIT=1 exits non-zero when any repo emits error sentinel":
  verifies fail-closed exit-2 path on per-repo API failure
- "STRICT_AUDIT=1 remains exit 0 on a fully clean run": regression guard
  so STRICT_AUDIT does not break healthy audits
- "--skip-owners with a single owner (no commas)": verifies the case
  framing handles a single-value SKIP_OWNERS (",actions,")
- "--skip-owners with empty string does not skip anything": empty
  SKIP_OWNERS produces "," framing that matches no owner
- "--skip-owners with double internal commas still matches listed
  owners": noisy empty segments do not break the match

update-pinned-actions.bats:
- "--pin-tags reports branch refs that carry an inline comment":
  extract_branch_pins must surface a comment-annotated branch ref
  (previously only the tag-extraction path was tested with comments)
- "--pin-tags --apply on a workflow with no third-party refs succeeds
  without warnings": first-party-only fixture exercises the rc-aware
  extract_*_pins zero-match path under set -euo pipefail
- "--pin-tags --apply skips when annotated-tag dereferencing returns
  missing .object.sha": stubs the second-step call to return an object
  without .sha; asserts the new null guard marks the action SKIPPED
  and never writes "actions/checkout@null" into the workflow file

Addresses Suggested findings S-1 (REPO_LIMIT test), S-2 (branch-pin
inline-comment test), S-3 (zero-match fixture), S-5 (SKIP_OWNERS
edges), S-6 test (annotated-tag null), and S-7 tests (STRICT_AUDIT)
from the /pr-review of #176.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add ### Added entry for the new STRICT_AUDIT=1 environment toggle
  and the env-overridable REPO_LIMIT
- Add ### Fixed entry for the resolve_tag_sha guard against jq's
  literal "null" output when a tag-ref response is missing object
  fields
- Add ### Fixed entry summarizing the nine new bats cases added in
  this branch covering Suggested-tier regression vectors

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 02:59
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@williaby, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fda17804-ffe5-4fdf-b47e-2c6381e99618

📥 Commits

Reviewing files that changed from the base of the PR and between 20d63e1 and 6bf735b.

📒 Files selected for processing (7)
  • .claude/CLAUDE.md
  • .github/workflows/shell-tests.yml
  • CHANGELOG.md
  • scripts/fleet-audit-sha-pins.sh
  • scripts/update-pinned-actions.sh
  • tests/fleet-audit-sha-pins.bats
  • tests/update-pinned-actions.bats
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/sha-pin-suggested-followup-176

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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Follow-up hardening for the SHA-pin tooling scripts, addressing Suggested-tier review findings by adding fail-closed audit behavior, tightening tag resolution correctness, and expanding Bats regression coverage.

Changes:

  • Add STRICT_AUDIT=1 fail-closed mode and env-overridable REPO_LIMIT to fleet-audit-sha-pins.sh, plus new regression tests for saturation and skip-owner edge cases.
  • Prevent update-pinned-actions.sh from accepting jq -r "null" values during tag SHA resolution, plus add tests for annotated-tag edge cases and zero third-party workflows.
  • Document the new behaviors in CHANGELOG.md and clarify sed delimiter escape semantics.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/fleet-audit-sha-pins.sh Add STRICT_AUDIT and env-overridable REPO_LIMIT, track incompleteness to optionally fail closed.
scripts/update-pinned-actions.sh Add "null" guards in resolve_tag_sha, clarify sed replacement escaping documentation.
tests/fleet-audit-sha-pins.bats Add coverage for REPO_LIMIT saturation warnings, STRICT_AUDIT exit behavior, and skip-owner edge cases.
tests/update-pinned-actions.bats Add coverage for branch refs with inline comments, zero third-party refs, and annotated-tag dereference missing .object.sha.
CHANGELOG.md Document STRICT_AUDIT and REPO_LIMIT behavior and new regression tests.

Comment on lines 188 to 192
if [[ "$api_error" == true ]]; then
emit "$full,error"
audit_incomplete=true
else
emit "$full,$violations"
REPO_LIMIT=1000
# REPO_LIMIT defaults to 1000 but may be overridden via environment for tests
# and for fleets that grow beyond the default.
REPO_LIMIT="${REPO_LIMIT:-1000}"
@williaby
Copy link
Copy Markdown
Collaborator Author

PR Review

Comprehensive /pr-review complete. All 26 CI checks pass, but the review surfaced a reproduced production bug and a CI gap that hides it. Recommend blocking merge until Critical #1 and #2 are fixed together.

Critical

  1. STRICT_AUDIT contract violation -- scripts/fleet-audit-sha-pins.sh#L135
    The workflows-listing failure path emits "$full,error" and continue without setting audit_incomplete=true. Only line 124 (REPO_LIMIT saturation) and line 190 (per-file-fetch error) set the flag. Verified by manual reproduction: STRICT_AUDIT=1 with a 429-stub exits 0 (not 2) even though a repo,error row was emitted. Violates the contract the PR introduces. Also flagged by Copilot.
    Fix: add audit_incomplete=true between the emit "$full,error" and continue at line 135-136.

  2. Test file never executed by CI -- .github/workflows/shell-tests.yml#L55
    The Bats job hardcodes tests/update-pinned-actions.bats; tests/fleet-audit-sha-pins.bats (13 tests, including all 4 new STRICT_AUDIT/REPO_LIMIT cases added here) is silently skipped. This is why the build passes despite Critical feat: update organization files to align with current best practices #1. ~118 of 285 PR lines provide zero regression protection.
    Fix: change line 55 to ./tests/libs/bats-core/bin/bats tests/*.bats.

Important

  1. mapfile rc loss on gh repo list failure -- scripts/fleet-audit-sha-pins.sh#L121
    mapfile -t repos < <(gh repo list ...) discards gh's exit code via the process substitution. Auth loss or rate-limit during enumeration yields an empty repos array; that org silently contributes zero rows and audit_incomplete is never set, defeating STRICT_AUDIT in a second way.

  2. REPO_LIMIT not validated as positive integer -- scripts/fleet-audit-sha-pins.sh#L112
    Non-numeric value (e.g. REPO_LIMIT=abc) makes [[ ${#repos[@]} -eq $REPO_LIMIT ]] throw integer expression expected and abort under set -euo pipefail. Also flagged by Copilot.

  3. Annotated-tag null-guard is silent -- scripts/update-pinned-actions.sh#L135
    The new null guards return empty without a stderr WARN, leaving operators no signal distinguishing "tag missing" from "malformed GitHub API response."

Suggested + Informational (10 additional findings)

Test gaps for the first-call null-guard path, mapfile failure path, and invalid REPO_LIMIT; CRLF handling at <<< "$content"; header-comment Exit-code line still contradicting STRICT_AUDIT; weak assertion in the branch-with-comment test; minor UX/comment-accuracy items. Full list in the local review report.

Other reviewer signals

Recommended sequencing

Fix Critical #1 and #2 together. Fix #2 alone and the existing STRICT_AUDIT error-sentinel test will start failing CI, confirming Critical #1.

Triggering /pr-fix to address findings in an isolated worktree.

🤖 Generated with Claude Code

… and CI

Critical

- scripts/fleet-audit-sha-pins.sh: the workflows-listing failure path
  emitted a "$full,error" sentinel and continued without setting
  audit_incomplete=true, so STRICT_AUDIT=1 exited 0 even when error
  rows were present. Verified by manual reproduction with a 429-stub
  before the fix. New bats case "exits non-zero on workflows-listing
  failure (Critical #1 regression)" is the regression guard.
- .github/workflows/shell-tests.yml: bats runner only executed
  tests/update-pinned-actions.bats; the 13 cases in
  tests/fleet-audit-sha-pins.bats (including all STRICT_AUDIT and
  REPO_LIMIT tests from PR #176 and the new ones in PR #177) never
  ran in CI. Switched to find + xargs over tests/*.bats, which is
  glob-injection-safe and discovers new test files automatically.
  This is the reason Critical #1 escaped detection until self-review.

Important

- scripts/fleet-audit-sha-pins.sh: mapfile -t repos < <(gh repo list ...)
  discarded the gh exit code via process substitution; auth-loss or
  rate-limit mid-enumeration silently contributed zero rows. Now
  captures stdout to a tmpfile and surfaces the rc, flagging
  audit_incomplete on failure.
- scripts/fleet-audit-sha-pins.sh: REPO_LIMIT validated as positive
  integer at script entry; previously a non-numeric value crashed
  inside [[ -eq ]] under set -euo pipefail. Exits 1 with a clear
  error message instead.
- scripts/update-pinned-actions.sh: null-guard paths in resolve_tag_sha
  now emit a stderr WARN before returning empty so operators can
  distinguish a malformed GitHub API response from a missing tag.
- scripts/fleet-audit-sha-pins.sh: STRICT_AUDIT accepts truthy
  spellings 1, true, yes (case-insensitive). Previously only "1"
  worked and STRICT_AUDIT=true silently degraded to report-only.

Suggested

- scripts/fleet-audit-sha-pins.sh: strip trailing CR from each line
  before regex matching so workflow files with CRLF endings do not
  leak \r into BASH_REMATCH and inflate violation counts.
- tests/fleet-audit-sha-pins.bats: 8 new regression tests covering
  the above paths.
- tests/update-pinned-actions.bats: 1 new test for the first-call
  null guard in resolve_tag_sha (the second-call path was already
  covered); strengthened branch-with-comment assertion to anchor on
  the "WARN: branch ref detected" header; added positive controls
  to the empty-SKIP_OWNERS test.
- scripts/update-pinned-actions.sh: maintenance note that changing
  the sed delimiter requires updating both the replacement-side
  escape set and the pattern-side escape_sed_pat() function.
- .claude/CLAUDE.md: corrected stale statement that bats coverage
  is limited to scripts/update-pinned-actions.sh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

@williaby
Copy link
Copy Markdown
Collaborator Author

PR Fix Summary

Addressed all Critical and Important findings from the /pr-review of this PR (and most Suggested) in a single commit: 6bf735b.

Critical (both fixed)

  1. STRICT_AUDIT contract violation (scripts/fleet-audit-sha-pins.sh:135) -- workflows-listing failure path now sets audit_incomplete=true before continuing, matching the per-file-fetch error path. Reproduced the bug locally with a 429 stub before the fix (exit 0) and confirmed exit 2 after. Also addresses @copilot's first inline comment.

  2. bats CI gap (.github/workflows/shell-tests.yml:55) -- bats runner now discovers all tests/*.bats files via find ... -print0 | xargs -0. Previously only tests/update-pinned-actions.bats ran in CI, which is why Critical feat: update organization files to align with current best practices #1 shipped green. The 21 cases in tests/fleet-audit-sha-pins.bats (including all STRICT_AUDIT and REPO_LIMIT regression tests) will now execute on every push.

Important (all fixed)

  1. mapfile rc loss on gh repo list (scripts/fleet-audit-sha-pins.sh:128-139) -- captures stdout to a tmpfile so the gh exit code surfaces. Auth-loss or rate-limit mid-enumeration now flags the audit incomplete with a stderr WARN.

  2. REPO_LIMIT validation (scripts/fleet-audit-sha-pins.sh:118-122) -- positive-integer validation up front; non-numeric, zero, or negative values exit 1 with a clear error message before the loop body. Empty strings continue to fall back to the default 1000 (per ${VAR:-default} bash semantics). Also addresses @copilot's second inline comment.

  3. Silent null-guards in resolve_tag_sha (scripts/update-pinned-actions.sh:139, 148) -- both null-guard paths now emit a stderr WARN so operators can distinguish a malformed GitHub API response from a missing tag.

Suggested (6 of 7 fixed)

  • CRLF stripping in the audit inner loop
  • STRICT_AUDIT accepts 1|true|yes case-insensitively
  • First-call null-guard test added (the second-call path was already covered)
  • Branch-with-comment assertion strengthened to anchor on WARN: branch ref detected block
  • Empty-SKIP_OWNERS test gains positive controls (clean-row assertion, refute spurious WARN)
  • Maintenance note on delimiter / escape_sed_pat sync requirement
  • Deferred: double-comma + multi-owner SKIP_OWNERS combo (low value, not regression-blocking)

Test coverage

  • tests/fleet-audit-sha-pins.bats: 13 -> 21 cases (8 new)
  • tests/update-pinned-actions.bats: 49 -> 50 cases (1 new)
  • All 71 cases pass locally
  • All 11 pre-commit hooks pass on the changed files
  • shellcheck clean on both modified scripts (only the pre-existing SC2001 style note remains)
  • actionlint clean on .github/workflows/shell-tests.yml
  • Commit is SSH-signed

CI re-run triggered by push. The Bats Tests check will exercise the new test surface for the first time.

🤖 Generated with Claude Code

@williaby williaby merged commit 4eedf13 into main May 26, 2026
27 checks passed
@williaby williaby deleted the claude/sha-pin-suggested-followup-176 branch May 26, 2026 03:42
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.

2 participants