fetchLiveReviewThreadBlockers (src/github/backfill.ts ~4011-4021) reads connection?.nodes and returns [] when falsy. It never checks the GraphQL top-level errors array.
GitHub's standard partial-failure shape under load is HTTP 200 with:
{"data":{"repository":{"pullRequest":{"reviewThreads":null}}},"errors":[{...}]}
That yields [] on page 1 — indistinguishable from a genuinely thread-free PR. Not logged, not metered. The consumer folds it away too (src/queue/processors.ts ~10552-10559, .catch(() => [])).
Result: a maintainer's unresolved blocking review thread is dropped from advisory.findings, the gate can conclude success, and the PR merges over an open objection.
The sibling GraphQL readers in the same file do guard this — fetchLiveCiAggregateViaGraphQl (~3215) and fetchLinkedIssueClosedByPullRequest (~3440) both return uncertainty on errors. This one is the outlier.
Same class, second instance: fetchLivePullRequestReviewDecision (~3898-3916) also has no errors check and returns undefined, and the caller falls back to the stored pr.reviewDecision (processors.ts ~2590/~3426, liveReviewDecision ?? pr.reviewDecision). A PR approved at backfill time and later flipped to CHANGES_REQUESTED reads as still APPROVED → approvalsSatisfied → merge. Only bites repos with autoMaintain.requireApprovals > 0.
Fix
- Add
if (Array.isArray(result.errors) && result.errors.length > 0) to both readers and surface an unreadable signal distinct from "none found".
- Fail closed on unreadable: treat unknown review threads as blocking, and unknown review decision as not-satisfied whenever
requireApprovals > 0.
Acceptance
- A 200-with-errors GraphQL response results in a hold, never a merge.
fetchLiveReviewThreadBlockers(src/github/backfill.ts~4011-4021) readsconnection?.nodesand returns[]when falsy. It never checks the GraphQL top-levelerrorsarray.GitHub's standard partial-failure shape under load is HTTP 200 with:
{"data":{"repository":{"pullRequest":{"reviewThreads":null}}},"errors":[{...}]}That yields
[]on page 1 — indistinguishable from a genuinely thread-free PR. Not logged, not metered. The consumer folds it away too (src/queue/processors.ts~10552-10559,.catch(() => [])).Result: a maintainer's unresolved blocking review thread is dropped from
advisory.findings, the gate can concludesuccess, and the PR merges over an open objection.The sibling GraphQL readers in the same file do guard this —
fetchLiveCiAggregateViaGraphQl(~3215) andfetchLinkedIssueClosedByPullRequest(~3440) both return uncertainty onerrors. This one is the outlier.Same class, second instance:
fetchLivePullRequestReviewDecision(~3898-3916) also has noerrorscheck and returnsundefined, and the caller falls back to the storedpr.reviewDecision(processors.ts~2590/~3426,liveReviewDecision ?? pr.reviewDecision). A PR approved at backfill time and later flipped toCHANGES_REQUESTEDreads as stillAPPROVED→approvalsSatisfied→ merge. Only bites repos withautoMaintain.requireApprovals > 0.Fix
if (Array.isArray(result.errors) && result.errors.length > 0)to both readers and surface an unreadable signal distinct from "none found".requireApprovals > 0.Acceptance