Repository navigation
fix: refresh PR Sous Chef metadata through scoped GraphQL #69229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -294,28 +294,49 @@ function inspect(repo, pr, runs, api = gh, now = Date.now()) { | |||||
| const prFields = "number,updatedAt,author,isDraft,state,labels"; | ||||||
|
|
||||||
| export function fetchCandidate(repo, number, api = gh, now) { | ||||||
| // gh pr view's union fragments are rewritten incorrectly by the GitHub CLI proxy. | ||||||
| const candidate = api(["api", `repos/${repo}/pulls/${number}`]); | ||||||
| if (candidate?.number !== Number(number) || !candidate.updated_at || !candidate.head?.sha || !candidate.head.ref || !candidate.base?.ref) throw new Error(`PR #${number}: could not refresh PR metadata`); | ||||||
| // The proxy can mistake REST PR labels for the response collection and return []. | ||||||
| // Use an explicit repository-scoped query, without gh pr view's union fragments. | ||||||
| const [owner, name] = repo.split("/"); | ||||||
| const response = api([ | ||||||
| "api", | ||||||
| "graphql", | ||||||
| "-f", | ||||||
| `query=query($owner:String!,$name:String!,$number:Int!){ | ||||||
| repository(owner:$owner,name:$name){pullRequest(number:$number){ | ||||||
| number title url state isDraft headRefOid headRefName baseRefName | ||||||
| createdAt updatedAt changedFiles mergeStateStatus author{login} authorAssociation | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Requesting 💡 Why this is risky
A safer pattern is to request
Suggested change
That keeps the refresh path resilient while preserving the optional zero-diff optimization. |
||||||
| labels(first:100){pageInfo{hasNextPage} nodes{name}} | ||||||
| }}}`, | ||||||
| "-f", | ||||||
| `owner=${owner}`, | ||||||
| "-f", | ||||||
| `name=${name}`, | ||||||
| "-F", | ||||||
| `number=${number}`, | ||||||
| ]); | ||||||
| if (response?.errors?.length) throw new Error(`PR #${number}: ${JSON.stringify(response.errors)}`); | ||||||
| const candidate = response?.data?.repository?.pullRequest; | ||||||
| if ( | ||||||
| candidate?.number !== Number(number) || | ||||||
| !candidate.updatedAt || | ||||||
| !candidate.headRefOid || | ||||||
| !candidate.headRefName || | ||||||
| !candidate.baseRefName || | ||||||
| !["OPEN", "CLOSED", "MERGED"].includes(candidate.state) || | ||||||
| typeof candidate.isDraft !== "boolean" || | ||||||
|
|
||||||
| !Array.isArray(candidate.labels?.nodes) || | ||||||
| !candidate.labels.nodes.every(label => typeof label?.name === "string") || | ||||||
| candidate.labels?.pageInfo?.hasNextPage !== false | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failing closed on 💡 Why this is riskyThis query only fetches the first 100 labels, but the new guard treats pagination as a metadata corruption case instead of an incomplete-but-recoverable response. If the real goal is only to detect the
Suggested change
Then either follow the cursor or degrade gracefully when the first page is incomplete. |
||||||
| ) | ||||||
| throw new Error(`PR #${number}: could not refresh PR metadata`); | ||||||
| const metadata = { | ||||||
| number: candidate.number, | ||||||
| title: candidate.title, | ||||||
| url: candidate.html_url, | ||||||
| state: candidate.state.toUpperCase(), | ||||||
| isDraft: candidate.draft, | ||||||
| headRefOid: candidate.head.sha, | ||||||
| headRefName: candidate.head.ref, | ||||||
| baseRefName: candidate.base.ref, | ||||||
| createdAt: candidate.created_at, | ||||||
| updatedAt: candidate.updated_at, | ||||||
| changedFiles: candidate.changed_files, | ||||||
| author: candidate.user ? { login: candidate.user.login } : null, | ||||||
| labels: candidate.labels ?? [], | ||||||
| mergeStateStatus: candidate.mergeable_state === "dirty" ? "CONFLICTING" : (candidate.mergeable_state ?? "unknown").toUpperCase(), | ||||||
| ...candidate, | ||||||
| labels: candidate.labels.nodes, | ||||||
| mergeStateStatus: candidate.mergeStateStatus === "DIRTY" ? "CONFLICTING" : candidate.mergeStateStatus, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] 💡 Why this mattersGitHub's Consider adding @copilot please address this. |
||||||
| }; | ||||||
| if (now !== undefined && metadataSkip(metadata, now)) return metadata; | ||||||
| const checkRuns = api(["api", `repos/${repo}/commits/${candidate.head.sha}/check-runs?per_page=100&filter=latest`, "--paginate", "--slurp"]).flatMap(page => page.check_runs); | ||||||
| const statuses = api(["api", `repos/${repo}/commits/${candidate.head.sha}/status?per_page=100`, "--paginate", "--slurp"]).flatMap(page => page.statuses); | ||||||
| const checkRuns = api(["api", `repos/${repo}/commits/${candidate.headRefOid}/check-runs?per_page=100&filter=latest`, "--paginate", "--slurp"]).flatMap(page => page.check_runs); | ||||||
| const statuses = api(["api", `repos/${repo}/commits/${candidate.headRefOid}/status?per_page=100`, "--paginate", "--slurp"]).flatMap(page => page.statuses); | ||||||
| return { | ||||||
| ...metadata, | ||||||
| statusCheckRollup: [ | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/codebase-design]
fetchCandidatenow duplicates theowner/namesplit,-f owner=/-f name=argument construction, andresponse?.errors?.lengtherror-check thatinspect(just above, ~line 269-288) already implements for the same repo-scoped GraphQL pattern.💡 Suggested refactor
Extract a small shared helper, e.g.:
Both call sites could then pass only their query body and PR-number variable. This isn't blocking, but as a third repo-scoped GraphQL call site gets added later this duplication will make it easy for one copy to drift (e.g. only
fetchCandidatecurrently guards withresponse?.errorsoptional chaining whileinspectusesresponse.errorsunguarded).@copilot please address this.