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: 7 additions & 3 deletions src/github/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeInstallationOctokit } from "./client";
import { maintainerControlPanelUrl } from "./footer";
import type { AgentActionMode } from "../settings/agent-execution";
import { signRs256Jwt } from "../utils/crypto";
import { evaluateGateCheck, formatCheckRunOutput, formatGateCheckOutput, type CheckRunAnnotationContext, type CheckRunOutput, type GateCheckConclusion, type GateCheckPolicy } from "../rules/advisory";
import { evaluateGateCheck, formatCheckRunOutput, formatGateCheckOutput, type CheckRunAnnotationContext, type CheckRunOutput, type GateCheckConclusion, type GateCheckEvaluation, type GateCheckPolicy } from "../rules/advisory";

type CheckRunResponse = {
id: number;
Expand Down Expand Up @@ -159,10 +159,14 @@ export async function createOrUpdateGateCheckRun(
repoFullName: string,
advisory: Advisory,
policy: GateCheckPolicy = {},
options: { checkRunId?: number | undefined } = {},
options: { checkRunId?: number | undefined; gate?: GateCheckEvaluation | undefined } = {},
mode: AgentActionMode = "live",
): Promise<CheckRunOutcome | null> {
const gate = evaluateGateCheck(advisory, policy);
// Prefer the AUTHORITATIVE pre-computed evaluation when the caller has one (#5 / audit): the surface/content
// lane can OVERRIDE the generic verdict (surface_lane_reject → failure, surface_lane_manual → action_required),
// and re-deriving here via evaluateGateCheck would discard that override — publishing a GREEN check while the
// PR is actually auto-closed/held. Callers without a surface lane omit `gate` and re-derive as before (identical).
const gate = options.gate ?? evaluateGateCheck(advisory, policy);
return createOrUpdateNamedCheckRun(env, installationId, repoFullName, advisory, {
name: GITTENSORY_GATE_CHECK_NAME,
status: "completed",
Expand Down
4 changes: 4 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2720,6 +2720,10 @@ async function maybePublishPrPublicSurface(
gatePolicy,
{
checkRunId: pendingGateCheckRunId,
// #5 (audit): publish the AUTHORITATIVE surface-lane-merged verdict so the check-run conclusion matches
// the disposition; without this the check re-derives the generic verdict and shows green on a surface-
// lane reject/manual PR that is actually auto-closed/held. Undefined (gate off) ⇒ re-derive (identical).
gate: gateEvaluation,
},
mode,
);
Expand Down
32 changes: 32 additions & 0 deletions test/unit/github-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,38 @@ describe("GitHub check runs", () => {
});
});

it("publishes the precomputed authoritative gate (surface-lane override) instead of re-deriving (#5)", async () => {
const privateKey = await generatePrivateKeyPem();
let capturedBody: { conclusion?: string; output?: { title?: string; text?: 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")) {
capturedBody = JSON.parse(String(init?.body)) as typeof capturedBody;
return Response.json({ id: 91 }, { status: 201 });
}
return new Response("not found", { status: 404 });
});

const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey });
// The advisory is CLEAN (re-deriving via evaluateGateCheck would publish "success"), but the surface lane
// REJECTED the PR. The published check must reflect the authoritative override, not the generic re-derivation.
const surfaceGate = {
enabled: true,
conclusion: "failure" as const,
title: "Metagraphed surface review",
summary: "Surface payload rejected.",
blockers: [{ code: "surface_lane_reject", title: "Surface rejected", severity: "critical" as const, detail: "Registry payload failed validation." }],
warnings: [],
};
const result = await createOrUpdateGateCheckRun(env, 123, "JSONbored/gittensory", gateAdvisory("surface-sha"), {}, { gate: surfaceGate });

expect(result).toEqual({ kind: "published", id: 91 });
expect(capturedBody.conclusion).toBe("failure"); // the surface override, NOT the clean re-derivation
expect(capturedBody.output?.title).toBe("Metagraphed surface review");
});

it("updates an existing pending Gate check without adding a conclusion", async () => {
const privateKey = await generatePrivateKeyPem();
let capturedBody: { status?: string; conclusion?: string } = {};
Expand Down
Loading