ci(server): fail the Postgres probe on an unmeasured baseline

The probe shipped with scripts/pg-probe-baseline.json at `failing: null`,
and an unmeasured baseline only reported. Every repository statement
Postgres refused, new SQLite-only SQL included, still passed the job;
only the dialect helper cases could fail it, so the ratchet was off until
some later commit seeded the file.

An unmeasured baseline now fails the run outside --update, and the log,
step summary and annotation say which artifact file to commit. The job
stays red until the first measurement lands, so the gate cannot reach dev
holding nothing. A run that called no repository method (an unreadable
tsconfig, a moved repositories directory) fails as well, also under
--update, so a broken plan can neither pass nor seed an empty baseline.
This commit is contained in:
Maurice
2026-10-08 16:31:32 +02:00
parent b8144bb92a
commit 12bf6c0da3
8 changed files with 102 additions and 25 deletions
+20 -6
View File
@@ -29,6 +29,11 @@
* --url=<postgres url> instead of TREK_PG_PROBE_URL
* --update lower scripts/pg-probe-baseline.json to this run (never raises or adds an entry;
* an unmeasured baseline gets its first measurement)
*
* A baseline whose `failing` is `null` has never been measured and fails the
* run (outside `--update`); the baseline the run writes with
* `--next-baseline` (the CI artifact) is the first measurement to commit. A
* run that called no repository method fails as well.
* --next-baseline=<path> write the baseline this run would leave (the CI artifact)
* --report=<path> write everything the run saw as JSON
* --summary=<path> append the Markdown summary (defaults to $GITHUB_STEP_SUMMARY)
@@ -45,7 +50,7 @@ import { compareWithBaseline, formatBaseline, lowerBaseline, readBaseline } from
import { connectProbeOrm, connectSetupOrm, postgresHelperEngine, prepareDatabase, probeRepositories, sqliteHelperEngine } from './pg-probe/engines';
import { runHelperCases, type HelperResult } from './pg-probe/helper-cases';
import { StatementRecorder } from './pg-probe/recorder';
import { formatConsole, formatMarkdown, passed, summarize, type MethodResult, type ReportInput } from './pg-probe/report';
import { formatConsole, formatMarkdown, passed, probedNothing, summarize, type MethodResult, type ReportInput } from './pg-probe/report';
const SERVER_ROOT = path.join(__dirname, '..');
export const BASELINE_PATH = path.join(SERVER_ROOT, 'scripts', 'pg-probe-baseline.json');
@@ -96,14 +101,16 @@ export function repositoryFiles(dir: string): string[] {
}
/**
* 0 when every helper case passed and no method fails more statements than
* 0 when every helper case passed, the probe called at least one repository
* method, the baseline is measured and no method fails more statements than
* its entry allows. An entry above what its method fails now fails the run
* too, unless this run is the `--update` that lowers it.
* too, and so does an unmeasured baseline, unless this run is the `--update`
* that lowers or seeds it.
*/
export function exitCode(input: Pick<ReportInput, 'verdict' | 'helpers'>, update: boolean): number {
export function exitCode(input: Pick<ReportInput, 'verdict' | 'helpers' | 'summary'>, update: boolean): number {
if (passed(input)) return 0;
const helpersOk = input.helpers.every((result) => result.failure === null);
return helpersOk && input.verdict.grown.length === 0 && update ? 0 : 1;
return helpersOk && !probedNothing(input.summary) && input.verdict.grown.length === 0 && update ? 0 : 1;
}
/** GitHub annotations when running in Actions, plain lines otherwise. */
@@ -186,7 +193,14 @@ async function main(): Promise<number> {
'Lower it (--update, or the pg-probe-baseline artifact).',
);
}
if (verdict.unseeded) annotate('notice', 'pg-probe: the baseline has not been measured yet; the pg-probe-baseline artifact holds the first one.');
if (probedNothing(summary)) annotate('error', `pg-probe: no repository method was called (${summary.methods} planned).`);
if (verdict.unseeded) {
annotate(
'error',
'pg-probe: the baseline has not been measured yet. Commit pg-probe-baseline.json from the pg-probe-baseline artifact ' +
'as server/scripts/pg-probe-baseline.json.',
);
}
for (const failure of schemaFailures) annotate('warning', `pg-probe schema: ${failure.message}`);
return exitCode(input, args.update);
}
+4 -3
View File
@@ -10,8 +10,9 @@
* `--update` lowers entries and drops the ones that reach zero or whose
* method is gone; it never raises or adds one. The only exception is the
* first run: a baseline whose `failing` is `null` has never been measured,
* so the check reports and passes, and `--update` (or the CI artifact)
* writes the first measurement.
* so there is nothing to compare and the run fails (scripts/pg-probe.ts)
* until the first measurement, written by `--update` or taken from the CI
* artifact, is committed.
*/
import { readFileSync } from 'node:fs';
@@ -22,7 +23,7 @@ export interface ProbeBaseline {
}
export interface BaselineVerdict {
/** The baseline has never been measured: report only. */
/** The baseline has never been measured: nothing to compare, and the run fails until it is seeded. */
unseeded: boolean;
/** Methods failing more statements than their entry allows. */
grown: { method: string; allowed: number; now: number }[];
+36 -5
View File
@@ -101,9 +101,28 @@ export function helperFailures(helpers: readonly HelperResult[]): HelperResult[]
return helpers.filter((result) => result.failure !== null);
}
/** True when the run passes: every helper case on both engines, and the ratchet. */
export function passed(input: Pick<ReportInput, 'verdict' | 'helpers'>): boolean {
return helperFailures(input.helpers).length === 0 && input.verdict.grown.length === 0 && input.verdict.stale.length === 0;
/**
* True when the probe called no repository method at all. A broken plan (an
* unreadable tsconfig, a moved repositories directory) would otherwise pass
* with nothing failing, and its empty measurement could seed the baseline.
*/
export function probedNothing(summary: Pick<ProbeSummary, 'called'>): boolean {
return summary.called === 0;
}
/**
* True when the run passes: every helper case on both engines, at least one
* repository method called, a measured baseline, and the ratchet held. An
* unmeasured baseline fails, so the gate cannot merge before it holds anything.
*/
export function passed(input: Pick<ReportInput, 'verdict' | 'helpers' | 'summary'>): boolean {
return (
helperFailures(input.helpers).length === 0 &&
!probedNothing(input.summary) &&
!input.verdict.unseeded &&
input.verdict.grown.length === 0 &&
input.verdict.stale.length === 0
);
}
export function formatConsole(input: ReportInput): string {
@@ -128,8 +147,14 @@ export function formatConsole(input: ReportInput): string {
lines.push(` ${failed.method} ${failed.code} ${failed.message}`);
lines.push(` ${oneLine(failed.sql)}`);
}
if (probedNothing(summary)) {
lines.push(`FAIL No repository method was called (${summary.methods} planned); there is nothing to hold to the baseline.`);
}
if (verdict.unseeded) {
lines.push('Baseline: not measured yet (failing is null). This run passes and writes the first measurement as the next baseline.');
lines.push(
'FAIL Baseline: not measured yet (failing is null). Commit the next baseline this run wrote ' +
'(the pg-probe-baseline artifact in CI) as scripts/pg-probe-baseline.json.',
);
}
for (const entry of verdict.grown) {
lines.push(`FAIL ${entry.method} sends ${entry.now} statement(s) Postgres refuses, ${entry.allowed} allowed.`);
@@ -153,7 +178,13 @@ export function formatMarkdown(input: ReportInput): string {
out.push(`| Distinct statements | ${summary.statements} |`);
out.push(`| Refused by Postgres | ${summary.failedStatements.length} |`);
out.push('');
if (verdict.unseeded) out.push('The baseline has not been measured yet; the `pg-probe-baseline` artifact holds the first one.', '');
if (probedNothing(summary)) out.push('No repository method was called, so there is nothing to hold to the baseline.', '');
if (verdict.unseeded) {
out.push(
'The baseline has not been measured yet, which fails the run. Commit `pg-probe-baseline.json` from the `pg-probe-baseline` artifact as `server/scripts/pg-probe-baseline.json`.',
'',
);
}
if (verdict.grown.length > 0 || verdict.stale.length > 0) {
out.push('### Ratchet', '', '| Method | Allowed | Now |', '|---|---|---|');
for (const entry of [...verdict.grown, ...verdict.stale]) out.push(`| ${cell(entry.method)} | ${entry.allowed} | ${entry.now} |`);