feat(pr-status): name the unsigned commit instead of saying investigate - #142
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
8c26361 to
3a7be0b
Compare
Review (Copilot check failed, so this one is by hand)The Three limitations the change does not handle, and only one of them is defensible
Signature enforcement outside rulesets. The verdict is gated on
What I checked and found sound
Evidence status, stated plainlyThe positive branch has never run against real data. Both live PRs it was tested on report |
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>
3a7be0b to
c3b7997
Compare
|



The gap
A
required_signaturesruleset is shut by one commit anywhere on the branch that carries no valid signature. GitHub surfaces that only asmergeStateStatus: BLOCKED— no failing check, no entry in the status rollup, nothing in the review or thread state. Every signalpr-statuslooks at is green, so it fell through to:Which is honest, and useless.
Where it came from
Eight PRs in today's dependency campaign sat at
BLOCKEDwith everything green. Diagnosing it by hand meant reading thepull_requestruleset parameters (zero approvals required, threads resolved — not it), diffing the 51 required contexts against the 65 present ones (all present — not it), and finally callingGET /repos/{repo}/commits/{sha}per commit, which showedverified: false, reason: unsignedon the first commit of each branch.The tool now answers that in its first run.
The change
allCommits: commits(first:100){ nodes{ commit{ oid signature{ isValid } } } }— one extra field on a query that already runs.unsignedis exposed in--json.fix-signaturesfires when unsigned commits exist and therequired_signaturesruleset is active, naming the offending short SHAs and stating that GitHub reports this only asBLOCKED.The suggested command re-signs every commit on the branch:
Amending only
HEADleaves 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
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 withisValid: true, one with no signature object at all: the extraction returns the two unsigned ones, the verdict readsfix-signatures — 2 commit(s) … only as mergeStateStatus BLOCKEDwhile the ruleset is active, and falls back toinvestigatewhen 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.gpgsignis 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.