Skip to content

fix(verify): report unverified commits under required_signatures - #129

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/required-signatures-unverified-commits
Oct 9, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
fix/required-signatures-unverified-commits

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

squabble verify-satisfied now reports a PR as not done when the base branch requires signed commits and any commit on the PR lacks a verified signature.

Why. ACCOUNTED_RULE_TYPES listed required_signatures among the rule types that cannot hold a squash merge, so a PR carrying an unsigned commit came back DONE. GitHub holds such a PR BLOCKED, squash armed or not. Measured 2026-10-02 on boj-server-cartridges#155: automerge was armed, and the PR was blocked by one unsigned coderabbitai[bot] autofix commit (8546edbde6). The same gap was logged on nextgen-databases#107, panll#136 and project-wharf#99 (dev-notes inbox/findings.md, 2026-10-02; VERIFICATION.md row P0-004).

Closes: no issue. The open issues #117, #118 and #121 do not cover signatures; the finding lives in the owner's findings ledger.

Changes

  • squabble-core (done.rs):
    • New UnverifiedCommit { oid, state, author } and PrFacts::unverified_commits. The field is #[serde(default)], so older JSON still reads.
    • New agent item unverified_commits, raised only when rule_types contains required_signatures and the list is non-empty. It names each commit with its GitSignatureState, or UNSIGNED when there is no signature at all, plus the author and the re-sign recipe (git rebase --exec 'git commit --amend --no-edit -S' <merge-base>, then --force-with-lease). The re-sign keeps the bot as author and makes the person re-signing the committer.
    • A merged or closed PR owes nothing, as before.
    • ACCOUNTED_RULE_TYPES keeps required_signatures; its doc now says the rule is evaluated over unverified_commits rather than met by construction.
  • squabble-forge (pr_done.graphql, pr_done.rs):
    • A third paged connection, prCommits: commits(first: 100, after: $cmtAfter), reads oid, the author and signature { isValid state } for every commit.
    • It is aliased because the query already reads commits(last: 1) for the head rollup.
    • Paging shares the existing MAX_PAGES bound and its error.
    • A commit counts as verified only when isValid == true.
  • squabble-cli (fetch.rs): base_gate also reads classic protection's required_signatures.enabled. The rules API reports ruleset rules only, so a classic toggle was invisible. It is de-duplicated against the ruleset type.
  • CHANGELOG.adoc: a Fixed entry under Unreleased.

📌 New pins

  • Head SHA: a7656d493bcfdbedc0c935431fa59d9d9454f6db
  • No new or changed pins. No uses:, actions.lock, Cargo.lock or container digest change; no new crate dependency.

RSR Quality Checklist

Required

  • Tests pass. These are the two commands of the just test recipe, run directly because cargo on PATH here resolves to a mise shim that refuses this checkout:
    • cargo test --workspace: 60 + 127 + 64 + 35 passed, 0 failed;
    • cargo test -p squabble-cli --features boj: 64 passed, 0 failed.
  • Code is formatted: cargo fmt --all -- --check, clean. The just fmt-check recipe is still a TODO stub, so this goes beyond what just quality checks.
  • Linter is clean: cargo clippy --workspace --all-targets -- -D warnings and cargo clippy -p squabble-cli --features boj --all-targets -- -D warnings, both exit 0. The just lint recipe is also a stub.
  • No banned language patterns: Rust and GraphQL only.
  • No unsafe blocks: none added (git diff origin/main | grep -cE '^\+.*\bunsafe\b' → 0).
  • No banned functions: none.
  • SPDX headers present: no new files. All four modified source files keep their MPL-2.0 header.
  • No secrets, credentials or .env files.

As Applicable

  • STATE.a2ml / ECOSYSTEM.a2ml / META.a2ml: not updated. A2ML is retired (D99, D269c), and no project state, integration or architecture decision changed.
  • Documentation updated: the ACCOUNTED_RULE_TYPES, PrFacts and base_gate docs, and the describe() text that the CLI prints.
  • TOPOLOGY.md: the repo has none, and the architecture did not change.
  • CHANGELOG updated.
  • New dependencies: none.
  • ABI/FFI changes: none.

Testing

Unit and fixture, all on this head:

  • Planted positive: an_unverified_commit_under_required_signatures_is_agent_work.
    • Mutant kill: replacing the rule's condition with if false failed exactly this test (126 passed, 1 failed). The file was restored afterwards.
  • Negatives:
    • every commit verified;
    • the rule absent;
    • a merged PR.
  • Forge: unverified_commits_are_read_across_pages_and_verified_ones_are_not_reported.
    • Two pages: one VALID commit, one coderabbitai[bot] commit with signature: null, and one isValid: false, UNKNOWN_KEY commit with no linked user.
    • It asserts the cursor is forwarded and the request count is 2.
    • Plus an all-verified page.
  • PR_DONE validates against the committed GitHub schema snapshot (pr_done.rs test, via crate::tests::validate), and validator_actually_rejects_a_field_the_schema_lacks shows that validator can fail.
  • Classic: classic_required_signatures_is_read_only_when_enabled covers enabled true and false, the key absent, and invalid JSON (an error).

