mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 }[];
|
||||
|
||||
@@ -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} |`);
|
||||
|
||||
Reference in New Issue
Block a user