diff --git a/.github/scripts/pr-readiness/test.mjs b/.github/scripts/pr-readiness/test.mjs index 0f9ffab..bc20033 100644 --- a/.github/scripts/pr-readiness/test.mjs +++ b/.github/scripts/pr-readiness/test.mjs @@ -55,7 +55,50 @@ const context = { const blockerCodes = (readiness) => ( readiness.deterministic_blockers.map((item) => item.code) ); +const closingChildren = Array.from({ length: 31 }, (_, index) => ({ + ...input.linked_issues[0], + number: 101 + index, + parent_number: 100, +})); +const manyLinkedIssues = [{ + ...input.linked_issues[0], + number: 100, + issue_type: "Task", + body: taskBody, + sub_issue_count: closingChildren.length, + sub_issues: closingChildren.map((issue) => ({ + repository: issue.repository, + number: issue.number, + state: issue.state, + })), +}, ...closingChildren]; +const manyLinkedIssuesInput = { + ...input, + linked_issues: manyLinkedIssues, + linked_issue_count: manyLinkedIssues.length, +}; +const scriptDirectory = path.dirname(new URL(import.meta.url).pathname); +const verifySource = fs.readFileSync( + path.join(scriptDirectory, "verify.mjs"), + "utf8", +); +const workflowSource = fs.readFileSync( + path.join(scriptDirectory, "..", "..", "workflows", "codex-openai-review.yml"), + "utf8", +); +for (const source of [verifySource, workflowSource]) { + assert.match(source, /closingIssuesReferences\(first: 100\)/); + assert.doesNotMatch(source, /closingIssuesReferences\.nodes\s*\.slice\(/); +} assert.deepEqual(context.readiness.deterministic_blockers, []); +assert.deepEqual( + analyzePullRequest(manyLinkedIssuesInput).deterministic_blockers, + [], +); +assert.ok(blockerCodes(analyzePullRequest({ + ...manyLinkedIssuesInput, + linked_issue_count: manyLinkedIssues.length + 1, +})).includes("too-many-closing-issues")); assert.ok(analyzePullRequest({ ...input, title: "Bad title" }) .deterministic_blockers.some((item) => item.code === "invalid-title")); assert.ok(analyzePullRequest({ ...input, body: "" }) @@ -427,6 +470,35 @@ try { new RegExp(context.readiness.snapshot_sha256), ); + const manyFixture = structuredClone(fixture); + manyFixture.repository.pullRequest.closingIssuesReferences = { + totalCount: manyLinkedIssues.length, + nodes: manyLinkedIssues.map((issue) => ({ + repository: { nameWithOwner: issue.repository }, + number: issue.number, + title: issue.title, + body: issue.body, + state: issue.state, + issueType: { name: issue.issue_type }, + parent: issue.parent_number == null + ? null + : { number: issue.parent_number }, + subIssues: { + totalCount: issue.sub_issue_count ?? 0, + nodes: (issue.sub_issues ?? []).map((subIssue) => ({ + repository: { nameWithOwner: subIssue.repository }, + number: subIssue.number, + state: subIssue.state, + })), + }, + })), + }; + fs.writeFileSync(fixtureFile, JSON.stringify(manyFixture)); + const manyReadiness = analyzePullRequest(manyLinkedIssuesInput); + const manyResult = verify(manyReadiness.snapshot_sha256); + assert.equal(manyResult.status, 0, manyResult.stderr); + fs.writeFileSync(fixtureFile, JSON.stringify(fixture)); + const assertStale = (mutate) => { const staleFixture = structuredClone(fixture); mutate(staleFixture.repository.pullRequest); diff --git a/.github/scripts/pr-readiness/verify.mjs b/.github/scripts/pr-readiness/verify.mjs index 7d9e999..6c431c6 100644 --- a/.github/scripts/pr-readiness/verify.mjs +++ b/.github/scripts/pr-readiness/verify.mjs @@ -40,7 +40,7 @@ async function fetchPullRequest() { body baseRefOid headRefOid - closingIssuesReferences(first: 20) { + closingIssuesReferences(first: 100) { totalCount nodes { repository { nameWithOwner } @@ -100,7 +100,6 @@ const data = await fetchPullRequest(); const pullRequest = data.repository?.pullRequest; if (!pullRequest) throw new Error("Pull request was not found"); const linkedIssues = pullRequest.closingIssuesReferences.nodes - .slice(0, 10) .map((issue) => ({ repository: issue.repository.nameWithOwner, number: issue.number, diff --git a/.github/workflows/codex-openai-review.yml b/.github/workflows/codex-openai-review.yml index 3bd474a..c39f994 100644 --- a/.github/workflows/codex-openai-review.yml +++ b/.github/workflows/codex-openai-review.yml @@ -392,7 +392,7 @@ jobs: pullRequest(number: $number) { title body - closingIssuesReferences(first: 20) { + closingIssuesReferences(first: 100) { totalCount nodes { repository { nameWithOwner } @@ -449,7 +449,6 @@ jobs: linked_issue_count: pullRequest.closingIssuesReferences.totalCount, linked_issues: pullRequest.closingIssuesReferences.nodes - .slice(0, 10) .map((issue) => ({ repository: issue.repository.nameWithOwner, number: issue.number, diff --git a/README.md b/README.md index b93f655..4082c8a 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,11 @@ linkage requirements: - The complete PR result follows the current Issue plan. Material deviations must be reflected in the Issue or disclosed and resolved in the PR. +The reviewer preserves every native closing Issue returned by GitHub's maximum +100-node GraphQL page. It fails closed when `totalCount` exceeds the collected +nodes, rather than silently reviewing a truncated relationship set. Native +sub-issue snapshots use the same 100-node fail-closed bound. + Caller policy can add trusted organization-level constraints. Repository-owned title formats, Issue Types, sections, relationships, ownership rules, validation commands, platform requirements, and finding severity rules belong