From 2c002ed7498de230b851cdd08b691109f758465a Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 20:33:14 +0000 Subject: [PATCH 1/4] fix: resolve PRs from workflow run heads --- .github/workflows/label-pr-review-state.yml | 24 +++++++ .../pr-review-state-workflow.test.ts | 66 +++++++++++++++++-- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/.github/workflows/label-pr-review-state.yml b/.github/workflows/label-pr-review-state.yml index 48d836420e..b0bb560d98 100644 --- a/.github/workflows/label-pr-review-state.yml +++ b/.github/workflows/label-pr-review-state.yml @@ -88,6 +88,30 @@ jobs: } else if (context.payload.workflow_run?.pull_requests) { eventPrNumbers = context.payload.workflow_run.pull_requests.map(pr => pr.number); } + if (context.eventName === 'workflow_run' && eventPrNumbers.length === 0) { + const run = context.payload.workflow_run; + const headOwner = run.head_repository?.owner?.login; + if (headOwner && run.head_branch && run.head_sha) { + const candidates = await github.paginate(github.rest.pulls.list, { + owner, + repo, + state: 'open', + head: `${headOwner}:${run.head_branch}`, + per_page: 100, + }); + const baseRepository = `${owner}/${repo}`.toLowerCase(); + eventPrNumbers = candidates + .filter(pr => + pr.head?.sha === run.head_sha && + pr.base?.repo?.full_name?.toLowerCase() === baseRepository + ) + .map(pr => pr.number); + core.info( + `Resolved workflow_run ${run.id ?? '(unknown)'} to ` + + `${eventPrNumbers.length} PR(s) by exact head owner, branch, and SHA` + ); + } + } if (eventPrNumbers.length > 0) { prs = await Promise.all(eventPrNumbers.map(async pull_number => { const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); diff --git a/src/services/__tests__/pr-review-state-workflow.test.ts b/src/services/__tests__/pr-review-state-workflow.test.ts index 5cc0241db8..4dd59e052d 100644 --- a/src/services/__tests__/pr-review-state-workflow.test.ts +++ b/src/services/__tests__/pr-review-state-workflow.test.ts @@ -27,6 +27,8 @@ interface HarnessOptions { fork?: boolean eventName?: string workflowRunAssociated?: boolean + workflowRunFallback?: "match" | "sha-mismatch" | "none" + workflowRunHeadBranch?: string workflowDispatchPrNumber?: number existingGuide?: boolean existingGuideHead?: string @@ -80,6 +82,7 @@ interface HarnessOptions { /** Executes the embedded github-script workflow against deterministic GitHub API doubles. */ async function runWorkflow(options: HarnessOptions = {}) { + const eventName = options.eventName ?? "pull_request_target" const headRepository = options.fork ? "contributor/Zoo-Code" : "Zoo-Code-Org/Zoo-Code" const pr = { number: 1437, @@ -227,6 +230,15 @@ async function runWorkflow(options: HarnessOptions = {}) { if (!permission) throw Object.assign(new Error("Not Found"), { status: 404 }) return { data: { permission } } }) + const listPullRequests = vi.fn(async () => { + if (eventName === "workflow_run" && options.workflowRunAssociated === false) { + if (options.workflowRunFallback === "none" || options.workflowRunFallback === undefined) return [] + if (options.workflowRunFallback === "sha-mismatch") { + return [{ ...pr, head: { ...pr.head, sha: OLD_SHA } }] + } + } + return [pr] + }) const github = { paginate: vi.fn(async (target: unknown, args: unknown) => { @@ -251,7 +263,7 @@ async function runWorkflow(options: HarnessOptions = {}) { rest: { pulls: { get: vi.fn(async () => ({ data: pr })), - list: vi.fn(async () => [pr]), + list: listPullRequests, listReviews: vi.fn(async () => reviews), }, issues: { @@ -305,7 +317,6 @@ async function runWorkflow(options: HarnessOptions = {}) { }, }, } - const eventName = options.eventName ?? "pull_request_target" const pullRequestPayload = { number: 1437, head: { repo: { full_name: headRepository } }, @@ -320,6 +331,12 @@ async function runWorkflow(options: HarnessOptions = {}) { ? { workflow_run: { pull_requests: options.workflowRunAssociated === false ? [] : [{ number: 1437 }], + head_repository: { + owner: { login: options.fork ? "contributor" : "Zoo-Code-Org" }, + }, + head_branch: options.workflowRunHeadBranch ?? "feature/test", + head_sha: SHA, + id: 123456, }, } : { @@ -1439,10 +1456,51 @@ describe("PR review-state workflow", () => { expect(result.addLabels).not.toHaveBeenCalled() }) - it("does not list every PR when a workflow run has no associated PR", async () => { + it("resolves an unassociated same-repository workflow run by exact head", async () => { + const result = await runWorkflow({ + eventName: "workflow_run", + workflowRunAssociated: false, + workflowRunFallback: "match", + }) + + expect(result.listPullRequests).toHaveBeenCalledWith( + expect.objectContaining({ head: "Zoo-Code-Org:feature/test", state: "open" }), + ) + expect(result.addLabels).toHaveBeenCalledWith(expect.objectContaining({ labels: ["coderabbit-review-active"] })) + }) + + it("resolves an unassociated fork workflow run by exact head", async () => { + const result = await runWorkflow({ + eventName: "workflow_run", + workflowRunAssociated: false, + workflowRunFallback: "match", + fork: true, + }) + + expect(result.listPullRequests).toHaveBeenCalledWith( + expect.objectContaining({ head: "contributor:feature/test", state: "open" }), + ) + expect(result.addLabels).toHaveBeenCalledWith(expect.objectContaining({ labels: ["coderabbit-review-active"] })) + }) + + it("ignores an unassociated workflow run when the candidate head SHA differs", async () => { + const result = await runWorkflow({ + eventName: "workflow_run", + workflowRunAssociated: false, + workflowRunFallback: "sha-mismatch", + }) + + expect(result.listPullRequests).toHaveBeenCalled() + expect(result.createCommitStatus).not.toHaveBeenCalled() + expect(result.addLabels).not.toHaveBeenCalled() + }) + + it("does not sweep every PR when an unassociated workflow run has no exact match", async () => { const result = await runWorkflow({ eventName: "workflow_run", workflowRunAssociated: false }) - expect(result.listPullRequests).not.toHaveBeenCalled() + expect(result.listPullRequests).toHaveBeenCalledWith( + expect.objectContaining({ head: "Zoo-Code-Org:feature/test", state: "open" }), + ) expect(result.createCommitStatus).not.toHaveBeenCalled() }) From 8586fa7b9ff76e8cb7f019ad49caf7c4d9f532b1 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 21:17:18 +0000 Subject: [PATCH 2/4] test: cover workflow run fallback boundaries --- .../pr-review-state-workflow.test.ts | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/src/services/__tests__/pr-review-state-workflow.test.ts b/src/services/__tests__/pr-review-state-workflow.test.ts index 4dd59e052d..c9dc32d222 100644 --- a/src/services/__tests__/pr-review-state-workflow.test.ts +++ b/src/services/__tests__/pr-review-state-workflow.test.ts @@ -27,8 +27,9 @@ interface HarnessOptions { fork?: boolean eventName?: string workflowRunAssociated?: boolean - workflowRunFallback?: "match" | "sha-mismatch" | "none" + workflowRunFallback?: "match" | "sha-mismatch" | "base-mismatch" | "none" workflowRunHeadBranch?: string + workflowRunMissing?: "repository" | "branch" | "sha" workflowDispatchPrNumber?: number existingGuide?: boolean existingGuideHead?: string @@ -236,9 +237,13 @@ async function runWorkflow(options: HarnessOptions = {}) { if (options.workflowRunFallback === "sha-mismatch") { return [{ ...pr, head: { ...pr.head, sha: OLD_SHA } }] } + if (options.workflowRunFallback === "base-mismatch") { + return [{ ...pr, base: { ...pr.base, repo: { full_name: "another/repository" } } }] + } } return [pr] }) + const getPullRequest = vi.fn(async () => ({ data: pr })) const github = { paginate: vi.fn(async (target: unknown, args: unknown) => { @@ -262,7 +267,7 @@ async function runWorkflow(options: HarnessOptions = {}) { }), rest: { pulls: { - get: vi.fn(async () => ({ data: pr })), + get: getPullRequest, list: listPullRequests, listReviews: vi.fn(async () => reviews), }, @@ -331,11 +336,15 @@ async function runWorkflow(options: HarnessOptions = {}) { ? { workflow_run: { pull_requests: options.workflowRunAssociated === false ? [] : [{ number: 1437 }], - head_repository: { - owner: { login: options.fork ? "contributor" : "Zoo-Code-Org" }, - }, - head_branch: options.workflowRunHeadBranch ?? "feature/test", - head_sha: SHA, + head_repository: + options.workflowRunMissing === "repository" + ? null + : { owner: { login: options.fork ? "contributor" : "Zoo-Code-Org" } }, + head_branch: + options.workflowRunMissing === "branch" + ? null + : (options.workflowRunHeadBranch ?? "feature/test"), + head_sha: options.workflowRunMissing === "sha" ? null : SHA, id: 123456, }, } @@ -368,6 +377,7 @@ async function runWorkflow(options: HarnessOptions = {}) { createLabel, setFailed, warning: core.warning, + getPullRequest, listPullRequests: github.rest.pulls.list, listCommitStatusesForRef: github.rest.repos.listCommitStatusesForRef, } @@ -1491,13 +1501,44 @@ describe("PR review-state workflow", () => { }) expect(result.listPullRequests).toHaveBeenCalled() + expect(result.getPullRequest).not.toHaveBeenCalled() + expect(result.createCommitStatus).not.toHaveBeenCalled() + expect(result.addLabels).not.toHaveBeenCalled() + }) + + it("ignores an unassociated workflow run when the candidate base repository differs", async () => { + const result = await runWorkflow({ + eventName: "workflow_run", + workflowRunAssociated: false, + workflowRunFallback: "base-mismatch", + }) + + expect(result.listPullRequests).toHaveBeenCalledTimes(1) + expect(result.getPullRequest).not.toHaveBeenCalled() expect(result.createCommitStatus).not.toHaveBeenCalled() expect(result.addLabels).not.toHaveBeenCalled() }) + it.each(["repository", "branch", "sha"] as const)( + "ignores an unassociated workflow run with missing %s metadata", + async (workflowRunMissing) => { + const result = await runWorkflow({ + eventName: "workflow_run", + workflowRunAssociated: false, + workflowRunMissing, + }) + + expect(result.listPullRequests).not.toHaveBeenCalled() + expect(result.getPullRequest).not.toHaveBeenCalled() + expect(result.createCommitStatus).not.toHaveBeenCalled() + expect(result.addLabels).not.toHaveBeenCalled() + }, + ) + it("does not sweep every PR when an unassociated workflow run has no exact match", async () => { const result = await runWorkflow({ eventName: "workflow_run", workflowRunAssociated: false }) + expect(result.listPullRequests).toHaveBeenCalledTimes(1) expect(result.listPullRequests).toHaveBeenCalledWith( expect.objectContaining({ head: "Zoo-Code-Org:feature/test", state: "open" }), ) From 28079ccb04758fec31d0ea6ff460328396814fcb Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 23:53:11 +0000 Subject: [PATCH 3/4] test: exclude closed workflow run candidates --- .../__tests__/pr-review-state-workflow.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/services/__tests__/pr-review-state-workflow.test.ts b/src/services/__tests__/pr-review-state-workflow.test.ts index c9dc32d222..b8bb63c312 100644 --- a/src/services/__tests__/pr-review-state-workflow.test.ts +++ b/src/services/__tests__/pr-review-state-workflow.test.ts @@ -231,7 +231,8 @@ async function runWorkflow(options: HarnessOptions = {}) { if (!permission) throw Object.assign(new Error("Not Found"), { status: 404 }) return { data: { permission } } }) - const listPullRequests = vi.fn(async () => { + const listPullRequests = vi.fn(async ({ state }: { state?: string }) => { + if (state === "open" && options.prState === "closed") return [] if (eventName === "workflow_run" && options.workflowRunAssociated === false) { if (options.workflowRunFallback === "none" || options.workflowRunFallback === undefined) return [] if (options.workflowRunFallback === "sha-mismatch") { @@ -1479,6 +1480,20 @@ describe("PR review-state workflow", () => { expect(result.addLabels).toHaveBeenCalledWith(expect.objectContaining({ labels: ["coderabbit-review-active"] })) }) + it("ignores closed PRs when resolving an unassociated workflow run", async () => { + const result = await runWorkflow({ + eventName: "workflow_run", + prState: "closed", + workflowRunAssociated: false, + workflowRunFallback: "match", + }) + + expect(result.listPullRequests).toHaveBeenCalledWith(expect.objectContaining({ state: "open" })) + expect(result.getPullRequest).not.toHaveBeenCalled() + expect(result.createCommitStatus).not.toHaveBeenCalled() + expect(result.addLabels).not.toHaveBeenCalled() + }) + it("resolves an unassociated fork workflow run by exact head", async () => { const result = await runWorkflow({ eventName: "workflow_run", From b914493233257a1e007498551037e736c1a28231 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 23:58:40 +0000 Subject: [PATCH 4/4] test: assert closed fallback is inert --- src/services/__tests__/pr-review-state-workflow.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/__tests__/pr-review-state-workflow.test.ts b/src/services/__tests__/pr-review-state-workflow.test.ts index b8bb63c312..9b93b428c2 100644 --- a/src/services/__tests__/pr-review-state-workflow.test.ts +++ b/src/services/__tests__/pr-review-state-workflow.test.ts @@ -1488,10 +1488,15 @@ describe("PR review-state workflow", () => { workflowRunFallback: "match", }) - expect(result.listPullRequests).toHaveBeenCalledWith(expect.objectContaining({ state: "open" })) + expect(result.listPullRequests).toHaveBeenCalledTimes(1) + expect(result.listPullRequests).toHaveBeenCalledWith( + expect.objectContaining({ head: "Zoo-Code-Org:feature/test", state: "open" }), + ) expect(result.getPullRequest).not.toHaveBeenCalled() expect(result.createCommitStatus).not.toHaveBeenCalled() expect(result.addLabels).not.toHaveBeenCalled() + expect(result.removeLabel).not.toHaveBeenCalled() + expect(result.createLabel).not.toHaveBeenCalled() }) it("resolves an unassociated fork workflow run by exact head", async () => {