From 0121d6d87da254787cdf017856d9a15c977fb8e1 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:41:00 -0700 Subject: [PATCH] fix(review): ignore suppressed check-run writes --- src/github/app.ts | 10 +++++---- test/unit/github-app.test.ts | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/github/app.ts b/src/github/app.ts index f4c5584f02..a929132482 100644 --- a/src/github/app.ts +++ b/src/github/app.ts @@ -53,6 +53,7 @@ export { type CheckRunResponse = { id: number; html_url?: string; + dryRunSuppressed?: boolean; }; type CheckRunListResponse = { @@ -624,7 +625,7 @@ async function createOrUpdateNamedCheckRun( const detailsUrlBody = detailsUrl ? { details_url: detailsUrl } : {}; // POST a fresh check-run THIS App owns. Used for a brand-new run AND as the cross-app fallback below. - const postNewCheckRun = async (): Promise => { + const postNewCheckRun = async (): Promise => { const response = await octokit.request( "POST /repos/{owner}/{repo}/check-runs", { @@ -727,8 +728,8 @@ async function createOrUpdateNamedCheckRun( } } }; - const finish = async (outcome: CheckRunOutcome): Promise => { - await finalizeLegacyPendingCheckRuns(); + const finish = async (outcome: CheckRunOutcome | null): Promise => { + if (outcome) await finalizeLegacyPendingCheckRuns(); return outcome; }; @@ -796,7 +797,8 @@ function outputForCheckRunUpdate(output: CheckRunOutput): CheckRunOutput { return safeOutput; } -function publishedOutcome(data: CheckRunResponse): CheckRunOutcome { +function publishedOutcome(data: CheckRunResponse): CheckRunOutcome | null { + if (data.dryRunSuppressed) return null; const outcome: { kind: "published"; id: number; html_url?: string } = { kind: "published", id: data.id, diff --git a/test/unit/github-app.test.ts b/test/unit/github-app.test.ts index cf3121b7ee..dcd6c70d50 100644 --- a/test/unit/github-app.test.ts +++ b/test/unit/github-app.test.ts @@ -108,6 +108,47 @@ describe("GitHub check runs", () => { ).toBe(true); }); + it("returns no published check-run outcome for dry-run suppressed writes", async () => { + const privateKey = await generatePrivateKeyPem(); + const calls: string[] = []; + vi.stubGlobal( + "fetch", + async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + calls.push(`${init?.method ?? "GET"} ${url}`); + if (url.includes("/access_tokens")) + return Response.json({ token: "installation-token" }); + if (url.includes("/check-runs")) + return Response.json({ check_runs: [] }); + return new Response("not found", { status: 404 }); + }, + ); + + const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey }); + const result = await createOrUpdateGateCheckRun( + env, + 123, + "JSONbored/gittensory", + gateAdvisory("dry-run-gate"), + {}, + {}, + "dry_run", + ); + + expect(result).toBeNull(); + expect( + calls.some( + (call) => call.startsWith("POST ") && call.includes("/check-runs"), + ), + ).toBe(false); + const audit = await env.DB.prepare( + "SELECT detail FROM audit_events WHERE event_type = ?", + ) + .bind("github.write.suppressed") + .first<{ detail: string }>(); + expect(audit?.detail).toContain("suppressed POST"); + }); + it("accepts GitHub App RSA private key PEMs for installation tokens", async () => { const privateKey = generateRsaPrivateKeyPem(); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {