fix: handle null files field from GraphQL on very large PRs (#1593)

GitHub's GraphQL API returns files: null (with no errors entry, and
changedFiles misreported as 0) when a PR's diff is too large to compute.
The unguarded pullRequest.files.nodes dereference in the fetcher crashed
the action with 'TypeError: null is not an object', and the formatter had
the same latent crash on prData.files.nodes.length.

Widen the GitHubPullRequest type to files | null so the compiler enforces
guards, degrade gracefully in the fetcher with a warning, and render the
file count as unavailable (not '0 files') in the formatter.

Fixes #1587
This commit is contained in:
leepokai
2026-08-07 07:55:10 -07:00
committed by GitHub
parent 6ef6450f51
commit 0a5f191964
5 changed files with 92 additions and 3 deletions
+43
View File
@@ -1775,6 +1775,49 @@ describe("fetchGitHubData integration with time filtering", () => {
// Webhook says no body at trigger time — attacker-added GraphQL body must not be used
expect(result.contextData.body).toBe("");
});
it("should not crash when GraphQL returns null files for a very large PR", async () => {
// GitHub declines to compute the diff for very large PRs: `files` comes
// back as null (with no errors entry) and `changedFiles` is misreported
// as 0. The fetch must degrade gracefully instead of throwing.
const mockOctokits = {
graphql: jest.fn().mockResolvedValue({
repository: {
pullRequest: {
number: 7912,
title: "Very large PR",
body: "PR body",
author: { login: "author" },
createdAt: "2024-01-15T10:00:00Z",
state: "OPEN",
labels: { nodes: [] },
comments: { nodes: [] },
files: null,
reviews: { nodes: [] },
},
},
user: { login: "trigger-user" },
}),
rest: {
pulls: {
listFiles: jest.fn().mockResolvedValue({ data: [] }),
},
},
};
const result = await fetchGitHubData({
octokits: mockOctokits as any,
repository: "test-owner/test-repo",
prNumber: "7912",
isPR: true,
triggerUsername: "trigger-user",
triggerTime: "2024-01-15T12:00:00Z",
});
// No file list is available, so the PR is processed without file-level context.
expect(result.changedFiles).toEqual([]);
expect(result.changedFilesWithSHA).toEqual([]);
});
});
describe("filterCommentsByActor", () => {
+37
View File
@@ -108,6 +108,43 @@ Changed Files: 2 files`,
);
});
test("renders an unknown file count when GraphQL returns null files (very large PR)", () => {
// GitHub declines to compute the diff for very large PRs and returns
// `files: null`. `changedFiles` is misreported as 0 in that case, so the
// count must render as unavailable rather than "0 files".
const prData: GitHubPullRequest = {
title: "Very large PR",
body: "PR body",
author: { login: "test-user" },
baseRefName: "main",
headRefName: "feature/test",
headRefOid: "abc123",
isCrossRepository: false,
headRepository: { owner: { login: "testowner" }, name: "testrepo" },
createdAt: "2023-01-01T00:00:00Z",
additions: 50,
deletions: 30,
state: "OPEN",
labels: {
nodes: [],
},
commits: {
totalCount: 3,
nodes: [],
},
files: null,
comments: {
nodes: [],
},
reviews: {
nodes: [],
},
};
const result = formatContext(prData, true);
expect(result).toContain("Changed Files: unknown (file list unavailable)");
});
test("formats Issue context correctly", () => {
const issueData: GitHubIssue = {
title: "Test Issue",