mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #140] [MERGED] fix(pr-risk): a repo-owned GitHub App is a runbook candidate, not external (BE-6715) #150
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#150
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/140
Author: @mattmillerai
Created: 8/5/2026
Status: ✅ Merged
Merged: 8/6/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-6715-pr-risk-bot-provenance📝 Commits (3)
f73860bfix(pr-risk): a repo-owned GitHub App is a runbook candidate, notexternal(BE-6715)22663c6fix(pr-risk): a label must not route a bot past the runbook registry (BE-6715)adfc838fix(pr-risk): restore an App's[bot]suffix in the grader, not in every registry (BE-6715)📊 Changes
7 files changed (+335 additions, -18 deletions)
View changed files
📝
.github/workflows/pr-risk.yml(+7 -2)📝
docs/callers/pr-risk.md(+2 -2)📝
scripts/pr-risk/README.md(+18 -4)📝
scripts/pr-risk/grade-pr-risk.sh(+94 -8)📝
scripts/pr-risk/risk-map.v0.json(+1 -1)📝
scripts/pr-risk/runbook-registry.v0.json(+9 -1)📝
scripts/pr-risk/tests/test_grade_pr_risk.sh(+204 -0)📄 Description
ELI-5
The risk grader gave every robot-opened PR the scariest score (R3) no matter what the PR actually changed. That is because GitHub says a robot is "NONE" when asked how it relates to the repo — the same answer it gives a total stranger — and the grader treated "NONE" as "stranger from outside". So the label stopped telling you anything about the diff, and worse, it trained people to ignore R3. This teaches the grader to check "is this from a fork?" first (that guard is untouched and still absolute), and only then treat "NONE" as stranger-ish — and only for authors that are not robots. A robot with no registered runbook still gets no trust; it just gets graded on its diff instead of on its account type.
What changed
grade-pr-risk.sh, provenance axis ($base_class):is_fork == true→external, unconditionally, tested first. Unchanged and load-bearing: a bot opening a PR from a fork is stillexternalR3.author_association IN (FIRST_TIME_CONTRIBUTOR, FIRST_TIMER, NONE)→externalonly for authors not classified as a bot. The first-time-contributor guard is untouched for humans.runbook-candidatepath, where the registry entry's identity + shape assertion decide. A candidate with no asserting entry still resolves tohuman— identity alone buys no trust, as before.unknownhandling is unchanged: a non-okprovenance_statusorlabels_statusstill refuses to grade the axis.The ordering constraint from the ticket is enforced by construction and pinned by tests: the bot test cannot run before the fork test.
One thing the ticket did not anticipate, and why the diff is bigger than "narrow the
or"The plan says to key on "authors that are known bots (
[bot]suffix or thebot_loginsinput)". That is not sufficient on the live path, and I found it by grading the real PR rather than by reasoning about it. GraphQL reports a Bot actor's login without the[bot]suffix. The API read that backs--prreturnsauthor.login = "cloud-code-bot"where REST and the web UI both showcloud-code-bot[bot], so the resolver's suffix test can only ever fire on a REST-shaped or offline-corpus record — never on a CI grade. With the narrowing alone, the named PR still gradedexternalR3 (verified before adding this).So the record now also carries
author_is_bot, fromauthor.__typename == "Bot"— GitHub's own actor resolution for that account, in the same spirit as "the author login is the resolution GitHub itself made", and not forgeable by the PR being graded. It is ORed into the bot test rather than folded intoclassify_login, because that jq is shared verbatim with the fleet's offline collector and a field only this collector emits would fork the shared definition. Additive:schema_version3 → 4, read as// false, so an existing schema-3 corpus record grades exactly as it did.Corollary, revised after review. The first cut told consumers to list both login forms. The panel pointed out that a bare slug is matchable by a same-named human, who would then inherit the runbook — so the grader now restores the suffix itself: when
author_is_botsaysBotand the login carries none, it matches againstxandx[bot]. One direction only (synthesized, never stripped), so an App reaches an"x[bot]"entry and a user namedxcannot. Registry advice therefore inverts, in the same three places: name an App by its suffixed login alone. Bare slugs still match literally — machine USER accounts need that — with the impersonation caveat stated.Chesterton's Fence
This changes an existing guard, so:
git log -Lon those lines shows theexternalbranch has one origin, #111 (feat(pr-risk): advisory PR risk grader…), where it landed as part of the initial grader with the intent recorded in the map comment — "an unreviewed outside diff is the one provenance class never routed unattended". The intent is about outsiders, and a repo-owned App is not one; theNONEstring was standing in for "outsider" and turned out to over-match. The half of the rule that actually implements the intent — the fork flag, which is what makes a diff outside — is untouched and now runs first. The map comment, the README, and the loader's own quoted invariant are all updated in the same commit so the promise and the code still agree.Verification
Repo suites (all green, run from
scripts/pr-risk/), matchingtest-pr-risk.ymljob for job:shellcheck -xover all 11 scripts — clean87 passed, 0 failed(was 63; +24 across two new phases)48/0, targets105/0, pin-contract20/0, publish-surfaces67/0, resolve-enabled27/0check_agents_md.py --root .— cleanNew hermetic cases — phase 21 (
--stdin, the corpus/REST record shape) and phase 22 (the--prpath with a stubbedghwhose fixture mirrors the real GraphQL response,{"login":"cloud-code-bot","__typename":"Bot"}). Both phases cover all four acceptance cases, plus a registered App whose diff shape fails, a--bot-loginsmachine user, and an entry that lists only the suffixed login.Live regression check, real API: PRs #132/#98/#93 in this repo grade byte-identical before and after.
Acceptance criteria
author_association: NONEclassifiesrunbook-candidateand gradesrunbook/R0 when a registry entry asserts.external→ R3 (both record shapes).NONEstill classifiesexternal→ R3.human, notrunbook.--stdinsuite (and to the stubbed live path), run bytest-pr-risk.yml.provenance: runbook, iddata-snapshot-refresh, R0 on that axis (before:external, R3). Two deviations. (a) That repo has no.github/risk-runbooks.jsontoday — a 404 — so I verified with an equivalent registry passed via--runbooks; the consumer still has to commit the file (listing both login forms) for this to take effect there. (b) The grade lands R2, decided by reversibility, not R1 from the path floor: that PR's check rollup isFAILURE, so reversibility is pinned R2 by "no GREEN check rollup", and its single path matches no rule in the default map, so the path floor is R0, not R1. The AC's R1 presumably assumed a consumer.github/risk.jsonthat also does not exist yet. Either way the grade now responds to the diff and the rollup instead of to the author's account type, which is the point.Judgment calls
Botactor type, because criteria 1 and 6 are unreachable without it (or without every consumer hand-listing each App inbot_logins, which is the same "consumer cannot fix it" complaint the ticket opens with). Flagging it explicitly since it is scope the ticket did not ask for.Did not touch the runbook identity matcher— reversed after review. The original call was that normalizing[bot]suffixes would broaden a trust check, because a human could register the plain username and match an App's entry. That is true of two-sided normalization; it is not true of the one-way, actor-type-gated synthesis that shipped instead (see the corollary above), which only ever adds the suffix and only when GitHub itself types the authorBot. Pinned by a test that grades the same fixture as aUserand asserts it does not inherit the runbook.agent-codedlabel / a bot login infleet_loginsnow resolve torunbook-candidate, notagent-supervised. Corrected after review (CodeRabbit): narrowing the association half made that branch reachable for an App for the first time — before, every App PR was pinnedexternaltwo branches earlier and never reached it, so "pre-existing ordering" was the wrong read. A registered producer was never affected either way (the shape assertion resolves ahead of the base class) and the default map tiersagent-supervisedandhumanboth R1, so no shipped grade moves; what it closes is a consumer map that tiers the two apart, where anagent-codedlabel alone would have bought a bot R0 with no registry entry behind it. Four regression cases in phase 21, two of which fail on the old ordering.dependabot-preview[bot]from the default registry (also CodeRabbit): the Dependabot Preview service shut down in Aug 2021, and the dead login is suffix-only — exactly what this PR's new "list BOTH login forms" note says never asserts on a live grade.pr-risk.yml(one input description) adds no extra fleet churn:bump-pr-risk-callers.ymlalready triggers onscripts/pr-risk/**.Provenance
Ticket BE-6715. Negative-claim falsification does not apply — this diff grants a capability (bot PRs can reach
runbook/humaninstead of being pinned R3) rather than denying one; no deny/dead-end path or capability-denying string was added. The behavior was nevertheless confirmed empirically against the live API, not just against tests I wrote.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.