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
4 changes: 2 additions & 2 deletions migrations/0023_gate_quality_modes.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ALTER TABLE repository_settings ADD COLUMN linked_issue_gate_mode TEXT NOT NULL DEFAULT 'advisory';
ALTER TABLE repository_settings ADD COLUMN duplicate_pr_gate_mode TEXT NOT NULL DEFAULT 'advisory';
ALTER TABLE repository_settings ADD COLUMN linked_issue_gate_mode TEXT NOT NULL DEFAULT 'block';
ALTER TABLE repository_settings ADD COLUMN duplicate_pr_gate_mode TEXT NOT NULL DEFAULT 'block';
ALTER TABLE repository_settings ADD COLUMN quality_gate_mode TEXT NOT NULL DEFAULT 'advisory';
ALTER TABLE repository_settings ADD COLUMN quality_gate_min_score INTEGER;
5 changes: 5 additions & 0 deletions migrations/0025_restore_gate_blocking_modes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
UPDATE repository_settings
SET
linked_issue_gate_mode = CASE WHEN linked_issue_gate_mode = 'advisory' THEN 'block' ELSE linked_issue_gate_mode END,
duplicate_pr_gate_mode = CASE WHEN duplicate_pr_gate_mode = 'advisory' THEN 'block' ELSE duplicate_pr_gate_mode END
WHERE gate_check_mode = 'enabled';
4 changes: 2 additions & 2 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ const repositorySettingsSchema = z.object({
checkRunMode: z.enum(["off", "enabled"]).default("off"),
checkRunDetailLevel: z.enum(["minimal", "standard", "deep"]).default("standard"),
gateCheckMode: z.enum(["off", "enabled"]).default("off"),
linkedIssueGateMode: z.enum(["off", "advisory", "block"]).default("advisory"),
duplicatePrGateMode: z.enum(["off", "advisory", "block"]).default("advisory"),
linkedIssueGateMode: z.enum(["off", "advisory", "block"]).default("block"),
duplicatePrGateMode: z.enum(["off", "advisory", "block"]).default("block"),
qualityGateMode: z.enum(["off", "advisory", "block"]).default("advisory"),
qualityGateMinScore: z.number().int().min(0).max(100).nullable().optional(),
autoLabelEnabled: z.boolean().default(true),
Expand Down
8 changes: 4 additions & 4 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
checkRunMode: "off",
checkRunDetailLevel: "minimal",
gateCheckMode: "off",
linkedIssueGateMode: "advisory",
duplicatePrGateMode: "advisory",
linkedIssueGateMode: "block",
duplicatePrGateMode: "block",
qualityGateMode: "advisory",
qualityGateMinScore: null,
autoLabelEnabled: true,
Expand Down Expand Up @@ -436,8 +436,8 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
checkRunMode: settings.checkRunMode ?? "off",
checkRunDetailLevel: settings.checkRunDetailLevel ?? "minimal",
gateCheckMode: settings.gateCheckMode ?? "off",
linkedIssueGateMode: settings.linkedIssueGateMode ?? "advisory",
duplicatePrGateMode: settings.duplicatePrGateMode ?? "advisory",
linkedIssueGateMode: settings.linkedIssueGateMode ?? "block",
duplicatePrGateMode: settings.duplicatePrGateMode ?? "block",
qualityGateMode: settings.qualityGateMode ?? "advisory",
qualityGateMinScore: normalizeQualityGateMinScore(settings.qualityGateMinScore),
autoLabelEnabled: settings.autoLabelEnabled ?? true,
Expand Down
4 changes: 2 additions & 2 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export const repositorySettings = sqliteTable("repository_settings", {
checkRunMode: text("check_run_mode").notNull().default("off"),
checkRunDetailLevel: text("check_run_detail_level").notNull().default("minimal"),
gateCheckMode: text("gate_check_mode").notNull().default("off"),
linkedIssueGateMode: text("linked_issue_gate_mode").notNull().default("advisory"),
duplicatePrGateMode: text("duplicate_pr_gate_mode").notNull().default("advisory"),
linkedIssueGateMode: text("linked_issue_gate_mode").notNull().default("block"),
duplicatePrGateMode: text("duplicate_pr_gate_mode").notNull().default("block"),
qualityGateMode: text("quality_gate_mode").notNull().default("advisory"),
qualityGateMinScore: integer("quality_gate_min_score"),
autoLabelEnabled: integer("auto_label_enabled", { mode: "boolean" }).notNull().default(true),
Expand Down
2 changes: 1 addition & 1 deletion src/github/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export async function createOrUpdatePendingGateCheckRun(
output: {
title: "Gittensory Gate is evaluating",
summary: "Gittensory is running deterministic public PR hygiene checks.",
text: "The Gate is advisory-first unless this repository explicitly configures a rule to block merge.",
text: "The Gate preserves legacy linked-issue and duplicate-PR blockers unless this repository explicitly sets those rules to advisory or off.",
},
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,8 @@ function isEvaluationBlocker(code: string): boolean {
}

function isConfiguredGateBlocker(code: string, policy: GateCheckPolicy): boolean {
if (code === "missing_linked_issue") return gateMode(policy.linkedIssueGateMode) === "block";
if (code === "duplicate_pr_risk") return gateMode(policy.duplicatePrGateMode) === "block";
if (code === "missing_linked_issue") return gateMode(policy.linkedIssueGateMode ?? "block") === "block";
if (code === "duplicate_pr_risk") return gateMode(policy.duplicatePrGateMode ?? "block") === "block";
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion test/unit/github-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ describe("GitHub check runs", () => {
output: { title: "Gittensory Gate is evaluating" },
});
expect(capturedBody).not.toHaveProperty("conclusion");
expect(capturedBody.output?.text).toContain("advisory-first");
expect(capturedBody.output?.text).toContain("preserves legacy linked-issue and duplicate-PR blockers");
});

it("finalizes a known pending Gate check by id without listing check runs first", async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ describe("queue processors", () => {
);
});

it("publishes an opt-in gate check without requiring comment output", async () => {
it("publishes an opt-in gate check without requiring comment output while preserving linked-issue blockers", async () => {
const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem() });
await persistRegistrySnapshot(
env,
Expand Down Expand Up @@ -728,7 +728,7 @@ describe("queue processors", () => {
}
if (url.includes("/check-runs/900") && (init?.method ?? "GET") === "PATCH") {
const body = JSON.parse(String(init?.body ?? "{}")) as { name?: string; status?: string; conclusion?: string; output?: { title?: string } };
expect(body).toMatchObject({ name: "Gittensory Gate", status: "completed", conclusion: "success", output: { title: "Gittensory Gate passed" } });
expect(body).toMatchObject({ name: "Gittensory Gate", status: "completed", conclusion: "failure", output: { title: "Gittensory Gate is blocking merge" } });
calls.gateChecks += 1;
return Response.json({ id: 900 });
}
Expand Down
5 changes: 3 additions & 2 deletions test/unit/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe("advisory rules", () => {
expect(output.text).not.toMatch(/reward|wallet|trust score|score estimate/i);
});

it("keeps missing linked issue and duplicate PR findings advisory unless their gate modes block", () => {
it("keeps legacy Gate blockers by default while honoring explicit advisory or off modes", () => {
const pr: PullRequestRecord = {
repoFullName: repo.fullName,
number: 21,
Expand All @@ -195,7 +195,7 @@ describe("advisory rules", () => {
};
const missingIssueAdvisory = buildPullRequestAdvisory(repo, pr, { requireLinkedIssue: true });

expect(evaluateGateCheck(missingIssueAdvisory).conclusion).toBe("success");
expect(evaluateGateCheck(missingIssueAdvisory).conclusion).toBe("failure");
expect(evaluateGateCheck(missingIssueAdvisory, { linkedIssueGateMode: "advisory" }).conclusion).toBe("success");
expect(evaluateGateCheck(missingIssueAdvisory, { linkedIssueGateMode: "off" }).conclusion).toBe("success");
expect(evaluateGateCheck(missingIssueAdvisory, { linkedIssueGateMode: "block" }).conclusion).toBe("failure");
Expand All @@ -206,6 +206,7 @@ describe("advisory rules", () => {
});

expect(duplicateAdvisory.findings.map((finding) => finding.code)).toContain("duplicate_pr_risk");
expect(evaluateGateCheck(duplicateAdvisory).conclusion).toBe("failure");
expect(evaluateGateCheck(duplicateAdvisory, { duplicatePrGateMode: "advisory" }).conclusion).toBe("success");
expect(evaluateGateCheck(duplicateAdvisory, { duplicatePrGateMode: "off" }).conclusion).toBe("success");
expect(evaluateGateCheck(duplicateAdvisory, { duplicatePrGateMode: "block" }).conclusion).toBe("failure");
Expand Down