Skip to content

feat(pr-status): name the unsigned commit instead of saying investigate - #142

Merged
CybotTM merged 1 commit into
mainfrom
feat/name-the-unsigned-commit
Aug 6, 2026
Merged

feat(pr-status): name the unsigned commit instead of saying investigate#142
CybotTM merged 1 commit into
mainfrom
feat/name-the-unsigned-commit

Conversation

@CybotTM

@CybotTM CybotTM commented Aug 5, 2026

Copy link
Copy Markdown
Member

The gap

A required_signatures ruleset is shut by one commit anywhere on the branch that carries no valid signature. GitHub surfaces that only as mergeStateStatus: BLOCKED — no failing check, no entry in the status rollup, nothing in the review or thread state. Every signal pr-status looks at is green, so it fell through to:

NEXT: investigate — mergeState=BLOCKED with no failing check, no open thread
and no missing review — check branch protection manually

Which is honest, and useless.

Where it came from

Eight PRs in today's dependency campaign sat at BLOCKED with everything green. Diagnosing it by hand meant reading the pull_request ruleset parameters (zero approvals required, threads resolved — not it), diffing the 51 required contexts against the 65 present ones (all present — not it), and finally calling GET /repos/{repo}/commits/{sha} per commit, which showed verified: false, reason: unsigned on the first commit of each branch.

The tool now answers that in its first run.

The change

  • The GraphQL query gains allCommits: commits(first:100){ nodes{ commit{ oid signature{ isValid } } } } — one extra field on a query that already runs.
  • unsigned is exposed in --json.
  • A new verdict fix-signatures fires when unsigned commits exist and the required_signatures ruleset is active, naming the offending short SHAs and stating that GitHub reports this only as BLOCKED.
  • It sits last in the chain, because it only matters once the visible gates are satisfied.

The suggested command re-signs every commit on the branch:

git rebase --exec "git commit --amend --no-edit -S" $(git merge-base HEAD origin/<base>) ; git push --force-with-lease

Amending only HEAD leaves an unsigned parent and the gate stays shut — that is exactly how the campaign PRs got into this state, since a later amend signed the tip while the parent stayed unsigned.

Verification

$ bash -n … && shellcheck …            Syntax ok / clean
$ pr-status.sh … t3x-nr-image-sitemap 42 --json | jq -c '{unsigned, next:.next.action}'
{"unsigned":[],"next":"triage-ci"}
$ pr-status.sh … t3x-nr-temporal-cache 71 --json | jq -c '{unsigned, next:.next.action}'
{"unsigned":[],"next":"rebase"}

Both branches were re-signed a few minutes before this PR, so the field correctly reports an empty list and the verdict falls through unchanged.

The positive branch is shown on synthetic input, which is weaker evidence and worth stating plainly — there is no longer an unsigned PR in the org to point it at. Three fixture commits, one with isValid: false, one with isValid: true, one with no signature object at all: the extraction returns the two unsigned ones, the verdict reads fix-signatures — 2 commit(s) … only as mergeStateStatus BLOCKED while the ruleset is active, and falls back to investigate when it is not.

The missing-signature-object case is covered by (.signature.isValid // false), since such a commit returns null rather than false.

One thing this does not do

It does not explain why those commits were unsigned. commit.gpgsign is true globally here and a probe repository signs immediately, so the batch scripts that produced them should have signed too. I have not established the cause and am not guessing at one in the code.

Copilot AI lite review requested due to automatic review settings August 5, 2026 20:09
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CybotTM

CybotTM commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Review (Copilot check failed, so this one is by hand)

The copilot-pull-request-reviewer check run on this head ended in failure — not a review with findings, an aborted job. The standing rule is that a missing bot review gets done by hand, so this is a review of my own change, written to find fault with it.

Three limitations the change does not handle, and only one of them is defensible

commits(first:100). A pull request with more than 100 commits would have its tail silently unexamined, and the verdict would report "0 unsigned" while an unsigned commit sat at position 101. That is a wrong answer, not a missing one, which is worse. In this repository's practice a PR that long does not occur, but the code does not say so and does not paginate. Acceptable only because the failure mode is bounded and visible in the field: unsigned would be empty while the PR stays BLOCKED, landing back on investigate.

Signature enforcement outside rulesets. The verdict is gated on required_signatures appearing in the rules endpoint. A repository that enforces signatures through classic branch protection does not report it there, so the tool would see unsigned commits, know they are unsigned, and still answer investigate. The gate could be dropped — reporting unsigned commits is useful regardless — but then a repo that permits unsigned commits would get a misleading verdict. I kept the gate; it is the conservative error.

isValid: false is not the same as unsigned. GitHub returns false both for an absent signature and for a signature it cannot verify — an unregistered key, an expired one. The message says "no valid signature", which covers both, but the suggested remedy (re-sign) only fixes the first. Someone whose key is simply not registered on their account will re-sign and see no change.

What I checked and found sound

  • (.signature.isValid // false) handles the null case: a commit with no signature object returns null, not false, and would otherwise pass the filter.
  • The verdict sits last, after every visible gate, so it cannot mask a failing check or an open thread.
  • The suggested command amends every commit from the merge-base, not just the tip. That distinction is the whole point — a tip-only amend is how the campaign branches ended up with a signed head and an unsigned parent.
  • No single quotes inside the jq program; the earlier version had them and shellcheck caught it (SC2026) before the parse broke further down.

Evidence status, stated plainly

The positive branch has never run against real data. Both live PRs it was tested on report unsigned: [] because they were re-signed minutes earlier, and there is no unsigned PR left in the org to point it at. The branch condition and the extraction are shown on synthetic input in the PR body. That is weaker than an observation and should not be described as verified.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CybotTM
CybotTM requested a lite review from Copilot August 6, 2026 01:35
A required_signatures ruleset is shut by one commit anywhere on the
branch that carries no valid signature. GitHub surfaces that only as
mergeStateStatus BLOCKED: no failing check, no rollup entry, nothing in
the review or thread state. Every visible signal is green, so the tool
fell through to investigate and left the reader to guess.

Observed today across eight PRs in this campaign, all reporting BLOCKED
with everything green. Diagnosing it by hand took a walk through the
ruleset parameters, a diff of required contexts against present ones,
and finally a per-commit verification call. The tool now answers it in
the first run.

The check is last in the chain on purpose: it only matters once the
visible gates are satisfied, and it costs one extra field on a query
that already runs.

The suggested command re-signs every commit on the branch, not just the
tip — amending only HEAD leaves an unsigned parent and the gate stays
shut.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@CybotTM
CybotTM force-pushed the feat/name-the-unsigned-commit branch from 3a7be0b to c3b7997 Compare August 6, 2026 01:37
@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CybotTM
CybotTM merged commit 2fb9f95 into main Aug 6, 2026
21 checks passed
@CybotTM
CybotTM deleted the feat/name-the-unsigned-commit branch August 6, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants