From 12a56f03880e29fca9bd59a2fd06e0992bea19c1 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Sun, 12 Jul 2026 07:48:58 -0700 Subject: [PATCH] fix(github): dismiss stale orb approvals without app slug --- src/github/pr-actions.ts | 4 ++- test/unit/github-pr-actions.test.ts | 46 +++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/github/pr-actions.ts b/src/github/pr-actions.ts index 43e7d4802..e0d9fd8e3 100644 --- a/src/github/pr-actions.ts +++ b/src/github/pr-actions.ts @@ -10,6 +10,7 @@ const ISSUE_EVENTS_RECENT_PAGE_LIMIT = 10; // earliest reviews on a PR with a long review history and could dismiss (or miss) the wrong one. const REVIEW_PAGE_SIZE = 100; const REVIEW_PAGE_LIMIT = 10; +const PUBLIC_GITHUB_APP_SLUG = "gittensory-orb"; // The GitHub write primitives the maintainer auto-maintain layer (#778) uses to act on a PR's STATE — never // its source. Thin wrappers over the installation-scoped REST API, mirroring labels.ts / comments.ts. Each @@ -128,7 +129,8 @@ export async function dismissLatestBotApproval(env: Env, installationId: number, const { owner, repo } = splitRepo(repoFullName); return await withInstallationTokenRetry(env, installationId, async (token) => { const octokit = makeInstallationOctokit(env, token, "live", githubRateLimitAdmissionKeyForInstallation(installationId)); - const botLogin = `${env.GITHUB_APP_SLUG}[bot]`; + const botSlug = env.GITHUB_APP_SLUG?.trim() || PUBLIC_GITHUB_APP_SLUG; + const botLogin = `${botSlug}[bot]`; // Reviews are returned oldest-first; the LAST matching entry across ALL pages is the bot's most recent // APPROVE. Stopping at page 1 would find (or miss) the wrong review on a PR with >100 total reviews. let latestApprovalId: number | undefined; diff --git a/test/unit/github-pr-actions.test.ts b/test/unit/github-pr-actions.test.ts index 20c7a1bb4..923233f01 100644 --- a/test/unit/github-pr-actions.test.ts +++ b/test/unit/github-pr-actions.test.ts @@ -4,8 +4,8 @@ import { closeIssue, closePullRequest, createIssueComment, createPullRequestRevi import { clearInstallationTokenCacheForTest } from "../../src/github/app"; import { createTestEnv } from "../helpers/d1"; -function envWithKey() { - return createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem() }); +function envWithKey(overrides: Partial = {}) { + return createTestEnv({ GITHUB_APP_PRIVATE_KEY: generateRsaPrivateKeyPem(), ...overrides }); } describe("GitHub PR action primitives (#778)", () => { @@ -528,6 +528,48 @@ describe("GitHub PR action primitives (#778)", () => { expect(calls[0]?.body).toMatchObject({ message: "stale approval retracted", event: "DISMISS" }); }); + it("REGRESSION (#security): falls back to the public Orb bot when GITHUB_APP_SLUG is unset", async () => { + const env = envWithKey(); + delete (env as Partial).GITHUB_APP_SLUG; + const calls: Array<{ url: string; body: Record }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/pulls/12/reviews") && !url.includes("/dismissals") && method === "GET") { + return Response.json([{ id: 12, state: "APPROVED", user: { login: "gittensory-orb[bot]" } }]); + } + if (url.includes("/pulls/12/reviews/12/dismissals") && method === "PUT") { + calls.push({ url, body: init?.body ? JSON.parse(String(init.body)) : {} }); + return Response.json({ id: 12, state: "DISMISSED" }); + } + return new Response("unexpected", { status: 500 }); + }); + + await expect(dismissLatestBotApproval(env, 123, "owner/repo", 12, "retract")).resolves.toEqual({ dismissed: true }); + expect(calls).toHaveLength(1); + }); + + it("falls back to the public Orb bot when GITHUB_APP_SLUG is blank", async () => { + const calls: Array<{ url: string; body: Record }> = []; + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url.includes("/access_tokens")) return Response.json({ token: "t" }); + if (url.includes("/pulls/13/reviews") && !url.includes("/dismissals") && method === "GET") { + return Response.json([{ id: 13, state: "APPROVED", user: { login: "gittensory-orb[bot]" } }]); + } + if (url.includes("/pulls/13/reviews/13/dismissals") && method === "PUT") { + calls.push({ url, body: init?.body ? JSON.parse(String(init.body)) : {} }); + return Response.json({ id: 13, state: "DISMISSED" }); + } + return new Response("unexpected", { status: 500 }); + }); + + await expect(dismissLatestBotApproval(envWithKey({ GITHUB_APP_SLUG: " " }), 123, "owner/repo", 13, "retract")).resolves.toEqual({ dismissed: true }); + expect(calls).toHaveLength(1); + }); + it("is a no-op when the bot never approved this PR", async () => { vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { const url = input.toString();