Live, 2026-10-09 12:22–12:31Z. The parts are verified separately, not end to end:

  • The query shape on real data. The committed pr_done.graphql was run against boj-server-cartridges#155 (merged). It returned pageInfo and signature: null on 8546edbde6 by coderabbitai[bot], the same shape as the fixture.
  • A live negative through the CLI. squabble verify-satisfied hyperpolymath/boj-server 361 returned rc=5 with only unresolved_thread.
    • That base's effective rules include required_signatures, and all its commits are VALID, so no unverified_commits was raised.
    • The rc=5 is an unrelated open review thread.
  • The classic payload shape. branches/main/protection on metadatastician/sim-public-relations returned {"enabled":true,"url":…}; on metadatastician/phi-LAM, {"enabled":false,…}. Same shape as the fixtures.
    • A GraphQL enumeration of branchProtectionRules over all 465 repos of both owners found classic protection on 4 repos, 2 of which require signatures. Every other repo uses rulesets only.
  • Horizon: a live positive through the CLI was not possible. At 12:22Z the 41 open PRs across both owners (407 + 58 repos, cross-checked by per-repo pullRequests(states: OPEN) counts) included no PR with an unverified commit. The #155 positive is merged, and verify-satisfied does not read the base gate for a merged PR, by design. No PR was planted to manufacture a positive.

Screenshots

$ squabble verify-satisfied hyperpolymath/boj-server 361   # rules: deletion,non_fast_forward,required_signatures,required_status_checks
rc=5  agent: ["unresolved_thread"]   # no unverified_commits: all commits VALID

Merge: squash.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wo32J8Ym7XpPr9EYdBCgVB

`verify-satisfied` listed `required_signatures` among the rule types that
cannot hold a squash merge, so a PR carrying an unsigned commit read as
done. GitHub holds such a PR BLOCKED, squash armed or not: measured
2026-10-02 on boj-server-cartridges#155, armed and blocked by one unsigned
coderabbitai[bot] autofix commit.

- squabble-core: new agent item `unverified_commits`, raised when the base
  requires signatures and any PR commit lacks a verified signature. The
  describe line names each commit, its GitSignatureState (or UNSIGNED) and
  author, and the re-sign recipe. A merged or closed PR owes nothing.
- squabble-forge: pr_done.graphql reads every PR commit's signature through
  a third paged connection (`prCommits`, 100 per page, MAX_PAGES bound).
- squabble-cli: base_gate also reads classic protection's
  `required_signatures.enabled`; the rules API reports ruleset rules only.

Tests: a planted positive (killed by an `if false` mutant of the rule),
three negatives, paging across two commit pages with an absent and an
invalid signature, and the classic toggle on/off/absent/invalid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wo32J8Ym7XpPr9EYdBCgVB
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: f59e72df-e83f-40c2-adcc-480454b5c5f1

📥 Commits

Reviewing files that changed from the base of the PR and between 651ec38 and a7656d4.


📒 Files selected for processing (5)
  • CHANGELOG.adoc
  • crates/squabble-cli/src/fetch.rs
  • crates/squabble-core/src/done.rs
  • crates/squabble-forge/graphql/pr_done.graphql
  • crates/squabble-forge/src/pr_done.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📜 Recent review details
⏰ Context from checks skipped due to timeout. (23)
  • GitHub Check: Dogfooding compliance summary
  • GitHub Check: rust-ci / Cargo check + clippy + fmt
  • GitHub Check: semgrep-cloud-platform/scan
  • GitHub Check: hypatia / Hypatia Neurosymbolic Analysis
  • GitHub Check: governance / Licence consistency
  • GitHub Check: governance / Exemption ratchet
  • GitHub Check: governance / Trusted-base reduction policy
  • GitHub Check: governance / Debt ratchet
  • GitHub Check: governance / Code quality + docs
  • GitHub Check: governance / Language / package anti-pattern policy
  • GitHub Check: governance / Guix packaging policy (Nix retired)
  • GitHub Check: governance / Allowlist Preflight
  • GitHub Check: governance / Workflow security linter
  • GitHub Check: governance / Check Workflow Staleness
  • GitHub Check: governance / Security policy checks
  • GitHub Check: scan / gitleaks
  • GitHub Check: rust-ci / Detect Cargo.toml
  • GitHub Check: Hypatia neurosymbolic scan
  • GitHub Check: panic-attack assail
  • GitHub Check: Validate K9 contracts
  • GitHub Check: openssf-compliance
  • GitHub Check: analyze (actions, none)
  • GitHub Check: squabble action installs only the pinned, attested binary

🧰 Additional context used
📚 Code guidelines (1)
.github/copilot-instructions.md — auto-discovered

