Skip to content

feat(ci): exempt trusted bots from the independent-review-pending gate - #1859

Merged
tvna merged 13 commits into
mainfrom
claude/dependabot-merge-blocker-b3wwl6
Sep 6, 2026
Merged

tvna merged 13 commits into
mainfrom
claude/dependabot-merge-blocker-b3wwl6

Conversation

@tvna

@tvna tvna commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Exempts trusted bots (Dependabot today) from independent-review-pending's human-verdict requirement, substituting a machine-checkable condition (all other required status checks green) so Dependabot's own PRs stop being permanently unmergeable. Human- and AI-agent-authored PRs keep the existing strict rule unchanged.

Facts

Assumptions

Acceptance Criteria Map

(restated from issue #1858)

Criterion Interpretation Proof method Residual risk
Manage a whitelist of trusted bots New .github/trusted-bots.yml, CODEOWNERS-protected; matched on login+id+type, not login alone Positive/negative match unit tests Identity check is account-level only, not cryptographic
Exemption only for commits the bot actually authored Bot path requires BOTH a whitelist match AND the head commit's own author+committer email matching main.json's email-pattern rules; PASS iff every other required check is green; both trust-anchor files are fetched from the PR's base commit (not its own working tree), and that base ref must equal the repo's real default branch Simulated-bot-PR tests for PASS/FAIL/timeout; defeat tests for an email mismatch, a retargeted base branch, and a mid-poll check re-run Poll timeout may need retuning if a required job's runtime grows
Human/agent PRs keep the existing rule Non-whitelisted or email-mismatched PRs take the existing, byte-for-byte unmodified parse_verdict/check() path Full pre-existing regression suite passes unmodified None identified
main.json's other rule types are never misread as check contexts Extracts only the required_status_checks rule Fixture test with every other rule type present None identified

Dedup: not applicable to a PR body -- issue-level dedup was already disclosed in issue #1858 at creation time.

Risk / blast radius

Scoped to independent-review-pending's own decision logic and one new config file; human/agent PR review requirements are unaffected. Anti-spoofing (login+id+type, head-commit-email match, trust-anchor-ref pinned to the repo's default branch) limits impersonation to a GitHub-account-level attack, matching this repo's existing single-operator trust model.

Rollback

Revert this PR's commits (git revert). No live branch-protection change is required -- main.json's required-checks list itself is unchanged.

Verification

  • Full suite: uv run --frozen python3 -m pytest --no-cov -q -> 8735 passed.
  • uv run --frozen python3 .github/scripts/gitapex_gate_local_preflight.py -> 47/47 gates PASS (head 2b121f0a).
  • Codecov (informational, not required): patch 95.91%; the 10 uncovered lines are pre-existing malformed-input defensive branches, unrelated to this PR's own new code, reviewed and confirmed fail-closed.
  • Latest commit (2b121f0a) is docs/comments-only (trimmed revision-history narration from the design doc and gate-script docstrings/comments per reviewer feedback) -- no logic change; same 8735 tests, mypy, ruff, and local-preflight all still pass.

