Skip to content

fix(webhook): stale payloads can no longer revert close-critical columns; handle transfers; wake on legacy CI events#9116

Merged
JSONbored merged 2 commits into
mainfrom
fix/9055-9056-9057-9059-webhooks
Jul 27, 2026
Merged

fix(webhook): stale payloads can no longer revert close-critical columns; handle transfers; wake on legacy CI events#9116
JSONbored merged 2 commits into
mainfrom
fix/9055-9056-9057-9059-webhooks

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

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 add Fixes #123 (edited, T2). GitHub doesn't 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. Since synchronize is itself in PR_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/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 — my first pass only fixed the INSERT and a test caught it), and to the returned record, which callers act on in-process.

#9056repository.transferred was never handled. It was already listed in WEBHOOK_METRIC_ACTIONS, so the case was anticipated and simply never implemented. Because 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 under different policy, and staged maintainer approvals in agent_pending_actions were lost. Now derives the old name from changes.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/patch arrives 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 same ciReReviewCoalesced guard the check_run/check_suite path uses, so a status storm can't amplify.

Closes #9056
Closes #9057

Scope notes

Test plan

  • 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 proving the new wake fires
  • The two existing invalidation tests updated to pre-claim the coalesce window, so the invalidation stays directly observable alongside the new wake (same technique the check_run test already used for the same reason)
  • Full suite green: 22,175 passing, 0 failures — unit and integration, after building every artifact test:ci builds first
  • tsc --noEmit; migrations/schema-drift/docs/engine-parity gates green

Process note: I now run the complete suite with all bundles built before pushing. The earlier #9115 CI failure was integration tests I'd never run locally, and 327 apparent failures in a fresh worktree turned out to be missing MCP/miner/discovery-index/ui-kit builds rather than real breakage.

…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-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.67%. Comparing base (ee5da2e) to head (4aa2d75).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

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     
Flag Coverage Δ
backend 93.48% <100.00%> (-1.70%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/db/repositories.ts 96.74% <100.00%> (+<0.01%) ⬆️
src/queue/processors.ts 95.69% <100.00%> (+0.03%) ⬆️
src/types.ts 100.00% <ø> (ø)

... and 3 files with indirect coverage changes

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-27 00:32:09 UTC

5 files · 1 AI reviewer · 1 blocker · CI failing · unstable

🛑 Suggested Action - Manual Review

Review summary
This closes three linked issues in one PR: it extends the out-of-order webhook guard in src/db/repositories.ts to protect title/baseRef/headRef/authorAssociation/body/linkedIssuesJson (not just state/headSha/labels) on both the INSERT and onConflictDoUpdate paths, wires up repository.transferred handling (previously anticipated in WEBHOOK_METRIC_ACTIONS but never implemented), and makes the legacy status/workflow_run CI-cache invalidation also wake a coalesced re-review. The stale-payload fix is correct on the DB-write side and matches the described bug (a delayed `synchronize` reverting a just-added `Fixes #123`), but the function's own RETURNED record — which the PR's commit message explicitly says must reflect persisted state — drops the authorAssociation protection, so in-process callers can still see the stale value even though the DB row is correct. codecov/patch failed at 66.66% against a 99% target, consistent with several new branches (authorAssociation stale-path, the transferred-event owner-name derivation) having no visible test in this diff.

Blockers

  • src/db/repositories.ts: the final `return { ...record, title: resolvedTitle, ... }` (around line 530) overrides title/headSha/headRef/baseRef/mergedAt/labels but omits `authorAssociation: resolvedAuthorAssociation` — the returned record still spreads `record.authorAssociation` (built from the raw, possibly-stale `pr.author_association`), so any in-process caller reading the return value (per the commit's own stated goal that 'the returned record must describe what was PERSISTED') can act on a stale authorAssociation even though the DB row was correctly protected.
Nits — 6 non-blocking
  • No test in this diff exercises the `repository.transferred` old-full-name derivation in src/queue/processors.ts (`maybeHandleRepositoryRenamedWebhookEvent`) — given this is the exact code path orb(webhook): repository.transferred is unhandled — a transfer permanently orphans all ORB history for that repo #9056 says silently orphaned repository_settings/agent_pending_actions on a transfer, it deserves a dedicated regression test, not just the renamed-path coverage that presumably already existed.
  • test/unit/db-parsers.test.ts's new orb(webhook): out-of-order guard protects 5 columns but not linked-issue/body/baseRef — can cause a wrong 'no linked issue' close #9057 tests never assert on `authorAssociation` even though it's one of the four newly-protected columns (title/baseRef/headRef/authorAssociation) — combine with the blocker above, since a test would likely have caught the missing return-value override.
  • codecov/patch failed at 66.66% against the repo's 99% target; the diff adds several new conditional branches (stale-vs-live authorAssociation, transferred vs renamed old-name derivation) that don't appear to be independently covered.
  • Add `authorAssociation: resolvedAuthorAssociation` to the return statement in src/db/repositories.ts to close the gap noted in the blocker.
  • Add a webhook test for `repository.transferred` that asserts `renameRepositoryIdentity` is called with `previousOwner/currentName` -> `newFullName`, mirroring the existing renamed-path test.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Why this is blocked

  • src/db/repositories.ts: the final `return { ...record, title: resolvedTitle, ... }` (around line 530) overrides title/headSha/headRef/baseRef/mergedAt/labels but omits `authorAssociation: resolvedAuthorAssociation` — the returned record still spreads `record.authorAssociation` (built from the raw, possibly-stale `pr.author_association`), so any in-process caller reading the return value (per the commit's own stated goal that 'the returned record must describe what was PERSISTED') can act on a stale authorAssociation even though the DB row was correctly protected.
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. src/db/repositories.ts: the final \`return \{ ...record, title: resolvedTitle, ... \}\` \(around line 530\) overrides title/headSha/headRef/baseRef/mergedAt/labels but omits \`authorAssociation: resolvedAuthorAssociation\` — the returned record still spreads \`record.authorAssociation\` \(built from the raw, possibly-stale \`pr.author\_association\`\), so any in-process caller reading the return value \(per the commit's own stated goal that 'the returned record must describe what was PERSISTED'\) can act on a stale authorAssociation even though the DB row was correctly protected.

CI checks failing

  • codecov/patch — 66.66% of diff hit (target 99.00%)

Decision drivers

  • ❌ Code review — 1 blocker (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9056, #9057
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 13 registered-repo PR(s), 13 merged, 285 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 285 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Partially addressed
The PR fully implements the guard fix for repository.transferred (handling changes.owner.from, updating the payload type, and distinguishing rename vs transfer), which is the core symptom described. However, the issue's fix list also calls for storing repositories.github_id and reconciling identity by numeric id in backfillRegisteredRepositories (for self-healing lost deliveries) and for handling

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 13 PR(s), 285 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask 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.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

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.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 27, 2026
…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.
@JSONbored
JSONbored merged commit 31c7f21 into main Jul 27, 2026
4 checks passed
@JSONbored
JSONbored deleted the fix/9055-9056-9057-9059-webhooks branch July 27, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

1 participant