Skip to content

Fix: GitHub PR attestation misses PRs merged seconds before CI runs#819

Merged
jumboduck merged 8 commits intomainfrom
5391-github-pr-fix
Apr 23, 2026
Merged

Fix: GitHub PR attestation misses PRs merged seconds before CI runs#819
jumboduck merged 8 commits intomainfrom
5391-github-pr-fix

Conversation

@jumboduck
Copy link
Copy Markdown
Contributor

Fixes kosli-dev/server#5391.

GitHub's GraphQL associatedPullRequests field has eventual consistency: it can return 0 results for a commit merged only seconds earlier. Both attest pullrequest github and assert pullrequest github called PREvidenceForCommitV2 directly 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 — PREvidenceByPRNumber
New GraphQL query keyed by PR number rather than commit SHA. Querying by number bypasses the associatedPullRequests index so it is immediately consistent. Building block for the hybrid.

Slice 2 — PREvidenceForCommitHybrid
New method on the PRRetriever interface, wired into both call sites.

  1. Tries PREvidenceForCommitV2 (original GraphQL by commit SHA) — happy path unchanged
  2. If 0 results: calls PullRequestsForCommit (REST, immediately consistent) for PR numbers, then PREvidenceByPRNumber for 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 PREvidenceForCommitHybrid as a pass-through to V2 since they have no GraphQL eventual consistency issue.

Slice 3 — Retry on GraphQL error
Wraps the client.Query call in PREvidenceByPRNumber with a retry loop (10s + 20s + 30s = 60s max). GithubConfig.Sleep is 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.Server that fails N times before succeeding.

🤖 Generated with Claude Code

jumboduck and others added 3 commits April 22, 2026 16:29
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
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 22, 2026

Claude finished @jumboduck's task in 4m 22s —— View job


PR Review: Fix GitHub PR attestation misses PRs merged seconds before CI runs

  • Run static analysis (go build, go vet, go mod tidy)
  • Review changed files
  • Post review findings

Static Analysis

All checks pass: go build ./..., go vet ./..., and go mod tidy — no issues.

Summary

This 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:

  • buildPREvidence extraction — Eliminates the duplication between PREvidenceByPRNumber and PREvidenceForCommitV2 that was flagged in earlier reviews. Single place to change if fields evolve.
  • Shared graphqlCommitNode / graphqlReviewNode types — Keeps the two GraphQL queries structurally in sync.
  • Contract test patternrunGitHubContractTests and runPRByNumberContractTests verify that the fake and real implementations satisfy the same contract. The hybrid fallback path is tested independently with PRNumbersByCommit seeding.
  • Injectable Sleep — Makes retry tests deterministic without real delays.
  • Other providers as pass-throughs — Azure, Bitbucket, and GitLab just delegate to V2, which is the right call since they don't have this consistency issue.

Inline comments posted (all non-blocking):

  1. pr == nil guard vs doc comment (github.go:297) — The nil, nil return is unreachable but slightly contradicts the "returns an error" doc. Minor readability nit.
  2. Retry loop clarity (github.go:281-290) — Suggested a slightly more explicit pattern that separates initial attempt from retries. Current code is correct.
  3. Partial results on multi-PR fallback (github.go:109-124) — Confirming fail-fast is the intended behavior when one PREvidenceByPRNumber call fails after retries.

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.

Comment thread internal/github/github.go Outdated
Comment thread internal/github/github.go Outdated
jumboduck and others added 2 commits April 23, 2026 08:42
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>
Comment thread internal/github/github.go Outdated
Comment thread internal/github/github.go
…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>
Comment thread internal/github/github.go Outdated
Comment thread internal/github/github.go Outdated
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>
@jumboduck jumboduck marked this pull request as ready for review April 23, 2026 07:50
Comment thread internal/github/github.go Outdated
Comment thread internal/github/github.go
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Comment thread internal/github/github.go
Comment thread internal/github/github.go
Comment thread internal/github/github.go
@jumboduck jumboduck merged commit 8055fcb into main Apr 23, 2026
18 checks passed
@jumboduck jumboduck deleted the 5391-github-pr-fix branch April 23, 2026 08:14
Comment thread internal/github/github.go
HasNextPage graphql.Boolean
EndCursor graphql.String
}
} `graphql:"commits(first: 100, after: $commitCursor)"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

github-actions Bot pushed a commit that referenced this pull request Apr 23, 2026
…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>
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.

3 participants