mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #113] [MERGED] feat(pr-risk): grade a pull request by number on demand (workflow_dispatch) #122
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#122
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/113
Author: @mattmillerai
Created: 8/3/2026
Status: ✅ Merged
Merged: 8/3/2026
Merged by: @mattmillerai
Base:
main← Head:matt/pr-risk-dispatch-by-number📝 Commits (4)
d7e7fcefeat(pr-risk): grade a pull request by number, on demand7b67ee0test(pr-risk): cover the by-number path, base-ref resolution and batch isolation2f24371docs(pr-risk): document the on-demand grading surfacefacd005fix(pr-risk): encode the ref, retry the pre-grader reads, stop the summary claiming a label it did not apply📊 Changes
7 files changed (+1324 additions, -157 deletions)
View changed files
📝
.github/workflows/pr-risk.yml(+233 -153)📝
.github/workflows/test-pr-risk.yml(+11 -1)📝
scripts/pr-risk/README.md(+85 -0)📝
scripts/pr-risk/apply-risk-label.sh(+14 -3)➕
scripts/pr-risk/grade-targets.sh(+524 -0)📝
scripts/pr-risk/tests/test_apply_risk_label.sh(+40 -0)➕
scripts/pr-risk/tests/test_grade_targets.sh(+417 -0)📄 Description
ELI5
The risk grader could only ever grade the PR that just poked it. So a repo that turns the grader on today has thirty already-open PRs it can never label — each one has to be poked individually, and poking one means running that repo's entire CI. This adds a button: name a PR number (or a list) and it grades those, no poke required. With no number supplied, nothing about the old path changes.
Motivation
Two live gaps, both of them in the label-the-open-queue product rather than the offline calibration corpus:
synchronize/reopened/ready_for_review. Generating those events costs ~750 jobs on 2-vCPU runners for 30 advisory labels, because six workflows in that repo share the same event types. An empty-commit push costs the same and pollutes 30 branches' history..github/risk.jsonchange, or on a PR carrying the human-ownedrisk-disputelabel, the only way to re-grade was to push a commit to the PR.The grading logic needed no change to support this: it was already an API read keyed on a PR number, never a read of the event payload. The event coupling was two lines of workflow YAML.
Provenance
origin/mainat1c9443aactionlintclean on both workflows; YAML parses;shellcheck -xclean on all six shell files;test_grade_pr_risk.sh49 passed / 0 failed and untouched;test_apply_risk_label.sh24 passed / 0 failed; newtest_grade_targets.sh91 passed / 0 failed. (The review round moved the last two: the label script gained 404-tolerance on a concurrent stale-label DELETE, and the targets suite grew from 68 to 91 phases — see the review-resolution commit.) Plus a live read-only run of the new orchestration against real open PRs (details under Test plan).--stdinmode is not the batch vehicle — see Notes.facd005): ten panel findings resolved — the ref/path are now percent-encoded into the contentsread (a raw
#in a branch name truncated it into the silent default-branch read this PR exists to prevent), a404 for the ref is distinguished from a 404 for the file, the pre-grader reads retry a transient failure like
the grader already does, the step summary no longer renders a grade table for a target whose label write failed
and no longer discards rows when the grade step is cancelled mid-batch, the start gate is the job's deadline
rather than the wait budget, and sourcing the script no longer replaces the sourcing shell's EXIT trap.
Reviewer context
ghcall log, not stated — see Test planSummary
workflow_callinputs.pr_numbergrades one PR;pr_numbersgrades a comma-separated list and wins when both are set.PR_NUMBERSresolves topr_numbers || pr_number || github.event.pull_request.number, so the event path is the same single number it always was. Both arestring, notnumber:workflow_dispatchinputs arrive as strings, and an empty string is what lets the fall-through stay one expression..github/risk.jsonjudges the PR, and an empty?ref=is not an error to the contents API — it silently resolves to the default branch. Defaulting it would grade a PR against another branch's rules, which is the same failure the existing non-404 guard exists to prevent, arriving by a different door. It is not hypothetical: of five open PRs sampled on the pilot repo, two are stacked on feature branches.scripts/pr-risk/grade-targets.sh. It is per-target either way, so leaving it inline meant a second copy of the settle poll and the override contract for the by-number path. One copy — and it is unit-testable for the first time, which is what lets the claims above be tests rather than assertions.ungradedstill keeps the run green, on both paths. An unreadable PR is a reported verdict, not a broken run.if:are token guards — a bot's or a fork'spull_requestrun gets a read-onlyGITHUB_TOKEN, so the label write would 403 — and neither applies when a human presses the button. Fork risk is untouched:externalstill comes from the API's ownisCrossRepository, never the actor, and still grades R3.if:includesgithub.event.pull_request.head.repo.full_name == github.repository, which is false onworkflow_dispatch, so the job would skip silently — no run, no error, no annotation. Guard clauses are now scoped behindgithub.event_name != 'pull_request'.${{ github.workflow }}-${{ github.event.pull_request.number }}, which collapses to one constant group on dispatch, so withcancel-in-progressa batch would have each dispatch cancel the previous. The key now carries the resolved target.wait_for_checks_minuteson a backfill: measured rather than assumed. A dispatched run's own check attaches to the dispatched ref, not the PR's head commit, so it is absent from the graded PR's rollup and a settled PR reads its true state on the first poll — grading real tiers, not the R2 floor that0produces on the event path. It is still not free:0breaks out after a single read, ahead of the "require a settled reading to repeat" confirmation, so a target pushed to minutes ago lands the honest R2.1costs one 15s backoff plus a second read per PR. The header, the input description and the README all say prefer1, not0.Notes
--stdinis not the batch vehicle, despite being the obvious candidate. It grades pre-collected scorecard records fed to the grading jq — it performs no API read and cannot turn a PR number into a record. Batch is therefore a loop in the orchestration layer, where the per-target base ref and per-target override fetch have to live anyway.MAX_UNREADABLE_TRIES,READ_RETRY_BUDGET_SECONDS, plusPOLL_DELAY_SECONDS) purely so the suite can reach the unreadable-PR branch without sleeping through the production backoff. CI passes none of them, so the defaults are what runs.007to 7, so tolerating it would let7,007present as two targets for one PR and label it twice.test-pr-risk.yml's path filter now includespr-risk.yml, which passes the scripts their whole input contract through env and could otherwise break them without touching a file underscripts/pr-risk/.Test plan
Verified by unit test (hermetic —
ghstubbed on PATH, every call logged so the tests assert on which requests were made):ungradedkeeps the run green; a failed label write fails its targetSUCCESSand reversibility is not floored at R2one behaviour change there: a stale-label DELETE that 404s is the removal loop's goal state, not a failure
Verified live, read-only, against real open PRs on the pilot repo (
DRY_RUN=1, so no label was written):1094,1086,999999: both real PRs gradedR1; #1086's override was read from its feature-branch basematt/be-5532-cql-flat-pattern-costs, notmain— the load-bearing behaviour, against the real API999999failed at the base-ref hop and did not abandon the batch; the run reported2 graded, 0 ungraded, 1 failedwait_for_checks_minutes: 1is 15s per PR, matching the predicted single-backoff-plus-confirmationchecks_state=SUCCESS,pending=falseand a realR1— the empirical basis for the low-wait recommendationNot verified until this is merged and a caller pins it — stated plainly rather than claimed:
workflow_dispatchend-to-end run. It needs a consumer caller pinned to a SHA of this branch, which is awkward pre-merge; the reusable is only reachable through a caller'suses:GITHUB_TOKENis writable where a bot'spull_requesttoken is not is reasoned from GitHub's documented behaviour, not observed hereAfter merge, the natural first exercise is the backfill this was built for: bump the pilot consumer's caller to the new SHA, add the
workflow_dispatchblock from the header, and dispatch its open queue withwait_for_checks_minutes: 1. Running that backfill is deliberately a separate operational step, not part of this PR.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.