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
80 changes: 55 additions & 25 deletions src/github/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ type CheckRunListResponse = {
}>;
};

export type CheckRunOutcome =
| { kind: "published"; id: number; html_url?: string }
| { kind: "permission_missing"; warning: string };

export async function createInstallationToken(env: Env, installationId: number): Promise<string> {
const jwt = await createAppJwt(env);
const response = await fetch(`https://github.com/ghapi/app/installations/${installationId}/access_tokens`, {
Expand Down Expand Up @@ -65,47 +69,73 @@ export async function createOrUpdateCheckRun(
installationId: number,
repoFullName: string,
advisory: Advisory,
): Promise<CheckRunResponse | null> {
detailLevel: "minimal" | "standard" | "deep" = "minimal",
): Promise<CheckRunOutcome | null> {
if (!advisory.headSha) return null;
const [owner, repo] = repoFullName.split("/");
if (!owner || !repo) throw new Error(`Invalid repository full name: ${repoFullName}`);

const token = await createInstallationToken(env, installationId);
const octokit = new Octokit({ auth: token });
const output = formatCheckRunOutput(advisory);
const output = formatCheckRunOutput(advisory, detailLevel);

const existing = await octokit.request("GET /repos/{owner}/{repo}/commits/{ref}/check-runs", {
owner,
repo,
ref: advisory.headSha,
check_name: "Gittensory",
filter: "latest",
per_page: 1,
});
const existingCheckRun = (existing.data as CheckRunListResponse).check_runs?.[0];
if (existingCheckRun) {
const response = await octokit.request("PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", {
try {
const existing = await octokit.request("GET /repos/{owner}/{repo}/commits/{ref}/check-runs", {
owner,
repo,
ref: advisory.headSha,
check_name: "Gittensory",
filter: "latest",
per_page: 1,
});
const existingCheckRun = (existing.data as CheckRunListResponse).check_runs?.[0];
if (existingCheckRun) {
const response = await octokit.request("PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}", {
owner,
repo,
check_run_id: existingCheckRun.id,
name: "Gittensory",
status: "completed",
conclusion: advisory.conclusion,
output,
});
const data = response.data as CheckRunResponse;
return publishedOutcome(data);
}

const response = await octokit.request("POST /repos/{owner}/{repo}/check-runs", {
owner,
repo,
check_run_id: existingCheckRun.id,
name: "Gittensory",
head_sha: advisory.headSha,
status: "completed",
conclusion: advisory.conclusion,
output,
});
return response.data as CheckRunResponse;
const data = response.data as CheckRunResponse;
return publishedOutcome(data);
} catch (error) {
if (isCheckRunPermissionError(error)) {
return {
kind: "permission_missing",
warning: "GitHub App Checks: write permission is missing. Enable it in the GitHub App settings and re-approve the installation.",
};
}
throw error;
}
}

const response = await octokit.request("POST /repos/{owner}/{repo}/check-runs", {
owner,
repo,
name: "Gittensory",
head_sha: advisory.headSha,
status: "completed",
conclusion: advisory.conclusion,
output,
});
return response.data as CheckRunResponse;
function publishedOutcome(data: CheckRunResponse): CheckRunOutcome {
const outcome: { kind: "published"; id: number; html_url?: string } = { kind: "published", id: data.id };
if (data.html_url) outcome.html_url = data.html_url;
return outcome;
}

function isCheckRunPermissionError(error: unknown): boolean {
if (typeof error !== "object" || error === null) return false;
const e = error as { status?: number; message?: string };
if (e.status === 403) return true;
return typeof e.message === "string" && /resource not accessible by integration|not have permission/i.test(e.message);
}

export function getInstallationId(payload: GitHubWebhookPayload): number | null {
Expand Down
19 changes: 11 additions & 8 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,14 +776,17 @@ async function maybePublishPrPublicSurface(
});
}
if (decision.willCheckRun && advisory.headSha) {
await createOrUpdateCheckRun(env, installationId, repoFullName, {
...advisory,
conclusion: "success",
severity: "info",
title: "Gittensory context posted",
summary: "Gittensory posted public-safe contributor context.",
findings: [],
});
const checkRunResult = await createOrUpdateCheckRun(env, installationId, repoFullName, advisory, settings.checkRunDetailLevel);
if (checkRunResult?.kind === "permission_missing") {
await recordAuditEvent(env, {
eventType: "github_app.check_run_permission_missing",
actor: author,
targetKey: `${repoFullName}#${pr.number}`,
outcome: "error",
detail: checkRunResult.warning,
metadata: { deliveryId: webhook.deliveryId, repoFullName },
});
}
}
await recordAuditEvent(env, {
eventType: "github_app.pr_public_surface_published",
Expand Down
43 changes: 37 additions & 6 deletions src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,43 @@ export function buildIssueAdvisory(repo: RepositoryRecord | null, issue: IssueRe
return advisory("issue", targetKey, repoFullName, findings, "Issue advisory generated.", undefined, issue?.number);
}

export function formatCheckRunOutput(advisoryResult: Advisory): { title: string; summary: string; text: string } {
return {
title: advisoryResult.conclusion === "success" ? "Gittensory context checked" : "Gittensory context posted",
summary: "Gittensory public check output is intentionally minimal. Detailed maintainer context is available only through private API/MCP surfaces.",
text: "No detailed findings are published in check runs.",
};
const CHECK_RUN_FORBIDDEN_TERMS = /\b(reward|payout|farming|estimated score|raw trust score|wallet|hotkey|coldkey|reviewability|scoreability|private signal)\b/gi;

function sanitizeForCheckRun(text: string): string {
return text.replace(CHECK_RUN_FORBIDDEN_TERMS, "[context]").replace(/\s+/g, " ").trim();
}

export function formatCheckRunOutput(
advisoryResult: Advisory,
detailLevel: "minimal" | "standard" | "deep" = "minimal",
): { title: string; summary: string; text: string } {
const title = advisoryResult.conclusion === "success" ? "Gittensory context checked" : "Gittensory context posted";
const summary = "Gittensory public check output is intentionally minimal. Detailed maintainer context is available only through private API/MCP surfaces.";

if (detailLevel === "minimal" || advisoryResult.findings.length === 0) {
return { title, summary, text: "No detailed findings are published in check runs." };
}

const publicLines = advisoryResult.findings.map((f) => {
const label = f.severity === "warning" ? "⚠️" : "ℹ️";
const text = f.publicText ? sanitizeForCheckRun(f.publicText) : sanitizeForCheckRun(f.title);
return `${label} ${text}`;
});

if (detailLevel === "standard") {
return { title, summary, text: publicLines.join("\n") };
}

// deep: include action hints for findings that carry publicText
const deepLines = advisoryResult.findings.flatMap((f) => {
const label = f.severity === "warning" ? "⚠️" : "ℹ️";
const text = f.publicText ? sanitizeForCheckRun(f.publicText) : sanitizeForCheckRun(f.title);
const lines = [`${label} ${text}`];
if (f.publicText && f.action) lines.push(` → ${sanitizeForCheckRun(f.action)}`);
return lines;
});

return { title, summary, text: deepLines.join("\n") };
}

function addRepoFindings(repo: RepositoryRecord, findings: AdvisoryFinding[]): void {
Expand Down
134 changes: 132 additions & 2 deletions test/unit/github-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("GitHub check runs", () => {

const result = await createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory);

expect(result?.id).toBe(42);
expect(result).toMatchObject({ kind: "published", id: 42 });
expect(calls.some((url) => url.includes("/app/installations/123/access_tokens"))).toBe(true);
expect(calls.some((url) => url.includes("/repos/JSONbored/gittensory/check-runs"))).toBe(true);
});
Expand Down Expand Up @@ -114,10 +114,140 @@ describe("GitHub check runs", () => {

const result = await createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory);

expect(result?.id).toBe(42);
expect(result).toMatchObject({ kind: "published", id: 42 });
expect(methods.some((call) => call.startsWith("PATCH ") && call.includes("/check-runs/42"))).toBe(true);
});

it("returns permission_missing outcome when GitHub returns 403", async () => {
const privateKey = await generatePrivateKeyPem();
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
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")) return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 403 });
return new Response("not found", { status: 404 });
});

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

const result = await createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory);

expect(result).toMatchObject({ kind: "permission_missing" });
expect((result as { kind: string; warning: string }).warning).toMatch(/Checks: write/i);
});

it("publishes check run with standard detail level and includes public-safe finding text", async () => {
const privateKey = await generatePrivateKeyPem();
let capturedBody: { output?: { 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 { output?: { text?: string } };
return Response.json({ id: 77, html_url: "https://github.com/checks/77" }, { status: 201 });
}
return new Response("not found", { status: 404 });
});

const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: privateKey });
const advisory: Advisory = {
id: "advisory-std",
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: [{ code: "missing_linked_issue", title: "No linked issue detected", severity: "warning", detail: "No closing reference." }],
generatedAt: "2026-05-22T00:00:00.000Z",
};

const result = await createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory, "standard");

expect(result).toMatchObject({ kind: "published", id: 77 });
expect(capturedBody.output?.text).toMatch(/⚠️/);
expect(capturedBody.output?.text).not.toMatch(/reward|wallet|hotkey|trust score|reviewability|farming/i);
});

it("returns permission_missing for message-based 422 permission errors", async () => {
const privateKey = await generatePrivateKeyPem();
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
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")) {
return new Response(JSON.stringify({ message: "Resource not accessible by integration" }), { status: 422 });
}
return new Response("not found", { status: 404 });
});

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

const result = await createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory);
expect(result).toMatchObject({ kind: "permission_missing" });
});

it("rethrows non-permission errors from the check-run API", async () => {
const privateKey = await generatePrivateKeyPem();
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
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")) return new Response("internal server error", { status: 500 });
return new Response("not found", { status: 404 });
});

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

await expect(createOrUpdateCheckRun(env, 123, "JSONbored/gittensory", advisory)).rejects.toThrow();
});

it("skips check creation when no head SHA is available", async () => {
const result = await createOrUpdateCheckRun(createTestEnv(), 123, "JSONbored/gittensory", {
id: "advisory-3",
Expand Down
Loading