📓 Path-based instructions (1)
Source excerpt: SPDX: `MPL-2.0` on all new files.

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • crates/squabble-forge/graphql/pr_done.graphql
  • CHANGELOG.adoc
  • crates/squabble-cli/src/fetch.rs
  • crates/squabble-core/src/done.rs
  • crates/squabble-forge/src/pr_done.rs



🔇 Additional comments (1)
crates/squabble-forge/graphql/pr_done.graphql (1)

39-39: 🗄️ Data Integrity & Integration

The concern depends on a GitHub GraphQL connection limit that is not established by the supplied evidence. The reported reproducer does not establish that the limit applies to the current GitHub API behaviour, so the proposed failure-closed change is not supported.





📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Pull requests subject to required signed commits are now reported when they contain unsigned or unverified commits, including each affected commit’s signature status and author.
    • Signature checks now account for both classic branch protection and ruleset requirements.
    • All pull request commits are checked across pages; incomplete results are not returned if pagination exceeds the limit.

Walkthrough

verify-satisfied now recognises classic branch-protection signature requirements, reads PR commit signatures with pagination, and reports unverified commits when signature requirements apply to an open PR.

Changes

Commit signature checks

Layer / File(s) Summary
Detect classic signature requirements
crates/squabble-cli/src/fetch.rs
Classic protection parsing reads the optional required_signatures.enabled setting. base_gate adds required_signatures when classic protection is protected and signing is enabled.
Fetch paginated commit signatures
crates/squabble-forge/graphql/pr_done.graphql, crates/squabble-forge/src/pr_done.rs
The GraphQL query returns commit author and signature data. fetch_pr_done paginates the commit connection, omits valid signatures, and records commits with absent or invalid signatures.
Report unverified commits
crates/squabble-core/src/done.rs, CHANGELOG.adoc
PrFacts carries unverified commits. For open PRs, evaluation adds an agent item when required_signatures applies and the list is non-empty. The item reports commit details and re-signing instructions.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant verify_satisfied
  participant fetch_pr_done
  participant GitHub_GraphQL
  participant evaluate
  verify_satisfied->>fetch_pr_done: Fetch pull request facts
  fetch_pr_done->>GitHub_GraphQL: Request PR commit page
  GitHub_GraphQL-->>fetch_pr_done: Return commit signatures and page cursor
  fetch_pr_done->>GitHub_GraphQL: Request next page when a cursor remains
  fetch_pr_done-->>verify_satisfied: Return PrFacts with unverified commits
  verify_satisfied->>evaluate: Evaluate PR facts and required rule types
Loading

Merge Risk

Merge Risk: ⚪ Minimal · up to a7656

The signature check is mergeable after normal checks; no actionable issue remains established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to a7656

This strengthens signed-commit completion reporting without granting merge or signing authority. Completeness during concurrent PR updates and compatibility with externally saved facts remain unproven.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is the completion report and exit status for the selected repository and PR, including consumers of that report. The inspected path does not grant merge authority, modify protection, or execute signing operations.

Trust Boundaries and Controls

  • observed — The live CLI takes required-signature authority from base-branch protection and rulesets, not commit authors or PR description text. Hidden classic protection remains an error, preventing evaluation with partially visible requirements.
  • observed — Commit author, state, and OID values become diagnostic text and serialized verdict data. The re-signing recipe is literal guidance, not an executed command, so these fields do not cross a command-execution boundary in the inspected consumer.

Resilience and Maintainability Implications

  • observed — Transport failures, GraphQL errors, malformed connection metadata, and exhausted page limits prevent successful facts from escaping. Progress is local to the call; interruption exposes no partial verdict, and a new attempt starts with fresh cursors.
  • inferred — The new signature pages inherit an existing consistency limitation: scalar identity comes from the first response, and later headRefOid values are not compared. Repository evidence does not establish GitHub snapshot guarantees during concurrent pushes. This is an unresolved reporting guarantee, not a verified merge-control bypass or demonstrated worsening against the base.

Hardening Proposals

  • proposed — Bind paged evidence to a stable head identity and reject or restart on head changes, unless a documented API snapshot guarantee establishes equivalent consistency.



🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 3 files. (2 skipped: 2 …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title is concise, specific, and accurately describes the main change: reporting unverified commits under the required_signatures rule.
Description check Passed The description is complete and relevant. It covers the motivation, implementation changes, testing, live verification limits, checklist status, and changelog update.


✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

A rabbit checks each commit’s mark,
And hops through pages in the dark.
Unsigned ones get a clear report,
With names and states in brief retort.
Re-sign, then push; the checks restart.

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

@sonarqubecloud

sonarqubecloud Bot commented Oct 9, 2026

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit db1d146 into main Oct 9, 2026
53 checks passed
@hyperpolymath
hyperpolymath deleted the fix/required-signatures-unverified-commits branch October 9, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant