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
7 changes: 4 additions & 3 deletions src/github/app.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Octokit } from "@octokit/core";
import type { Advisory, GitHubWebhookPayload } from "../types";
import { signRs256Jwt } from "../utils/crypto";
import { evaluateGateCheck, formatCheckRunOutput, formatGateCheckOutput, type GateCheckConclusion, type GateCheckPolicy } from "../rules/advisory";
import { evaluateGateCheck, formatCheckRunOutput, formatGateCheckOutput, type CheckRunAnnotationContext, type CheckRunOutput, type GateCheckConclusion, type GateCheckPolicy } from "../rules/advisory";

type CheckRunResponse = {
id: number;
Expand Down Expand Up @@ -100,11 +100,12 @@ export async function createOrUpdateCheckRun(
repoFullName: string,
advisory: Advisory,
detailLevel: "minimal" | "standard" | "deep" = "minimal",
annotationContext?: CheckRunAnnotationContext,
): Promise<CheckRunOutcome | null> {
return createOrUpdateNamedCheckRun(env, installationId, repoFullName, advisory, {
name: GITTENSORY_CONTEXT_CHECK_NAME,
conclusion: advisory.conclusion,
output: formatCheckRunOutput(advisory, detailLevel),
output: formatCheckRunOutput(advisory, detailLevel, annotationContext),
});
}

Expand Down Expand Up @@ -171,7 +172,7 @@ async function createOrUpdateNamedCheckRun(
name: string;
status?: GitHubCheckStatus | undefined;
conclusion?: GitHubCheckConclusion | undefined;
output: { title: string; summary: string; text: string };
output: CheckRunOutput;
checkRunId?: number | undefined;
},
): Promise<CheckRunOutcome | null> {
Expand Down
8 changes: 7 additions & 1 deletion src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
listOtherOpenPullRequests,
listOpenPullRequests,
listPullRequests,
listPullRequestFiles,
listRecentMergedPullRequests,
listRepoLabels,
listRepoPullRequestFiles,
Expand Down Expand Up @@ -982,7 +983,12 @@ async function maybePublishPrPublicSurface(

if (decision.willCheckRun && advisory.headSha) {
try {
const checkRunResult = await createOrUpdateCheckRun(env, installationId, repoFullName, advisory, settings.checkRunDetailLevel);
const checkRunFiles = await listPullRequestFiles(env, repoFullName, pr.number);
const checkRunResult = await createOrUpdateCheckRun(env, installationId, repoFullName, advisory, settings.checkRunDetailLevel, {
files: checkRunFiles,
collisions,
pullNumber: pr.number,
});
if (checkRunResult?.kind === "permission_missing") {
failedOutputs.push({ output: "check_run", error: checkRunResult.warning });
await recordAuditEvent(env, {
Expand Down
52 changes: 52 additions & 0 deletions test/unit/github-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,58 @@ describe("GitHub check runs", () => {
expect(capturedBody.output?.text).toContain("does not post late first comments");
});

it("publishes Context check annotations on changed files while Gate stays text-only", async () => {
const privateKey = await generatePrivateKeyPem();
let contextBody: { name?: string; output?: { annotations?: Array<{ path: string; title: string }> } } = {};
let gateBody: { name?: string; output?: { annotations?: Array<{ path: string; title: string }> } } = {};
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" });
if (url.includes("/commits/")) return Response.json({ total_count: 0, check_runs: [] });
if (url.includes("/check-runs")) {
const body = JSON.parse(String(init?.body)) as {
name?: string;
output?: { annotations?: Array<{ path: string; title: string }> };
};
if (body.name === "Gittensory Context") contextBody = body;
if (body.name === "Gittensory Gate") gateBody = body;
return Response.json({ id: body.name === "Gittensory Gate" ? 90 : 77 }, { status: 201 });
}
return new Response("not found", { status: 404 });
});

const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey });
const advisory: Advisory = {
id: "advisory-annot",
targetType: "pull_request",
targetKey: "JSONbored/gittensory#9",
repoFullName: "JSONbored/gittensory",
pullNumber: 9,
headSha: "bbb999",
conclusion: "neutral",
severity: "warning",
title: "Gittensory advisory available",
summary: "1 advisory finding generated.",
findings: [],
generatedAt: "2026-05-22T00:00:00.000Z",
};

await createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory, "standard", {
pullNumber: 9,
files: [{ repoFullName: "JSONbored/gittensory", pullNumber: 9, path: "src/api/routes.ts", additions: 4, deletions: 0, changes: 4, payload: {} }],
collisions: {
repoFullName: "JSONbored/gittensory",
generatedAt: "2026-06-10T00:00:00.000Z",
summary: { clusterCount: 0, highRiskCount: 0, itemsReviewed: 0 },
clusters: [],
},
});
await createOrUpdateGateCheckRun(env, 123, "JSONbored/gittensory", advisory);

expect(contextBody.output?.annotations?.[0]).toMatchObject({ path: "src/api/routes.ts", title: "Missing test evidence" });
expect(gateBody.output?.annotations).toBeUndefined();
});

it("publishes check run with standard detail level and includes public-safe finding text", async () => {
const privateKey = await generatePrivateKeyPem();
let capturedBody: { output?: { text?: string } } = {};
Expand Down