Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/github/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export {
type CheckRunResponse = {
id: number;
html_url?: string;
dryRunSuppressed?: boolean;
};

type CheckRunListResponse = {
Expand Down Expand Up @@ -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<CheckRunOutcome> => {
const postNewCheckRun = async (): Promise<CheckRunOutcome | null> => {
const response = await octokit.request(
"POST /repos/{owner}/{repo}/check-runs",
{
Expand Down Expand Up @@ -727,8 +728,8 @@ async function createOrUpdateNamedCheckRun(
}
}
};
const finish = async (outcome: CheckRunOutcome): Promise<CheckRunOutcome> => {
await finalizeLegacyPendingCheckRuns();
const finish = async (outcome: CheckRunOutcome | null): Promise<CheckRunOutcome | null> => {
if (outcome) await finalizeLegacyPendingCheckRuns();
return outcome;
};

Expand Down Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions test/unit/github-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading