diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 022f413516..1b225a53f7 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -3883,7 +3883,7 @@ export async function maybeAddSecretLeakFinding( /* v8 ignore next -- fail-safe: a file-load error never destabilizes the gate. */ console.error( JSON.stringify({ - level: "warn", + level: "error", event: "secret_scan_failed", repository: args.repoFullName, pullNumber: args.pullNumber, @@ -4070,6 +4070,9 @@ async function auditGateCheckPermissionMissing( detail: warning, metadata: { deliveryId, repoFullName }, }); + // Surface the install-wide Checks:write gap to Sentry — until the scope is granted the required gate check-run + // silently never posts on ANY PR for this install; an operator must SEE this config fault, not just the ledger. + console.error(JSON.stringify({ level: "error", event: "gate_check_permission_missing", repository: repoFullName, pullNumber, deliveryId })); } /** @@ -4899,6 +4902,7 @@ async function maybePublishPrPublicSurface( detail: checkRunResult.warning, metadata: { deliveryId: webhook.deliveryId, repoFullName }, }); + console.error(JSON.stringify({ level: "error", event: "check_run_permission_missing", repository: repoFullName, pullNumber: pr.number, deliveryId: webhook.deliveryId })); } else if (checkRunResult?.kind === "published") { publishedOutputs.push("check_run"); } @@ -5305,6 +5309,16 @@ async function maybePublishPrPublicSurface( failedOutputs, }, }); + // The advisory ran but NOTHING reached the PR (revoked token / perms removed / GitHub 5xx). For an + // advisory-only bot this is the worst failure — escalate to Sentry at error level, not just the audit ledger. + captureReviewFailure(new Error("PR public-surface publish failed — review produced output but nothing was posted to the PR"), { + kind: "publish", + owner: repoFullName.split("/")[0], + repo: repoFullName, + pr: pr.number, + head_sha: advisory.headSha, + failedOutputs: failedOutputs.map((failure) => failure.output), + }); } return gateEvaluation; } diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index 74123a9a72..d9a0ad12a1 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { generateKeyPairSync } from "node:crypto"; import { clearInstallationTokenCacheForTest } from "../../src/github/app"; import * as repositoriesModule from "../../src/db/repositories"; +import * as sentryModule from "../../src/selfhost/sentry"; import { listCollisionEdges, createAgentRun, @@ -4673,6 +4674,7 @@ describe("queue processors", () => { if (url.includes("/commits/context500/check-runs")) return new Response("GitHub check API failed", { status: 500 }); return new Response("not found", { status: 404 }); }); + const captureSpy = vi.spyOn(sentryModule, "captureReviewFailure"); await expect( processJob(env, { @@ -4698,6 +4700,9 @@ describe("queue processors", () => { .first<{ detail: string; metadata_json: string }>(); expect(aggregate).toMatchObject({ detail: "check_run" }); expect(aggregate?.metadata_json).toContain('"output":"check_run"'); + // The total publish failure (nothing reached the PR) escalates to Sentry at error level, not just the ledger. + expect(captureSpy).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({ kind: "publish", repo: "JSONbored/gittensory" })); + captureSpy.mockRestore(); }); it("audits disabled public-surface skips without miner lookup", async () => {