Skip to content

fix(mirror): drive solved_by_pr from ClosedEvent.closer#123

Merged
entrius merged 1 commit into
testfrom
feat/closer-based-solver-attribution
May 23, 2026
Merged

fix(mirror): drive solved_by_pr from ClosedEvent.closer#123
entrius merged 1 commit into
testfrom
feat/closer-based-solver-attribution

Conversation

@anderdc
Copy link
Copy Markdown
Collaborator

@anderdc anderdc commented May 23, 2026

Summary

Switches issues.solved_by_pr from the cross-reference reconcile path to GitHub's ClosedEvent.closer, anchored to the issue's current closedAt. Same primitive that entrius/gittensor#1305 moved the issue-bounty solver lookup to, so the bounty path and the issue-discovery path (which reads MirrorIssue.solved_by_pr) stay 1:1.

Why

The old reconcile wrote solved_by_pr from each PR's closingIssueNumbers on every PR-metadata fetch — that's "PR text declared it would close #N," not "GitHub determined PR-N caused the close." Reopen-then-reclose cycles and stray Closes #N mentions in later PRs could attribute the issue to the wrong PR. ClosedEvent.closer is GitHub's own record of what caused the current close.

After this lands, both bounty payout attribution and issue-discovery scoring read the same column, and that column reflects the actual closer rather than a text-declared candidate.

What changed

  • Fetcher (github-fetcher.service.ts): new fetchIssueClosingPr runs a GraphQL query for CLOSED_EVENT timeline items, walks newest-to-oldest looking for the event whose createdAt == issue.closedAt, and returns the closer PR number when the closer is a merged same-repo PullRequest. null for manual closes, non-PR closers, NOT_PLANNED closures, or closedAt missing.
  • Backfill (backfillIssues): the same GraphQL query gains an aliased closureTimeline block (CLOSED_EVENT, last: 20). The backfill loop computes solved_by_pr per issue in the same pass — no extra round trip.
  • Queue (constants.ts + fetch.processor.ts): new ISSUE_CLOSURE job runs fetchIssueClosingPr and writes the result. Open issues short-circuit to solved_by_pr = null.
  • Webhook (issue.handler.ts): on action == "closed", enqueue an ISSUE_CLOSURE job. closure-<repo>-<issue> jobId dedupes. Reopen still nulls solved_by_pr via the existing OPEN-state branch.
  • Removed: reconcileSolvedIssueLinks. PR metadata still refreshes closing_issue_numbers on the PR row (used by pr_linked_issues to enumerate which issues each PR is linked to), but no longer writes to issues.solved_by_pr.

Schema

No DDL changes — same solved_by_pr column, semantics tightened from "any merged PR claimed to close" to "the PR GitHub recorded as causing the current close."

Deploy / runbook

  1. Merge → deploy to test → deploy to prod.
  2. After prod deploy, enqueue one BACKFILL_REPO job per tracked repo with a long window (e.g. days: 365) so existing solved_by_pr values get reconciled against ClosedEvent.closer. Going forward, the webhook handles new closes and the regular backfill cadence handles reconciliation.
  3. No SQL migration needed.

Validation

  • npm run build clean
  • npm run lint clean
  • npm run format:check clean
  • No test framework in this package; verified via build + lint + format.

Related

Solver attribution previously rode the cross-reference reconcile path:
on every PR metadata fetch, the mirror wrote issue.solved_by_pr from
the PR's closingIssueNumbers array. That's a record of "PR text
declared it would close #N," not "GitHub determined PR-N caused the
issue's current closure," so reopen-then-reclose cycles, late body
edits, and stray "Closes #N" mentions could attribute to the wrong PR.

Switches the source of truth to ClosedEvent.closer anchored to the
issue's current closedAt:

- Add fetchIssueClosingPr / selectClosingPrFromTimeline on the
  GitHub fetcher. Returns the closer PR number when the close event
  matches the current closedAt and the closer is a merged same-repo PR;
  null for manual closes, non-PR closers, or NOT_PLANNED closures.
- Add an ISSUE_CLOSURE BullMQ job. Webhook handler enqueues on the
  closed action; the processor writes solved_by_pr from the fetcher.
- Extend backfillIssues to query the closure timeline alongside the
  label timeline so each issue's solver is resolved in the same pass.
- Drop reconcileSolvedIssueLinks. PR metadata still refreshes the
  PR-side closing_issue_numbers column; it no longer writes to
  issues.solved_by_pr.

The downstream effect: gittensor's issue discovery (reads
MirrorIssue.solved_by_pr) and the issue-bounty solver lookup
(entrius/gittensor#1305, also moved to ClosedEvent.closer) now share
one primitive and stay 1:1.

Schema unchanged. After deploy, re-running BACKFILL_REPO with a
long window reconciles existing solved_by_pr values.
@entrius entrius merged commit 0f6a975 into test May 23, 2026
2 checks passed
@entrius entrius deleted the feat/closer-based-solver-attribution branch May 23, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants