Fix: GitHub PR attestation misses PRs merged seconds before CI runs#819
Fix: GitHub PR attestation misses PRs merged seconds before CI runs#819
Conversation
Adds a new method on GithubConfig that queries GitHub's GraphQL API by PR number rather than commit SHA, avoiding the eventual-consistency issue with associatedPullRequests. FakeGitHubClient gains PRsByNumber to support it in tests. Contract tests cover both the fake and real GitHub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PREvidenceForCommitHybrid to the PRRetriever interface and wires it into both call sites (attest pullrequest github, assert pullrequest github). When GitHub's GraphQL associatedPullRequests returns 0 results, the hybrid falls back to REST discovery + per-PR GraphQL so that PRs merged seconds before CI runs are correctly detected. GitLab, Azure, and Bitbucket delegate the method to their existing V2 implementations since they have no eventual consistency issue. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps the GraphQL query in PREvidenceByPRNumber with a retry loop that sleeps 10s, 20s, then 30s between attempts (60s total max). Any GraphQL error (network, HTTP, or API) triggers a retry. The Sleep field on GithubConfig is injectable for tests so retries complete instantly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Claude finished @jumboduck's task in 4m 22s —— View job PR Review: Fix GitHub PR attestation misses PRs merged seconds before CI runs
Static AnalysisAll checks pass: SummaryThis is a well-structured PR that solves a real and painful problem (GitHub GraphQL eventual consistency causing false "no PR found" in CI). The three-slice approach is clean and incrementally reviewable. What's good:
Inline comments posted (all non-blocking):
No blocking issues found. The code compiles, vets clean, the approach is sound, and the testing is thorough. Previously flagged review items (stale doc comment, retry loop restructure) have been addressed in recent commits. |
Defines graphqlCommitNode and graphqlReviewNode as named types shared by both PREvidenceByPRNumber and PREvidenceForCommitV2, and extracts the evidence construction (commit loop, review loop, PREvidence build) into a buildPREvidence helper. Both methods now call the helper instead of duplicating ~50 lines each. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no PRs are found via the fallback path, the hybrid was returning a nil slice which serialises to JSON null. The server rejects null for pull_requests — it requires a list. Initialise result as an empty non-nil slice in both GithubConfig and FakeGitHubClient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ify retry loop Real GitHub GraphQL returns an error for non-existent PR numbers, not nil. Align fake and contract test to match actual API behaviour. Also simplify the retry loop to a standard for-condition form. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use a delays slice starting with 0 so the initial attempt and all retries share a single call site inside the loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
| HasNextPage graphql.Boolean | ||
| EndCursor graphql.String | ||
| } | ||
| } `graphql:"commits(first: 100, after: $commitCursor)"` |
There was a problem hiding this comment.
We limit to 100 commits here and don't get any more. If the PR has more than 100 commits we would miss this in the evidence
…CI runs (#819) * green: PREvidenceByPRNumber fetches PR data by PR number via GraphQL Adds a new method on GithubConfig that queries GitHub's GraphQL API by PR number rather than commit SHA, avoiding the eventual-consistency issue with associatedPullRequests. FakeGitHubClient gains PRsByNumber to support it in tests. Contract tests cover both the fake and real GitHub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * green: PREvidenceForCommitHybrid fixes GitHub eventual consistency bug Adds PREvidenceForCommitHybrid to the PRRetriever interface and wires it into both call sites (attest pullrequest github, assert pullrequest github). When GitHub's GraphQL associatedPullRequests returns 0 results, the hybrid falls back to REST discovery + per-PR GraphQL so that PRs merged seconds before CI runs are correctly detected. GitLab, Azure, and Bitbucket delegate the method to their existing V2 implementations since they have no eventual consistency issue. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * green: retry PREvidenceByPRNumber on GraphQL error with 60s max backoff Wraps the GraphQL query in PREvidenceByPRNumber with a retry loop that sleeps 10s, 20s, then 30s between attempts (60s total max). Any GraphQL error (network, HTTP, or API) triggers a retry. The Sleep field on GithubConfig is injectable for tests so retries complete instantly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: extract buildPREvidence to eliminate duplication Defines graphqlCommitNode and graphqlReviewNode as named types shared by both PREvidenceByPRNumber and PREvidenceForCommitV2, and extracts the evidence construction (commit loop, review loop, PREvidence build) into a buildPREvidence helper. Both methods now call the helper instead of duplicating ~50 lines each. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: return non-nil empty slice from PREvidenceForCommitHybrid When no PRs are found via the fallback path, the hybrid was returning a nil slice which serialises to JSON null. The server rejects null for pull_requests — it requires a list. Initialise result as an empty non-nil slice in both GithubConfig and FakeGitHubClient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: PREvidenceByPRNumber returns error for unknown PR numbers; simplify retry loop Real GitHub GraphQL returns an error for non-existent PR numbers, not nil. Align fake and contract test to match actual API behaviour. Also simplify the retry loop to a standard for-condition form. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: unify retry loop so client.Query appears once Use a delays slice starting with 0 so the initial attempt and all retries share a single call site inside the loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update internal/github/github.go Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Fixes kosli-dev/server#5391.
GitHub's GraphQL
associatedPullRequestsfield has eventual consistency: it can return 0 results for a commit merged only seconds earlier. Bothattest pullrequest githubandassert pullrequest githubcalledPREvidenceForCommitV2directly with no fallback, so CI pipelines running immediately after a merge would get a false "found 0 pull request(s)".Approach
Three slices:
Slice 1 —
PREvidenceByPRNumberNew GraphQL query keyed by PR number rather than commit SHA. Querying by number bypasses the
associatedPullRequestsindex so it is immediately consistent. Building block for the hybrid.Slice 2 —
PREvidenceForCommitHybridNew method on the
PRRetrieverinterface, wired into both call sites.PREvidenceForCommitV2(original GraphQL by commit SHA) — happy path unchangedPullRequestsForCommit(REST, immediately consistent) for PR numbers, thenPREvidenceByPRNumberfor each — preserves all rich V2 fields (commits, approvers, author, etc.)We chose this over a plain V2→V1 fallback because V1 lacks the rich fields already in the attestation payload. GitLab, Azure, and Bitbucket implement
PREvidenceForCommitHybridas a pass-through to V2 since they have no GraphQL eventual consistency issue.Slice 3 — Retry on GraphQL error
Wraps the
client.Querycall inPREvidenceByPRNumberwith a retry loop (10s + 20s + 30s = 60s max).GithubConfig.Sleepis injectable so tests complete instantly.Testing
All new behaviour is covered by contract tests following the pattern from #807. Retry behaviour is tested with an
httptest.Serverthat fails N times before succeeding.🤖 Generated with Claude Code