mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #122] [MERGED] fix(pr-risk): make the label sync a single atomic PUT (BE-6343) #132
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#132
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/122
Author: @mattmillerai
Created: 8/4/2026
Status: ✅ Merged
Merged: 8/4/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-6343-atomic-label-put📝 Commits (3)
7b510bffix(pr-risk): make the label sync a single atomic PUT (BE-6343)c90926dfix(pr-risk): harden the atomic label PUT against case variants and a broken filter (BE-6343)f0ef9b4fix(pr-risk): keep label names inside JSON, and refuse to guess on a jq error (BE-6343)📊 Changes
4 files changed (+537 additions, -136 deletions)
View changed files
📝
.github/workflows/pr-risk.yml(+24 -9)📝
scripts/pr-risk/README.md(+29 -10)📝
scripts/pr-risk/apply-risk-label.sh(+171 -54)📝
scripts/pr-risk/tests/test_apply_risk_label.sh(+313 -63)📄 Description
ELI-5
Two grading runs could argue over one PR's risk label and both win. The old code did the label update in three steps — read what's there, delete the wrong one, add the right one — and two runs could slip between each other's steps, leaving the PR wearing two contradictory
risk:*stickers at once. Now the update is a single request that says "here is the complete final list of stickers", so whoever writes last wins cleanly and the PR always ends up with exactly one.What changed
scripts/pr-risk/apply-risk-label.shreplaces its read / delete-stale / add-target write sequence with onePUT /repos/{owner}/{repo}/issues/{n}/labels, which replaces the PR's full label set atomically. The desired set is built withjqfrom the snapshot read: every label the script does not own, carried through verbatim, plus the target.Why this closes the race: a
pr_numbersbatch dispatch run and apull_requestevent run for one of its numbers sit in different concurrency groups (a run belongs to exactly one group; a batch covers N PRs), so no concurrency key can serialize them. With the old shape, A reads{}, B reads{}, A addsrisk:R1, B addsrisk:R2→ two contradictory labels, until the next grade. With the PUT there is no delete/add window to interleave into: after any single writer's PUT the PR has exactly one owned label, and two concurrent writers end last-writer-wins with still exactly one (possibly the staler tier — acceptable, the label is advisory and the next grade re-syncs it). Zero-label is impossible because every PUT we build contains the target.Deliberately not done, per the ticket: no verify-and-retry loop (concurrent writers would just ping-pong, and the invariant already holds by construction), and no POST-when-nothing-is-stale hybrid (two writers that both read an empty owned set would both POST and reproduce the exact bug — every write must be the PUT). Both are called out in an inline comment so a future "optimization" doesn't quietly reopen the race.
The DELETE loop is deleted entirely, including the HTTP-404 tolerance #113 added — there is no DELETE left for it to guard, and its test block is removed with it. Kept unchanged: input validation,
LABEL_MAPparsing /OWNEDderivation, theDRY_RUNplan, theenc()path helper (still needed by the repo-label probe), theghq/gherrerror plumbing, the paginated read, and the ensure-label-exists pre-create (a PUT creates the association but never a label's color or description, so first-use enrollment still needs the explicitPOST repos/{r}/labels).Docs: the script header's ownership-contract paragraph, the
pr-risk.yml"A BATCH CANNOT SERIALIZE PER-PR" block, and thescripts/pr-risk/README.mdresidual-window section +apply-risk-label.shfile entry. The "preferpr_numberfor a serialized re-grade" guidance is kept — still true, still useful for superseding waits viacancel-in-progress.The new residual — please read, it is a pilot hazard
The PUT is built from a snapshot read, so a non-owned label added by a human between that read and the PUT is silently dropped —
risk-disputeincluded. The window is ~one API round-trip and only opens on a run that actually changes the grade (an in-sync PR writes nothing at all), and it is far narrower than the double-label window it replaces, which lasted until the next grade. But disputes are this pilot's whole calibration dataset, and one lost there vanishes without a trace. Documented in all three places above; re-adding the label restores it.Verification
bash scripts/pr-risk/tests/test_apply_risk_label.sh→ 35 passed, 0 failed (was 31 assertions; the 404-on-stale-DELETE block is gone, seven new cases added). Sibling suites unaffected:test_grade_pr_risk.sh57,test_grade_targets.sh91,test_resolve_enabled.sh21 — all green (the grade-targets suite copies the real labeler into its stub tooldir, so that integration is exercised too).shellcheck -xclean over all four scripts and all four test files, exactly astest-pr-risk.ymlinvokes it.grade-pr-risk.shorgrade-targets.sh— the batch loop shells out to the labeler per target and inherits the fix. Confirmed nothing greps the labeler's log lines, so the changedadded '…'/removed stale '…'→synced to '…'messages break no caller.New test coverage: PUT-not-DELETE/POST on the stale-present case; in-sync no-op (rc 0, target on stdout); the race-shape case (empty current set still PUTs, never an additive POST — with a comment on why a POST there reintroduces the interleaving); non-owned preservation (
risk-dispute+bugsurvive a tier change); first-use pre-create ordered before the PUT and carrying the tier color; and PUT-failure → rc 4 with gh's stderr, the PR name, and the UNCHANGED claim.Judgment calls
has "$TARGET"failed nothing. That mattered: PRs graded before this lands may be carrying tworisk:*labels right now, and a bare target-present check would call them in-sync and never heal them. Added a case asserting a double-labeled PR gets squashed to one (35th assertion; it fails under that mutation). This is a healing property the ticket did not ask for but the fix already had — the test just pins it.PUT .../issues/{n}/labelsexisting and replacing the set, so I checked GitHub's REST docs: PUT "Removes any previous labels and sets the new labels"; an empty array removes all (the footgun the ticket flagged — structurally absent here since the array always ends with the target); and POST "adds to existing labels", which independently confirms why the POST hybrid would reproduce the bug.pr-risk.yml'spull-requests: writerationale. That block says "SCOPE HONESTLY" and claimed the labeler "touches nothing but the mapped label names". A PUT now sends every label, so I narrowed the wording to "the only state it changes is the mapped label names — its atomic PUT re-sends every unowned label unchanged". Comment-only, and my diff is what made the old sentence imprecise.--jq '.[].name'+split("\n"), so a label name containing a newline was already mangled. Before, the consequence was "a stale label doesn't get removed"; now it is "an unowned label could be dropped or split". The ticket explicitly says to keep the read as-is, and fixing it properly means reworking the read into raw JSON — a larger change than this ticket scopes. Flagging rather than silently widening the diff. (GitHub's label API does not appear to permit newlines in names, so I believe this is theoretical.)No unmet acceptance criteria — all seven implementation points and all seven test points are done.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.