diff --git a/src/github/backfill.ts b/src/github/backfill.ts index d9a82e3268..0d4f22e7e2 100644 --- a/src/github/backfill.ts +++ b/src/github/backfill.ts @@ -2485,6 +2485,10 @@ function isOwnGitHubAppCheckRun(env: Env, run: { name: string; app?: { slug?: st return ownSlug.length > 0 && appSlug === ownSlug && BOT_OWNED_CHECK_NAMES.has(run.name); } +function isBotOwnedRequiredContextName(name: string): boolean { + return BOT_OWNED_CHECK_NAMES.has(name); +} + function normalizeCiContextName(name: string): string { const trimmed = name.trim(); const slashIndex = trimmed.lastIndexOf("/"); @@ -2813,6 +2817,9 @@ async function reduceLiveCiAggregate( // A required context that never appeared in any result is not safe to treat as passed — count it as pending. if (enforceRequiredOnly) { for (const ctx of requiredContexts!) { + // The app creates these check-runs as part of review/public-surface publication. If branch protection + // requires one before the first run exists, treating its absence as CI to wait on self-deadlocks the review. + if (isBotOwnedRequiredContextName(ctx)) continue; if (!seenContextNames.has(ctx)) { anyPending = true; anyMissingRequiredContext = true; diff --git a/test/unit/backfill.test.ts b/test/unit/backfill.test.ts index a5a9ce6c21..fb25cf3711 100644 --- a/test/unit/backfill.test.ts +++ b/test/unit/backfill.test.ts @@ -57,6 +57,7 @@ import { setGitHubResponseCache, type CachedGitHubResponse, } from "../../src/github/client"; +import { GITTENSORY_CONTEXT_CHECK_NAME, GITTENSORY_GATE_CHECK_NAME, GITTENSORY_LEGACY_GATE_CHECK_NAME } from "../../src/review/check-names"; import { normalizeRegistryPayload } from "../../src/registry/normalize"; import { persistRegistrySnapshot } from "../../src/registry/sync"; import { renderMetrics, resetMetrics } from "../../src/selfhost/metrics"; @@ -4743,6 +4744,29 @@ describe("GitHub backfill", () => { expect(aggregate.hasVisiblePending).toBe(false); }); + it("does not wait for absent bot-owned required contexts before the app can publish them", async () => { + const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL) => { + const url = input.toString(); + if (url.includes("/check-runs?")) return Response.json({ check_runs: [{ name: "build", status: "completed", conclusion: "success" }] }); + if (url.includes("/status?")) return Response.json({ statuses: [] }); + return new Response("not found", { status: 404 }); + }); + + const requiredContexts = mergeRequiredCiContexts(null, [ + "build", + GITTENSORY_GATE_CHECK_NAME, + GITTENSORY_LEGACY_GATE_CHECK_NAME, + GITTENSORY_CONTEXT_CHECK_NAME, + ]); + const aggregate = await fetchLiveCiAggregate(env, "JSONbored/gittensory", "abc123", "public-token", requiredContexts); + + expect(aggregate.ciState).toBe("passed"); + expect(aggregate.hasPending).toBe(false); + expect(aggregate.hasMissingRequiredContext).toBe(false); + expect(aggregate.hasVisiblePending).toBe(false); + }); + it("does NOT flag a missing required context as confidently absent when the check-runs page read was incomplete", async () => { const env = createTestEnv({ GITHUB_PUBLIC_TOKEN: "public-token" }); vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {