Skip to content

[Bug]: PR type label can silently downgrade gittensor:feature→gittensor:bug when a pull_request_review webhook races the merge webhook #4975

Description

@JSONbored

Summary

The per-PR TYPE label decision (gittensor:bug / gittensor:feature / gittensor:priority) can still silently and permanently downgrade a correctly-propagated gittensor:feature/gittensor:priority label back to gittensor:bug right after merge — via a third, distinct mechanism from the two previously fixed (#4528, #4816). This directly strips a contributor's reward multiplier and requires manual correction to notice.

Confirmed live right now: #4818 carries gittensor:bug even though its linked issue (#2192) carries gittensor:feature. It should read gittensor:feature.

Area

GitHub App (webhook processing / type-label decision)

History

Root cause

handlePullRequestWebhookEvent (src/queue/processors.ts:5577) is the shared entry point for every event that carries a payload.pull_request sub-object — not just pull_request itself, but also pull_request_review, pull_request_review_comment, and pull_request_review_thread (confirmed at the router, src/queue/processors.ts:6283; shouldProcessPullRequestPublicSurface, src/queue/processors.ts:6481, explicitly opts pull_request_review: submitted/edited/dismissed into reaching the public-surface/type-label pipeline).

It builds its pr object via upsertPullRequestFromGitHub(env, repoFullName, payload.pull_request) (processors.ts:5639) and threads that straight into maybePublishPrPublicSurface without ever re-verifying it live. pr.mergedAt therefore reflects whichever webhook happened to trigger this specific pass — not the PR's actual current state.

When a maintainer approves and merges within milliseconds of each other (a single "approve and merge" action, or two quick clicks), GitHub fires both a pull_request_review (submitted) webhook and a pull_request (closed) webhook almost simultaneously. The review webhook's embedded pull_request snapshot is taken at review time — a few ms before the merge — so it reports merged_at: null. If that job then gets delayed behind other work in the queue (confirmed happening under real load) long enough for the actual merge to complete and its linked issue to auto-close, the review-triggered pass runs its type-label recheck against a genuinely-closed issue while its own prMergedAt still reads null.

isLinkedIssueTrustworthy (src/review/linked-issue-label-propagation-fetch.ts:79, from #4816) requires prMergedAt !== null to trust a closed linked issue. With prMergedAt null, it correctly-by-its-own-logic refuses to trust the closure — this is by design, the same check that blocks a PR from opportunistically citing an issue someone else already closed. The bug is that this specific pass's prMergedAt is stale input, not a true "PR isn't merged yet." resolveIssueLabelsForPropagation then returns a confirmed-empty result silently (src/review/linked-issue-label-propagation-fetch.ts:139 — this specific early return has no logging, unlike every other branch in that function), the caller falls through to the title heuristic (deriveKindFromTitle, src/settings/pr-type-label.ts:40), and — since that heuristic requires a literal action verb (add/create/enable/implement/integrate/introduce/launch/support/wire) in the PR title's subject and most feature titles don't happen to contain one — it resolves bug.

Evidence chain (PR #4818)

  • Issue feat(ui): confidence-calibration curve card on the analytics dashboard #2192 closed at 2026-07-11T02:26:25Z; PR feat(ui): confidence-calibration curve card on the analytics dashboard (#2192) #4818 merged at 2026-07-11T02:26:24Z (same event, ~1s apart).
  • Webhook delivery records show a pull_request_review (submitted) delivery received 39ms before the pull_request (closed) delivery for the same PR.
  • The review-triggered job took 5.3 seconds to finish processing (consistent with real queue backlog at that moment — confirmed via independent queue-admission-deferral log lines in the same window) and its type_label_decision (applied:true, labels:["gittensor:bug"], source:"title") landed squarely inside that window, ~16s after the merge.
  • No linked_issue_label_propagation_inconclusive / _filtered log line appears for this decision — matching the silent, no-log early-return path described above exactly.
  • The parallel pull_request:closed webhook (the one with the correct, non-stale merged_at) finished in 1.28s but produced no visible type_label_decision of its own for this PR in the same window — it most likely lost the per-PR actuation lock race to the slower review-triggered job.

Ruled out

Scale

Across a 48-hour production sample (all type_label_decision events where a propagation_exclusive/propagation_additive decision was immediately followed by a title-sourced decision with a different label, for the same PR): 62 such transitions total, but only 1 is still wrong on live GitHub right now (#4818) — the other 61 were later corrected by a subsequent webhook/sweep pass recomputing a fresh, correct decision before anyone noticed. That self-healing is incidental, not guaranteed — nothing currently detects or backfills a PR that never receives a correcting follow-up pass, and a PR whose last-ever label mutation happens to be this race stays wrong indefinitely (as #4818 has).

Expected behavior

A PR's gittensor:bug/gittensor:feature/gittensor:priority label always reflects its linked issue's current label via propagation, regardless of which specific webhook event happens to trigger the recompute pass, and regardless of queue delay/ordering between related webhook deliveries for the same merge.

Actual behavior

A pull_request_review (or pull_request_review_comment / pull_request_review_thread) webhook whose embedded PR snapshot predates an imminent merge can, if delayed by queue backlog, silently and permanently downgrade the PR's label after the fact — with no log signal distinguishing it from a legitimate "issue closed independently, not by this PR" case.

Requirements

  1. The type-label decision must not treat "this pass's own webhook-embedded pr.mergedAt is null" as proof the PR isn't merged. When the linked issue is confirmed closed but this pass's prMergedAt is null, that is an ambiguous state, not a confirmed negative.
  2. Resolving that ambiguity must not blanket-convert it into "always inconclusive, defer forever" — that would break the original, legitimate anti-gaming case fix(review): merging a PR strips its propagated gittensor:priority/feature labels #4528/fix(review): never let an inconclusive linked-issue recheck downgrade a propagated label #4816 exist for (an issue closed independently by someone else while the PR is genuinely still open must still confidently resolve to the title fallback, not hang indefinitely).
  3. The fix should be scoped so it only pays for an extra live check in this one narrow ambiguous branch — not on every type-label pass — to avoid adding GitHub API cost to the common path.
  4. Must cover all four event types that reach this code path (pull_request, pull_request_review, pull_request_review_comment, pull_request_review_thread), not just the one implicated in feat(ui): confidence-calibration curve card on the analytics dashboard (#2192) #4818.
  5. Add the logging this specific silent branch is currently missing (src/review/linked-issue-label-propagation-fetch.ts:139) so a future recurrence is diagnosable from logs alone, matching every sibling branch in that function.
  6. A regression test reproducing feat(ui): confidence-calibration curve card on the analytics dashboard (#2192) #4818's exact scenario (a non-pull_request PR-family webhook with a stale/null merged_at racing a real merge whose linked issue is already closed).

Deliverables

Expected outcome

No PR-family webhook, regardless of type or processing delay, can strip a correctly-propagated gittensor:feature/gittensor:priority label after merge. The existing tri-state (confirmed / inconclusive) design in linked-issue-label-propagation-fetch.ts is extended to correctly classify this case instead of mis-classifying it as confirmed-absent.

Out of scope

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.roadmapOn the Wave-2 agent-layer roadmap board (project 9)

Projects

Status
Done

Relationships

None yet

Development

No branches or pull requests

Issue actions