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
32 changes: 32 additions & 0 deletions src/gate/schema-change.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { gateSchemaChange } from "./schema-change.ts";

describe("gateSchemaChange", () => {
it("blocks a schema change by default", () => {
expect(gateSchemaChange({ changed: true })).toEqual({
conclusion: "failure",
message: expect.stringContaining("changes the database schema"),
});
});

it("softens to a non-blocking warning under a warn policy", () => {
expect(
gateSchemaChange({ changed: true }, { "schema-drift": "warn" })?.conclusion,
).toBe("neutral");
});

it("drops the gate under an off policy", () => {
expect(
gateSchemaChange({ changed: true }, { "schema-drift": "off" }),
).toBeNull();
});

it("passes when the run reports no schema change", () => {
expect(gateSchemaChange({ changed: false })).toBeNull();
});

it("passes when the API returned no schema-change signal", () => {
expect(gateSchemaChange(null)).toBeNull();
expect(gateSchemaChange(undefined)).toBeNull();
});
});
43 changes: 43 additions & 0 deletions src/gate/schema-change.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// The schema-change gate (Site#3289). The API already computes the real schema
// diff between the PR and the baseline (`runMetadata.schemaChange`); this gate
// only decides the check outcome from it. It blocks by default so a person
// validates the migration before merge — the value is a human eyeball on every
// schema change, independent of any cost delta. A repo softens it to `warn` or
// `off` via the schema-drift policy (#3500). Detection is not this gate's job.

import type { RepoPolicyConfig } from "@query-doctor/core";
import { resolveVerdict } from "./policy.ts";

/** The schema diff the API returns on a run (`runMetadata.schemaChange`). */
export interface SchemaChangeSignal {
changed: boolean;
}

export interface SchemaGateResult {
/** `failure` blocks the check; `neutral` is a surfaced, non-blocking warning. */
conclusion: "failure" | "neutral";
message: string;
}

const MESSAGE =
"This PR changes the database schema — validate the migration before merge. " +
"To stop schema changes from blocking this repo, set the schema-drift check " +
"to warn or off in CI settings.";

/**
* Decide the schema-change gate from the API's schema diff and the repo policy.
* Returns the check outcome when the PR changes the schema and the policy
* surfaces it, or `null` when there is no change or the policy is `off`.
*/
export function gateSchemaChange(
schemaChange: SchemaChangeSignal | null | undefined,
config: RepoPolicyConfig = {},
): SchemaGateResult | null {
if (!schemaChange?.changed) return null;
const { conclusion, surfaced } = resolveVerdict(
{ condition: "schema-drift", verdictClass: "finding" },
config,
);
if (!surfaced || conclusion === "success") return null;
return { conclusion, message: MESSAGE };
}
17 changes: 17 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { formatCost, queryPreview } from "./reporters/github/github.ts";
import { fetchPrChangedFiles } from "./gate/changed-files.ts";
import { evaluateTestPresence } from "./gate/test-presence.ts";
import { gateRegression } from "./gate/regression.ts";
import { gateSchemaChange } from "./gate/schema-change.ts";
import { resolveVerdict } from "./gate/policy.ts";
import { DEFAULT_CONFIG } from "./config.ts";
import { ApiClient } from "./remote/api-client.ts";
Expand Down Expand Up @@ -276,6 +277,22 @@ async function runInCI(
}
}

// Schema-change gate (Site#3289). The API already computed the schema diff
// (`runMetadata.schemaChange`) and the comment renders it; this only decides
// the check. Block by default so a person validates the migration; a repo
// softens it to `warn`/`off` via the schema-drift policy.
const schemaGate = gateSchemaChange(
reportContext.runMetadata?.schemaChange,
"conditionPolicies" in config ? config.conditionPolicies : undefined,
);
if (schemaGate) {
if (schemaGate.conclusion === "failure") {
core.setFailed(schemaGate.message);
} else {
core.warning(schemaGate.message);
}
}

// Block PR if regressions exceed thresholds, or if a brand-new query ships
// with a high-confidence index recommendation (#3281). New queries have no
// baseline so they can never regress; the new-query gate catches the missing
Expand Down
Loading