fix(webhook): stale payloads can no longer revert close-critical columns; handle transfers; wake on legacy CI events#9116
Conversation
…handle transfers; wake on legacy CI events #9057 -- the out-of-order guard protected only state/headSha/mergedAt/ githubUpdatedAt/labels. Every OTHER column was written unconditionally from a possibly-stale payload, including the ones the linked-issue gate reads -- a hard blocker and a close reason. Real sequence: a contributor pushes (`synchronize`, T1), then edits the body to add "Fixes #123" (`edited`, T2). GitHub does not guarantee delivery order, and once heads diverge the two jobs have different coalesce keys, so `synchronize` can be dequeued LAST. The guard correctly preserved state/headSha/labels -- then reverted body and linkedIssuesJson to the pre-edit snapshot, and since `synchronize` is itself in PR_PUBLIC_SURFACE_ACTIONS the same pass re-ran the gate against the reverted body: a wrong autonomous close for "No linked issue detected" on a PR that has one. That close reason appears in the live ledger. body/linkedIssuesJson/linkedIssueClaimedAt now reuse the existing sparse-body preservation path (the incoming value is not current truth in either case), and title/baseRef/headRef/authorAssociation are protected on the same condition. Applied to the onConflictDoUpdate SET clause as well as the INSERT values -- the UPDATE is the path that actually runs for an existing row -- and to the returned record, which callers act on in-process. #9056 -- `repository.transferred` was never handled despite already being in WEBHOOK_METRIC_ACTIONS, so the case was anticipated and never implemented. Since `repositories` is keyed by full_name with no github_id, a transfer just INSERTed a fresh row and orphaned every table renameRepositoryIdentity migrates -- including repository_settings, so autonomy and gate config silently reverted to defaults while the repo kept operating, and staged approvals in agent_pending_actions were lost. Now derives the old name from changes.owner.from.{organization,user}.login plus the current repo name and runs the same identity migration as a rename. #9059(b) -- `status`/`workflow_run` invalidated the CI cache but never woke a re-review, which only moved the stall: the PR then waited on the ~2-minute sweep, which REST-budget backpressure can skip. That matters specifically because `codecov/patch` arrives as a commit STATUS and is the gate's hardest required check, so a repo whose last green signal is a status got no webhook wake at all and the gate looked hung. Now re-reviews behind the same ciReReviewCoalesced guard the check_run/check_suite path uses, so a status storm cannot amplify. Closes #9056 Closes #9057 Tests: 2 stale-payload regressions (a stale delivery cannot revert body, linked issues, title, baseRef or headRef; a newer one still updates all of them), a status-wakes-re-review test, and the two existing invalidation tests updated to pre-claim the coalesce window so the invalidation stays observable alongside the new wake. Full suite green: 22,175 passing.
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9116 +/- ##
==========================================
- Coverage 93.89% 92.67% -1.23%
==========================================
Files 813 813
Lines 80798 80809 +11
Branches 24529 24538 +9
==========================================
- Hits 75864 74887 -977
- Misses 3564 4844 +1280
+ Partials 1370 1078 -292
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
Caution 🛑 LoopOver review result - fixes requiredReview updated: 2026-07-27 00:32:09 UTC
Review summary Blockers
Nits — 6 non-blocking
Why this is blocked
📋 Copy for AI agents — paste into your coding agentCI checks failing
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
…ned record, and close the coverage gaps Review on #9116 was correct on both counts. 1. The returned record overrode title/headSha/headRef/baseRef/mergedAt/labels but omitted authorAssociation, so an in-process caller could still read the stale value even though the DB row was correctly protected -- contradicting the very invariant the previous commit stated. Added, and asserted on the return value (not just the stored row) in the regression test. 2. codecov/patch was low because the transferred-event derivation and the authorAssociation stale-path had no tests. Both covered now. The transfer-predecessor derivation is extracted into an exported pure helper, resolveRepositoryIdentityPredecessor, because several of its shapes are unreachable through the live pipeline -- the repository upsert running alongside this handler requires a full_name and fails first -- so they could not be tested end to end. That also let the dead `?? payload.repository.owner/name` fallbacks go: full_name is authoritative and already required. Full suite green: 22,182 passing. 100% line+branch coverage on all added lines.
Summary
#9057 — a stale webhook reverts the body and causes a wrong close. The out-of-order guard protected only
state/headSha/mergedAt/githubUpdatedAt/labels. Every other column was written unconditionally from a possibly-stale payload — including the ones the linked-issue gate reads, which is a hard blocker and a close reason.The sequence: a contributor pushes (
synchronize, T1), then edits the body to addFixes #123(edited, T2). GitHub doesn't guarantee delivery order, and once heads diverge the two jobs have different coalesce keys — sosynchronizecan be dequeued last. The guard correctly preserved state/headSha/labels, then revertedbodyandlinkedIssuesJsonto the pre-edit snapshot. Sincesynchronizeis itself inPR_PUBLIC_SURFACE_ACTIONS, the same pass re-ran the gate against the reverted body → wrong autonomous close for "No linked issue detected" on a PR that has one. That close reason appears in the live ledger.Body/linked-issue state now reuses the existing sparse-body preservation path — in both cases the incoming value simply isn't current truth — and
title/baseRef/headRef/authorAssociationare protected on the same condition. Applied to theonConflictDoUpdateSET clause as well as the INSERT values (the UPDATE is the path that actually runs for an existing row — my first pass only fixed the INSERT and a test caught it), and to the returned record, which callers act on in-process.#9056 —
repository.transferredwas never handled. It was already listed inWEBHOOK_METRIC_ACTIONS, so the case was anticipated and simply never implemented. Becauserepositoriesis keyed byfull_namewith nogithub_id, a transfer just INSERTed a fresh row and orphaned every tablerenameRepositoryIdentitymigrates — includingrepository_settings, so autonomy and gate config silently reverted to defaults while the repo kept operating under different policy, and staged maintainer approvals inagent_pending_actionswere lost. Now derives the old name fromchanges.owner.from.{organization,user}.login+ the current repo name and runs the same identity migration as a rename.#9059(b) — legacy CI events invalidated the cache but never woke a re-review. That only moved the stall: the PR then waited on the ~2-minute sweep, which REST-budget backpressure can skip. It matters specifically because
codecov/patcharrives as a commit status and is the gate's hardest required check — a repo whose last green signal is a status got no webhook wake at all, so the gate looked hung. Now re-reviews behind the sameciReReviewCoalescedguard thecheck_run/check_suitepath uses, so a status storm can't amplify.Closes #9056
Closes #9057
Scope notes
upsertIssueFromGitHub) and (c) (pull_request.labelednot inPR_PUBLIC_SURFACE_ACTIONS) are likewise left open; this PR takes only the (b) half, which is the one causing the "gate hung" symptom.Test plan
baseReforheadRef; a newer one still updates all of themcheck_runtest already used for the same reason)test:cibuilds firsttsc --noEmit; migrations/schema-drift/docs/engine-parity gates green