Skip to content
Closed
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
19 changes: 19 additions & 0 deletions config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,25 @@ gate:
- name: Contributor trust
appSlug: example-security-app

# Check-runs to IGNORE ENTIRELY (#9810) — the stronger sibling of advisoryCheckRuns above. Same
# spoof-resistant { name, appSlug } matching, but a matched run is treated as if it did not exist: it never
# gates CI, never counts as "still running", and — unlike advisory — never routes the PR to a manual-review
# hold either. Its conclusion is surfaced informationally only.
#
# Use this when a check's verdict carries no signal for YOUR repo while OTHER checks from the same app stay
# meaningful. The motivating case: a vendor app publishes both a real security scan AND a heuristic
# contributor-trust score. The scan is worth gating on; the trust score fails for perfectly good
# contributors, and listing it under advisoryCheckRuns still converts every one of their otherwise-clean PRs
# into a manual review — automation replaced by a queue of human decisions, and contributors left wondering
# whether they are being judged fairly. Ignoring the trust check keeps the scan's protection and drops the
# noise. If BOTH lists name the same check, ignore wins (it is the stronger, more explicit intent).
#
# List of { name, appSlug }, or omit. Default: not configured (byte-identical behavior for every repo that
# doesn't opt in). Config-as-code only — no DB column or dashboard toggle.
ignoredCheckRuns:
- name: Contributor trust
appSlug: example-security-app

# Promote a confident AI-judgment-only finding (one the reviewer itself placed under "Blockers", never
# a "Nit") into a real, deterministic gate blocker instead of leaving it advisory (#3907). Only matters
# for repos already running the registry content lane (see contentLane below) — content/registry repos
Expand Down
6 changes: 3 additions & 3 deletions test/unit/agent-approval-queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ describe("agent approval queue (#779)", () => {

expect(result.status).toBe("accepted");
expect(fetchRequiredStatusContexts).toHaveBeenCalledWith(env, "owner/repo", null, expect.any(String), expect.any(String));
expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build", "test"]), expect.any(String), undefined);
expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build", "test"]), expect.any(String), null, null);
});

it("unions branch-protection contexts into the accept-time live CI re-check", async () => {
Expand All @@ -599,7 +599,7 @@ describe("agent approval queue (#779)", () => {

expect(result.status).toBe("accepted");
expect(fetchRequiredStatusContexts).toHaveBeenCalledWith(env, "owner/repo", "main", expect.any(String), expect.any(String));
expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["branch-required", "build"]), expect.any(String), undefined);
expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["branch-required", "build"]), expect.any(String), null, null);
});

it("falls back to expectedCiContexts when the accept-time branch-protection read fails", async () => {
Expand All @@ -614,7 +614,7 @@ describe("agent approval queue (#779)", () => {
const result = await decidePendingAgentAction(env, { id: action.id, decision: "accept", decidedBy: "owner" });

expect(result.status).toBe("accepted");
expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build"]), expect.any(String), null);
expect(fetchLiveCiAggregate).toHaveBeenCalledWith(env, "owner/repo", "h7", expect.any(String), new Set(["build"]), expect.any(String), null, null);
});

it("accept supersedes a staged merge when live CI has since turned pending, not just failed (#2126)", async () => {
Expand Down
5 changes: 3 additions & 2 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1877,6 +1877,7 @@ describe("queue processors", () => {
new Set(["trusted-required-ci"]),
"installation:9001",
undefined, // #4372: advisoryCheckRuns (unconfigured here)
undefined, // #9813: ignoredCheckRuns (likewise)
);
expect(gateChecks).toBeGreaterThan(0);
const finalized = await env.DB.prepare("select count(*) as n from audit_events where event_type = ?")
Expand Down Expand Up @@ -3168,8 +3169,8 @@ describe("queue processors", () => {
await processJob(env, { type: "agent-regate-pr", deliveryId: "required-contexts-lookup-recovers", repoFullName: "owner/agent-repo", prNumber: 7, installationId: 9001 });
expect(liveCiSpy.mock.calls.length).toBeGreaterThan(liveReadsAfterFailedLookup);
expect(await renderMetrics()).toContain('loopover_ci_state_cache_total{field="aggregate",result="miss"} 1');
// #4372: the durable cache key now folds in the advisory-check-runs fingerprint (empty "|adv:" when unconfigured).
expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed", ciRequiredContextsKey: `${JSON.stringify(["trusted-required-ci"])}|adv:` });
// #4372: the durable cache key now folds in the advisory-check-runs fingerprint (empty "|adv:|ign:" when unconfigured).
expect(await getPullRequestDetailSyncState(env, "owner/agent-repo", 7)).toMatchObject({ ciState: "passed", ciRequiredContextsKey: `${JSON.stringify(["trusted-required-ci"])}|adv:|ign:` });
} finally {
liveCiSpy.mockRestore();
}
Expand Down