Checklist

  • Tests pass locally
  • Docs updated if behavior changed
  • Issue number cited in every commit
  • If this PR adds/modifies a skills/*/SKILL.md, a docs/superpowers/specs/*.md design doc, a security-relevant skill, or a deterministic checker script (skills/*/scripts/*.py, evals/scripts/*.py, .github/scripts/*.py), a ## Skill audit evidence section discloses the required verdicts/waivers (see .github/scripts/gitapex_gate_skill_audit_disclosure.py)
  • If this PR adds a new Kept-edit-log entry to any evals/*/split.md, that entry discloses a Transfer check line (see .github/scripts/gitapex_gate_transfer_check_disclosure.py)
  • If this PR adds or increases a skills/*/SKILL.md's Stop-boundary bullets or named dispatch branches, evals/<skill>/tasks/*.yaml gained at least as many new fixtures (see .github/scripts/gitapex_gate_skill_branch_fixture_coverage.py)

Skill audit evidence

This PR adds/modifies a design doc (docs/gitapex/specs/2026-09-06-dependabot-trusted-bot-gate-exemption-design.md) and a deterministic gate script (.github/scripts/gitapex_gate_independent_review_pending.py).

  • design-doc-adversarial-review: RAN -- found and fixed 1 critical defect (bot-identity matched only the PR opener, not the head commit); see the design doc's own Decision logic detail section.
  • checker-script-adversarial-review: RAN -- 8 independent review-persona rounds across implementation and the Step 8 merge review. Confirmed and fixed 4 defects: trust anchors read from the PR's own working tree instead of its base commit; main.json had no CODEOWNERS protection; the poll loop cached a check's conclusion instead of re-checking it; the base ref wasn't verified against the repo's real default branch. Final round found no surviving defects.
  • deterministic-gate-quality: RAN -- every new branch fails closed on malformed or unresolved input (dimension 15); verified with dedicated malformed-input tests, not only happy-path fixtures.
  • defeat-test-disclosure: RAN -- each new detection branch has a test built to defeat it, not just exercise its happy path (see tests/test_gitapex_gate_independent_review_pending.py).

Merge gate: independent review

This PR is also subject to the independent-review-pending required
status check (see .github/workflows/independent-review-pending.yml /
.github/scripts/gitapex_gate_independent_review_pending.py). It stays
pending/failing until a ## Independent review verdict section naming
this PR's current head commit is recorded in this body --
drafting-a-pr-to-merge's own Step 8 records it once its independent
review completes. There is nothing for you to do here now: do not
pre-fill this section yourself, and do not remove this note.

Independent review verdict

  • Verdict: CLEAN
  • Verified commit: 2b121f0

Related Issue

Closes #1858

Design doc elicited via eliciting-a-design dialogue, approved by the
user this session. Formalized into issue #1858 with its own
Acceptance Criteria Map. Addresses 6 open Dependabot PRs blocked by
the independent-review-pending required check, which has no path for
a bot PR to record a human verdict.

Refs #1858

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxcd2iHFzURaCjfH9BXvVr
Task Decomposition per executing-a-branch-plan step 3: 3 tasks (A:
trusted-bots.yml allowlist + CODEOWNERS, B: gate script bot-exemption
branch + tests, C: workflow wiring), no file-ownership conflicts,
C sequenced after B on an interface-dependency edge.

Refs #1858

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxcd2iHFzURaCjfH9BXvVr
An independent pre-PR design-doc-adversarial-review (executing-a-branch-
plan) found bot-identity matching against the PR opener alone (not the
head commit) would let a non-bot commit pushed to a bot-opened PR's
branch silently bypass independent review, since GitHub does not update
the PR's user field on synchronize and dependabot/* branches are not
covered by branch protection. Fix: require the head commit's own
author/committer email to also match main.json's existing
commit_author_email_pattern/committer_email_pattern rules for that bot;
a mismatch falls through to the existing human-verdict path.

Refs #1858

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxcd2iHFzURaCjfH9BXvVr
@tvna
tvna deployed to ruleset-verify September 6, 2026 02:21 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: da00c2a0-500f-41b6-8ab3-8563be791f4d


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.

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.04743% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.56%. Comparing base (1d1dbee) to head (2b121f0).

Files with missing lines Patch % Lines
...scripts/gitapex_gate_independent_review_pending.py 96.04% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1859      +/-   ##
==========================================
- Coverage   99.59%   99.56%   -0.04%     
==========================================
  Files         168      168              
  Lines       27715    27966     +251     
  Branches     3342     3390      +48     
==========================================
+ Hits        27602    27843     +241     
- Misses        113      123      +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Task A of issue #1858's Branch Plan: add .github/trusted-bots.yml
(login/id/type/purpose per trusted bot, seeded with dependabot[bot]'s
already-verified id 49699333 from .github/rulesets/main.json's
commit_author_email_pattern/committer_email_pattern rules, PR #1843),
and add CODEOWNERS coverage for it so an unreviewed edit can't silently
widen who bypasses independent review.

Also fixes tests/test_gitapex_sync_opencode.py::test_unreadable_skill_md_skipped,
found failing pre-existing (reproduced identically with this task's own
changes stashed out) because this repository's container runs the test
suite as uid 0, where chmod(0o000) mode bits are bypassed and the read
simply succeeds. Switched to a monkeypatched read_text raising
PermissionError, mirroring the same fix already applied in
test_gitapex_gate_commit_citation.py's
test_main_commit_msg_an_unreadable_file_exits_two -- required to get
the full verification suite green before this task could complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tvna
tvna deployed to ruleset-verify September 6, 2026 02:37 — with GitHub Actions Active
Extend gitapex_gate_independent_review_pending.py with a bot-path branch,
tried before the existing parse_verdict/check() human-verdict path and
falling through to it whenever the bot-path conditions are not fully met
(never modifying that existing path itself):

- is_trusted_bot: matches a PR author's (login, id, type) against
  .github/trusted-bots.yml on all three fields together -- login alone is
  never sufficient.
- head_commit_identity_matches_bot: the critical-defect fix from this
  issue's own design-doc Revision section. A bot-identity match on the
  PR's opener is not itself sufficient -- the head commit's own
  author/committer email must also match main.json's own
  commit_author_email_pattern/committer_email_pattern rules (PR #1843),
  the single source of truth for trusted-committer emails. A mismatch
  falls through to the human-verdict path rather than failing outright.
- required_check_contexts: reads main.json's required_status_checks rule
  (mirroring, not importing, gitapex_gate_ruleset_required_checks.py's
  own rule_of_type() pattern), excluding this check's own name. Never
  misreads deletion/pull_request/commit_author_email_pattern/
  committer_email_pattern rules as check contexts. Fails closed (an
  empty context list is never treated as "nothing to check").
- poll_bot_required_checks: polls GitHub's Checks API for the head SHA
  until every remaining required context completes; PASS iff all
  conclude success/neutral/skipped, FAIL immediately on any other
  conclusion, FAIL on timeout naming unresolved contexts. Transient API
  errors are retried within the same timeout budget.
- evaluate_bot_path: orchestrates the above; main() takes its result only
  when not None, otherwise falls back completely to the existing
  check(body, head_sha) path -- every --body/--head-sha-only invocation
  is unchanged.

CLI gains --pr-author-login/--pr-author-id/--pr-author-type, --owner/
--repo (defaulting from $GITHUB_REPOSITORY), --trusted-bots-path,
--ruleset-path, --head-commit-author-email/--head-commit-committer-email,
and --poll-timeout-seconds/--poll-interval-seconds, for Task C's workflow
wiring.

Adds direct-name-coverage tests plus the defeat tests this issue's ACM and
the design doc's own Testing section require: forged id with matching
login, an absent required_status_checks rule (fail-closed, not a vacuous
pass), a main.json fixture carrying every non-required_status_checks rule
type (never misread as check contexts), and GitHub API transient-error
retry within the polling budget. The pre-existing 42-test suite for this
file passes unmodified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…1858)

Follow-up to a2b08d3 after uv run --frozen python3
.github/scripts/gitapex_gate_local_preflight.py surfaced five findings
against that commits own diff:

- exception-handler-gap: load_trusted_bots/load_ruleset now catch
  OSError/UnicodeDecodeError at the read_text() boundary itself and
  re-raise as ValueError, matching gitapex_detect_changed_gate_scripts.pys
  own registered_gate_paths pattern instead of leaving that boundary
  unguarded.
- except-fail-open: main()s trusted_bots/ruleset loading no longer
  defaults either to a falsy placeholder and continues -- both loads now
  share one try/except, and evaluate_bot_path is only ever called from
  the success branch (both loads succeeded), so a broken/unreadable
  main.json or trusted-bots.yml skips the bot path entirely rather than
  running it against placeholder data. The two remaining fail-open sites
  (the malformed-regex catch in email pattern matching, and the
  fetch-head-commit-email-failure catch in evaluate_bot_path) are
  genuinely intentional fall-throughs to the existing human-verdict path
  per the design docs own Revision section, each disclosed inline with an
  except-fail-open WAIVED comment naming the reason.
- function-body-test-coverage: added direct-name tests for _rule_of_type,
  _fetch_check_runs, and _latest_run_for_context; the private _sort_key
  closure (no externally callable name) is disclosed inline with a
  function-body-test-coverage WAIVED comment, exercised through
  _latest_run_for_context's own test instead.
- detection-logic-property-coverage: added hypothesis property tests for
  the email-pattern-matching helper to
  tests/test_gitapex_gate_independent_review_pending_properties.py
  (starts_with/ends_with/contains match Pythons own semantics, regex
  matches re.search, unknown operator and empty/non-string pattern always
  fail closed, arbitrary input never raises).
- stdlib-only-claim-drift: removed the literal stale-claim phrase this
  files own docstring still carried after gaining a real yaml/
  _gitapex_github_http import, and switched every Usage example to
  uv run --frozen python3, matching
  gitapex_gate_retro_title_convention_citation.pys own precedent.

uv run --frozen python3 -m pytest --no-cov -q and
.github/scripts/gitapex_gate_local_preflight.py both green after this
commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tvna
tvna deployed to ruleset-verify September 6, 2026 05:05 — with GitHub Actions Active
… workflow (#1858)

Extend .github/workflows/independent-review-pending.yml so the required
check can take the bot-merge-gate path Task B's
gitapex_gate_independent_review_pending.py already implements:

- permissions: add checks:read (workflow- and job-level) for the Checks
  API poll the bot path needs; contents:read is unchanged.
- timeout-minutes: 5 -> 20, headroom over the script's own 900s (15 min)
  default poll timeout so the job's own GitHub Actions timeout never
  races the script's timeout FAIL; costs nothing for human/agent PRs,
  which never reach the polling branch.
- Pass github.event.pull_request.user.{login,id,type} to the script as
  --pr-author-login/--pr-author-id/--pr-author-type via env (never
  interpolated directly into the shell script, matching the existing
  PR_BODY convention), plus GITHUB_TOKEN for the script's own GitHub API
  calls. --owner/--repo are omitted (the script resolves them from
  $GITHUB_REPOSITORY). --head-commit-author-email/
  --head-commit-committer-email are deliberately never passed: they are
  a test-only override in the script, and wiring them here would let a
  forged value bypass the real GitHub-API-fetched commit identity the
  design's own critical-defect fix depends on.

Verified: uv run --frozen python3 -m pytest --no-cov -q (8714 passed)
and uv run --frozen python3 .github/scripts/gitapex_gate_local_preflight.py
(47/47 gates PASS, including independent-review-heading-drift and
main-ruleset-required-checks).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tvna
tvna deployed to ruleset-verify September 6, 2026 05:16 — with GitHub Actions Active
Deduplicate load_trusted_bots/load_ruleset's identical read-boundary
try/except into one shared _read_utf8_or_raise helper, and collapse
three copy-pasted is_trusted_bot negative-match tests and two
copy-pasted head_commit_identity_matches_bot mismatch tests into
pytest.mark.parametrize blocks. Adds three direct tests for the newly
extracted _read_utf8_or_raise (required by this repo's own
function-body-test-coverage gate for a new named function), including
the one UnicodeDecodeError branch that had no prior coverage even
before extraction. No production behavior changed: full pytest suite
and gitapex_gate_local_preflight.py both green before and after, same
pass/fail split for every pre-existing test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tvna
tvna deployed to ruleset-verify September 6, 2026 05:35 — with GitHub Actions Active
…rough (issue #1858)

evaluate_bot_path's own `except GitHubApiError: ... return None` branch
(guarding the head-commit email fetch) was confirmed correct only by
code reading -- Codecov reported it as never actually exercised at
runtime. Add a direct test: no head-commit email override given (forcing
the fetch), an injected opener that makes fetch_head_commit_emails raise
GitHubApiError, and an assertion that evaluate_bot_path falls through to
the strict human-verdict path (returns None) with the expected stderr
note, per the design doc's own Revision section and this exact except
clause's own except-fail-open WAIVED comment. No production logic
changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tvna
tvna deployed to ruleset-verify September 6, 2026 05:43 — with GitHub Actions Active
@tvna tvna removed the branch-plan-executing label Sep 6, 2026 — with Claude
@tvna
tvna marked this pull request as ready for review September 6, 2026 05:44
…sue #1858)

drafting-a-pr-to-merge Step 8's own independent review (five parallel
axis reviewers against the implemented PR) found two CONFIRMED issues,
both verified directly against the actual workflow/CODEOWNERS/gate-script
files rather than accepted on the reviewers' own say-so:

- load_trusted_bots/load_ruleset read .github/trusted-bots.yml and
  .github/rulesets/main.json off local disk, which for this
  pull_request-triggered workflow is the PR's own merge-ref checkout --
  a PR could in principle widen its own bot-exemption eligibility by
  editing either file within its own diff. main.json additionally had no
  CODEOWNERS protection at all (only trusted-bots.yml did), despite being
  equally load-bearing for the critical-defect fix
  (head_commit_identity_matches_bot). Fix: main() now accepts
  --trust-anchor-ref (the workflow passes
  github.event.pull_request.base.sha); when given, both files are fetched
  via the GitHub Contents API (fetch_repo_file_at_ref) from that ref
  instead of local disk. main.json now also carries the same @tvna
  CODEOWNERS entry trusted-bots.yml already had.

- poll_bot_required_checks cached a required context as permanently
  "concluded" the first time it observed a passing conclusion, and never
  looked at it again for the rest of the same poll call -- so a manual
  re-run into a worse conclusion, while the poll session was still open
  waiting on another context, would be silently missed. Every context's
  own conclusion is now re-derived from the latest full check-runs
  snapshot on every iteration.

Design doc updated with a "Second revision" section recording both
findings and their fixes. New tests cover fetch_repo_file_at_ref,
main()'s --trust-anchor-ref wiring (success and every fallback path),
the re-run-after-passing defeat test, and direct coverage for the two
newly-extracted _parse_trusted_bots/_parse_ruleset helpers
(function-body-test-coverage's own requirement).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxcd2iHFzURaCjfH9BXvVr
@tvna
tvna deployed to ruleset-verify September 6, 2026 06:34 — with GitHub Actions Active
… branch (issue #1858)

A second Step 8 independent-review round, run specifically against the
previous trust-anchor-ref-pinning fix, found two more issues:

- base.sha's own "immutable, no PR ref can move it" premise silently
  assumed the PR's base.ref stays this repository's real default branch.
  Since `edited` is one of this workflow's own trigger types, a PR could
  retarget its own base to a different, possibly branch-protection-free
  branch carrying forged trusted-bots.yml/main.json content, and the next
  re-run would fetch trust-anchor content from that forged base instead.
  Fix: main() now also requires --trust-anchor-base-ref
  (github.event.pull_request.base.ref) to equal --repo-default-branch
  (github.event.repository.default_branch -- the repository object's own
  field, never the PR's) before trusting --trust-anchor-ref at all; any
  mismatch, or either value unresolved, refuses the bot path the same way
  every other bot-path failure already does.

- An explicitly-given-but-empty --trust-anchor-ref was treated identically
  to the flag being omitted -- silently falling back to the local-disk
  read this whole fix exists to stop trusting, with no warning printed at
  all. Fix: an empty value is now refused with its own explicit error; the
  silent local-disk fallback is reserved for the flag being omitted
  entirely.

Design doc's "Second revision" section extended with a third item
recording both findings and their fixes. New tests cover the base-ref
mismatch (the core defeat test), missing base-ref/default-branch, and the
empty-string case; existing --trust-anchor-ref tests updated to pass a
matching base-ref/default-branch pair.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxcd2iHFzURaCjfH9BXvVr
@tvna
tvna deployed to ruleset-verify September 6, 2026 06:46 — with GitHub Actions Active
@tvna
tvna marked this pull request as draft September 6, 2026 06:55
…ocstrings (issue #1858)

The design doc and the gate script's own docstrings/comments had
accumulated "Revision"/"Second revision"/"a Step 8 review found..."
narration documenting every review round that led to the current design,
rather than just describing the current design itself. Consolidated the
design doc's Revision/Second revision sections into the current
Architecture/Decision logic detail/Residual risks content (351 -> 263
lines), and trimmed the equivalent narration out of the gate script's
module docstring, function docstrings, CLI help text, and inline
comments (now cross-referencing the design doc's plain "Decision logic
detail" section instead of a since-removed "second revision" heading).

No behavior change -- docs/comments only. Full suite still 8735 passed;
mypy/ruff clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qxcd2iHFzURaCjfH9BXvVr
@tvna
tvna deployed to ruleset-verify September 6, 2026 07:38 — with GitHub Actions Active
@tvna
tvna marked this pull request as ready for review September 6, 2026 07:43
@tvna
tvna merged commit 0f83cc8 into main Sep 6, 2026
43 of 46 checks passed
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.

feat(ci): exempt trusted bots from independent-review-pending's human-verdict requirement

2 participants