mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #100] [CLOSED] feat(pr-risk): shadow-mode risk grade — risk:R0..R3 label, Check Run, sticky comment (BE-5508) #110
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#110
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/100
Author: @mattmillerai
Created: 7/31/2026
Status: ❌ Closed
Base:
main← Head:matt/be-5508-pr-risk-shadow📝 Commits (6)
55708dafeat(pr-risk): shadow-mode risk grade — risk:R0..R3 label, Check Run, sticky comment (BE-5508)3eb48f7fix(pr-risk): harden the grader and publisher against the review panel findings (BE-5508)8d32d8dfix(pr-risk): second review round — sandbox the fallback renderer, validate the label, bound the comment (BE-5508)ecee79brefactor(pr-risk): single-source the sticky-comment footer and make the size bound unconditional (BE-5508)f8f342efix(pr-risk): third review round — pin the sticky comment's author, isolate the three publications, catch a retarget (BE-5508)32ec26afix(pr-risk): fourth review round — wrap every API failure, refuse a labelless graded report, harden the dispute tick (BE-5508)📊 Changes
10 files changed (+4134 additions, -0 deletions)
View changed files
➕
.github/pr-risk/README.md(+185 -0)➕
.github/pr-risk/grade-risk.sh(+235 -0)➕
.github/pr-risk/grade_risk.py(+686 -0)➕
.github/pr-risk/publish_risk.py(+758 -0)➕
.github/pr-risk/tests/test_grade_risk.py(+720 -0)➕
.github/pr-risk/tests/test_publish_risk.py(+1002 -0)➕
.github/workflows/pr-risk.yml(+483 -0)➕
.github/workflows/test-pr-risk.yml(+52 -0)📝
AGENTS.md(+12 -0)📝
README.md(+1 -0)📄 Description
ELI-5
Reviewers are staring at a 200-deep PR queue with no way to tell which changes are safe to move quickly on. This adds a check that reads every PR's diff, decides how much attention it needs (
R0trivial →R3needs a careful read), and publishes that grade so you can filter the queue withis:pr is:open label:risk:R0and knock out ten low-risk PRs in one pass.It grades nothing else. It never fails a check, never blocks a merge, and no automation reads the label. A human still clicks every button.
What it does
Adds
pr-risk.yml, a reusable workflow, plus its grading logic under.github/pr-risk/. On every PR event it publishes three things:risk:R0..risk:R3at any time. Stale tiers are removed on each re-grade, so a PR that grows from R0 into R3 carriesrisk:R3and onlyrisk:R3.PR risk (advisory)) — immutable, timestamped, attached to the head commit, carrying the tier and the reason it was assigned. This is the per-change audit artifact.risk-grade-disputedlabel, giving a queryable stream of real reviewer disagreement.The load-bearing bit: it re-grades on every push
The failure mode named in the ticket is a classifier that grades once, marks a PR extra-small, and never re-analyses it while it grows into several thousand lines. A stale grade is worse than no grade, because it spends trust that does not come back. So the caller pattern requires
synchronizein its trigger types, and the label is recomputed and replaced on every run — not added once. There is an end-to-end test (test_a_grown_pr_relabels_from_r0_to_r3_across_two_publishes) that asserts exactly this across two publishes against a shared fake API.Nothing is gated — four independent guarantees
Because "advisory" is easy to claim and easy to break later, it is enforced in four places rather than asserted once: the Check Run's conclusion is hardcoded
neutral(a neutral check cannot fail a PR even if someone later marks it required);grade-risk.shalways exits 0; the grade step wraps the grader inif ! … fi+exit 0so a broken grader still cannot redden a PR; and every publish step iscontinue-on-errorwith an explicit "degraded mode" note so nothing goes green silently.Unknown is published as unknown
A PR the grader could not read (bad refs, git failure, malformed diff) gets no tier: it is published as
unknownin the Check Run and the comment, and carries norisk:*label. It is never defaulted torisk:R0and never silently skipped — a silent default to the safest tier is the same trust-spending failure as a stale grade.reconcile_labels(…, desired=None)also removes any pre-existing tier, so a PR that becomes ungradable does not keep a stale grade.Shadow mode by default
modedefaults toshadow: Check Run only, no label and no comment. An adopting repo accumulates grades on its commits and checks them against reviewer intuition before any reviewer-facing surface changes.mode: publishopts in. This is the feature-flag equivalent for a CI surface, followingpr-size.yml'smode: warnprecedent.Security topology
Copied from
pr-size.yml, because it is the security model and not a convenience:gradejob runs against PR code withcontents: readand no write credentials. It hands its report to the next job as an artifact.publishjob holds the bot write token and checks out no PR code — its only checkout is this repo's own pinned scripts.pull_requestonly, deliberately notpull_request_target— the fork case is solved by the two-job split, not by handing a privileged context a PR checkout.disputejob only fires on a comment authored by a Bot, so a user pasting the marker into their own comment cannot drive the label.Verification
All run locally and wired into
test-pr-risk.yml(path-filtered, matchingtest-pr-size.yml/test-groom-scripts.yml):python3 -m unittest discover -s .github/pr-risk/tests -p 'test_*.py'— 68 tests, all passing. Covers classification,--numstat -zparsing (renames, binaries, paths with spaces, malformed records), tier/escalation math, the unknown path, rendering, the shell entrypoint end-to-end over throwaway git repos, and all of the publisher invariants above against an injected fake API.shellcheck -x .github/pr-risk/grade-risk.sh— clean.python3 .github/agents-md-integrity/check_agents_md.py --root .— passes.db/migrations/1.sql, rangrade-risk.shagainst it →risk:R3,status: graded, correct sticky marker in the rendered comment.Judgment calls / things to know
1. The risk map is a documented first cut, not derived thresholds. The ticket says grading comes from
grade-risk.sh(BE-5507) and that deriving thresholds is out of scope here. BE-5507 has not landed and has no branch, so there was nothing to consume. Rather than block a ticket whose other nine acceptance criteria are all CI wiring,RISK_RULES/FILE_ESCALATE_LINES/SIZE_ESCALATE_LINESingrade_risk.pyship as an explicitly-labelled placeholder with aRISK MAP PROVENANCEdocstring. Nothing else in the diff depends on their values and nothing anywhere is gated on the result, so BE-5507 replaces those three in place and nothing else moves. Flagging this as the one place I widened scope past the ticket's letter.2. No consumer caller lands in this PR. The ticket allows one or two. A caller must pin a SHA that contains
pr-risk.yml, and that SHA only exists after this merges — shipping a caller here would pin a SHA lacking the file and redden every PR in this repo at startup. Enrollment is the standard two-step follow-up (merge the caller and add the repo to its callers roster). The caller pattern is fully documented in the workflow header and.github/pr-risk/README.md.3. Diff size. ~2140 lines added, above the usual "small coherent PR" preference. It is one coherent feature that the ticket asks for as a unit — workflow + scripts + tests + harness — and splitting it would land a workflow calling scripts that do not exist. ~1100 of those lines are tests and READMEs.
4.
AGENTS.mdis now 154 lines, four over the aspirational 150 target (hard ceiling 200, warn-only, and the checker passes). The pre-existing no-CODEOWNERS warning is unchanged.5. Caller permissions are a startup requirement. The calling job must grant
contents: read+checks: write+pull-requests: write; GitHub rejects a caller granting less than a called job requests, even in shadow mode. Documented in the header, the README, and the catalog entry.6. Negative-claim falsification (self-review clause e): not applicable, checked deliberately. The diff does add "unknown" strings, but the trigger is a capability denial — a throw/deny dead-end telling a user something is not supported. This is the opposite: a new advisory feature where
unknownis a required status for an ungradable diff (an explicit acceptance criterion), not a claim that some capability does not exist. No existing path is replaced with a dead-end; the diff is purely additive.7. Chesterton's Fence: nothing existing changed behaviour. The only edits to existing files are additive documentation rows in
README.mdandAGENTS.md, plus twoopen()-without-context-manager cleanups in the grader's own new test file.Acceptance criteria
All ten met. The two worth naming precisely: "grading logic comes from BE-5507" is satisfied structurally (the entrypoint is
grade-risk.shand the map is a single replaceable block) but not literally (BE-5507's map does not exist yet) — see judgment call 1. "Land one or two consumer callers" is deferred to the enrollment follow-up for the SHA-pin reason in judgment call 2.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.