Skip to content

[bug] ISSUE_CLOSURE job can write solved_by_pr onto a reopened (OPEN) issue after async GraphQL fetch #199

Description

@andriypolanski

Description

The async ISSUE_CLOSURE fetch job can write issues.solved_by_pr onto an issue that has already been reopened (state = OPEN). IssueHandler clears solved_by_pr synchronously on reopen, but a slow in-flight closure job performs a slow GraphQL fetch and then unconditionally updates solved_by_pr without re-checking issue state or guarding the write with WHERE state = 'CLOSED'.

This corrupts issue-discovery scoring inputs: an OPEN issue can carry a solver PR link that should only exist on a closed issue.

Steps to Reproduce

  1. Close an issue that is solved by a merged PR. The issues.closed webhook upserts the row and enqueues ISSUE_CLOSURE with job id closure-{repoFullName}-{issueNumber}.

  2. Before the job finishes its GraphQL timeline fetch (fetchIssueClosingPr), reopen the issue on GitHub.

  3. The issues.reopened webhook runs first: upserts state = OPEN, solved_by_pr = null.

  4. The already-running ISSUE_CLOSURE job still holds a stale in-memory read from step 1 (state = CLOSED) and continues into fetchIssueClosingPr.

  5. When the job completes, it executes:

    await this.issueRepo.update({ repoFullName, issueNumber }, { solvedByPr });

    with no state guard.

  6. Query issues for that issue number — state = OPEN but solved_by_pr is populated again.

Expected Behavior

solved_by_pr should only be set while the issue remains closed. If the issue was reopened before the async job completes, the job should no-op (or re-read state and skip the write).

Actual Behavior

A reopened issue can be left with a non-null solved_by_pr, contradicting the synchronous reopen handling in IssueHandler and downstream assumptions that OPEN issues have no solver attribution.

Environment

  • OS: Linux
  • Runtime/Node version: Node 20 (NestJS + BullMQ worker)
  • Browser (if applicable): N/A

Additional Context

Affected code

  • packages/das/src/queue/fetch.processor.tshandleIssueClosure() reads issue state once at lines 118–122, then writes { solvedByPr } unconditionally at line 139.
  • packages/das/src/webhook/handlers/issue.handler.ts — reopen path correctly sets data.solvedByPr = null at lines 44–46, but cannot prevent a later async overwrite.

Why this is distinct from open items

Suggested fix direction

  • Re-read issues.state (or use UPDATE … WHERE state = 'CLOSED' RETURNING …) immediately before writing solved_by_pr.
  • Optionally skip fetchIssueClosingPr entirely if the issue is no longer closed after the GraphQL call returns.

Impact

solved_by_pr feeds issue-discovery scoring. An OPEN issue with a solver link can incorrectly count as solved or skew credibility ratios until a later webhook/backfill corrects it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions