mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(release): post-merge quality gates to main for v3.8.26 (#3964)
Cherry-picks #3961 + #3962 from release/v3.8.26 to main (parity before tagging).
This commit is contained in:
committed by
GitHub
parent
81a37b67ed
commit
4d21044ba5
@@ -16,13 +16,23 @@
|
||||
// arquivos apontados são artefatos gerados), emite `bundleSize=SKIP reason=no-build`
|
||||
// e sai 0.
|
||||
//
|
||||
// Esta versão é ADVISORY: sempre sai 0 independente do resultado.
|
||||
// O ratchet (direction:down) é gerenciado pelo motor de quality-baseline.json na
|
||||
// integração com o CI (Task 12 INT).
|
||||
// Por default é ADVISORY: sempre sai 0 independente do resultado. Passe --ratchet
|
||||
// para tornar BLOQUEANTE: lê metrics.bundleSize.value de
|
||||
// config/quality/quality-baseline.json, compara o total MEDIDO e SAI 1 SE — E SOMENTE
|
||||
// SE — o medido for MAIOR que o baseline (regressão real, direction:down).
|
||||
//
|
||||
// IMPORTANTE: o baseline (5601) é o valor GZIP do size-limit + @size-limit/file
|
||||
// (instalado por 'npm ci' no CI). O modo FALLBACK-stat lê bytes CRUS (uma métrica
|
||||
// DIFERENTE e maior) — comparar fallback-stat contra o baseline gzip seria um falso-
|
||||
// positivo. Por isso o --ratchet SÓ bloqueia quando a medição veio do size-limit
|
||||
// REAL (plugin presente); o fallback-stat e o no-build são SKIP gracioso (exit 0)
|
||||
// mesmo com --ratchet — falta de plugin/build nunca bloqueia, só uma regressão
|
||||
// medida na MESMA métrica do baseline bloqueia.
|
||||
//
|
||||
// Uso:
|
||||
// node scripts/check/check-bundle-size.mjs
|
||||
// node scripts/check/check-bundle-size.mjs --json (força saída JSON de size-limit se possível)
|
||||
// node scripts/check/check-bundle-size.mjs --json (força saída JSON de size-limit se possível)
|
||||
// node scripts/check/check-bundle-size.mjs --ratchet (falha exit 1 numa regressão)
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
@@ -31,6 +41,8 @@ import { pathToFileURL, fileURLToPath } from "node:url";
|
||||
const ROOT = process.cwd();
|
||||
const SIZE_LIMIT_CONFIG = path.join(ROOT, ".size-limit.json");
|
||||
const SIZE_LIMIT_BIN = path.join(ROOT, "node_modules", ".bin", "size-limit");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/quality-baseline.json");
|
||||
const RATCHET = process.argv.includes("--ratchet");
|
||||
|
||||
/**
|
||||
* Tenta rodar size-limit --json e retorna o array de resultados.
|
||||
@@ -110,9 +122,7 @@ export function measureViaFileStat(configPath = SIZE_LIMIT_CONFIG, cwd = ROOT) {
|
||||
let found = 0;
|
||||
const entries = [];
|
||||
for (const entry of config) {
|
||||
const entryPath = path.isAbsolute(entry.path)
|
||||
? entry.path
|
||||
: path.join(cwd, entry.path);
|
||||
const entryPath = path.isAbsolute(entry.path) ? entry.path : path.join(cwd, entry.path);
|
||||
if (!fs.existsSync(entryPath)) {
|
||||
entries.push({ name: entry.name, path: entry.path, size: null });
|
||||
continue;
|
||||
@@ -125,6 +135,83 @@ export function measureViaFileStat(configPath = SIZE_LIMIT_CONFIG, cwd = ROOT) {
|
||||
return { total, entries, allMissing: found === 0 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ratchet (direction:down) — exported for tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Avalia o total MEDIDO de bytes contra o baseline.
|
||||
* Direction: down (o tamanho só pode CAIR — maior = regressão).
|
||||
*
|
||||
* @param {number} current - Total de bytes medido agora (gzip, via size-limit).
|
||||
* @param {number} baseline - Total congelado em quality-baseline.json.
|
||||
* @returns {{ regressed: boolean, improved: boolean }}
|
||||
*/
|
||||
export function evaluateBundleSizeRatchet(current, baseline) {
|
||||
return {
|
||||
regressed: current > baseline,
|
||||
improved: current < baseline,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lê metrics.bundleSize.value do quality-baseline.json.
|
||||
* Retorna null se o arquivo ou a métrica estiverem ausentes (sem baseline não há
|
||||
* ratchet possível — o caller trata como SKIP gracioso, exit 0).
|
||||
*
|
||||
* @param {string} baselinePath
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function readBaselineBundleSizeValue(baselinePath = BASELINE_PATH) {
|
||||
if (!fs.existsSync(baselinePath)) return null;
|
||||
let baselineJson;
|
||||
try {
|
||||
baselineJson = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const metric = baselineJson?.metrics?.bundleSize;
|
||||
if (!metric || typeof metric.value !== "number") return null;
|
||||
return metric.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica o ratchet (direction:down) sobre o total medido vs o baseline.
|
||||
* Sem --ratchet: advisory (exit 0). Com --ratchet + medição comparável (gzip via
|
||||
* size-limit): exit 1 numa regressão real (medido > baseline). Baseline ausente →
|
||||
* SKIP gracioso (exit 0). Define process.exitCode; não lança.
|
||||
*
|
||||
* @param {number} totalBytes - Total MEDIDO pelo size-limit (gzip).
|
||||
*/
|
||||
function applyRatchet(totalBytes) {
|
||||
if (!RATCHET) {
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const baselineValue = readBaselineBundleSizeValue(BASELINE_PATH);
|
||||
if (baselineValue === null) {
|
||||
console.log("[bundle-size] --ratchet: baseline ausente (metrics.bundleSize) — SKIP, sai 0.");
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const { regressed } = evaluateBundleSizeRatchet(totalBytes, baselineValue);
|
||||
if (regressed) {
|
||||
console.error(
|
||||
`[bundle-size] REGRESSÃO — ${totalBytes} bytes > baseline ${baselineValue}.\n` +
|
||||
" → Reduza o tamanho dos entrypoints, ou re-baseline metrics.bundleSize em\n" +
|
||||
" config/quality/quality-baseline.json se o crescimento for legítimo e justificado."
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[bundle-size] --ratchet OK — ${totalBytes} bytes, baseline ${baselineValue} (sem regressão).`
|
||||
);
|
||||
process.exitCode = 0;
|
||||
}
|
||||
|
||||
function main() {
|
||||
// Step 1: tenta com size-limit + plugin instalado
|
||||
let totalBytes = null;
|
||||
@@ -140,10 +227,13 @@ function main() {
|
||||
const { total, entries, allMissing } = measureViaFileStat(SIZE_LIMIT_CONFIG, ROOT);
|
||||
|
||||
if (allMissing) {
|
||||
// Step 3: skip gracioso — entradas não existem (build necessário)
|
||||
// Step 3: skip gracioso — entradas não existem (build necessário).
|
||||
// SKIP sai 0 mesmo com --ratchet (build ausente nunca bloqueia).
|
||||
console.log("bundleSize=SKIP reason=no-build");
|
||||
if (process.env.CI) {
|
||||
console.log("::notice::check-bundle-size skipped — entradas do .size-limit.json não encontradas (build necessário)");
|
||||
console.log(
|
||||
"::notice::check-bundle-size skipped — entradas do .size-limit.json não encontradas (build necessário)"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -158,7 +248,8 @@ function main() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Erro inesperado — reporta mas não falha (advisory)
|
||||
// Erro inesperado — reporta mas não falha (advisory).
|
||||
// SKIP sai 0 mesmo com --ratchet (erro de medição nunca bloqueia).
|
||||
console.error(`[bundle-size] Aviso: size-limit retornou erro inesperado: ${err.message}`);
|
||||
console.log("bundleSize=SKIP reason=size-limit-error");
|
||||
return;
|
||||
@@ -167,7 +258,28 @@ function main() {
|
||||
|
||||
const kb = (totalBytes / 1024).toFixed(2);
|
||||
console.log(`bundleSize=${totalBytes}`);
|
||||
console.log(`[bundle-size] ${mode}: total ${kb} KB (${totalBytes} bytes) — advisory, saindo 0`);
|
||||
|
||||
// O ratchet só pode comparar a MESMA métrica que congelou o baseline (gzip via
|
||||
// size-limit + @size-limit/file). O fallback-stat lê bytes CRUS — uma métrica
|
||||
// diferente e maior — então com --ratchet ele faz SKIP gracioso (exit 0) em vez
|
||||
// de um falso-positivo. Sem --ratchet, ambos os modos só reportam (advisory).
|
||||
if (mode !== "size-limit") {
|
||||
if (RATCHET) {
|
||||
console.log(
|
||||
`[bundle-size] ${mode}: total ${kb} KB (${totalBytes} bytes) — ` +
|
||||
"--ratchet SKIP (medição não-comparável ao baseline gzip; instale @size-limit/file)."
|
||||
);
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
console.log(`[bundle-size] ${mode}: total ${kb} KB (${totalBytes} bytes) — advisory, saindo 0`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RATCHET) {
|
||||
console.log(`[bundle-size] ${mode}: total ${kb} KB (${totalBytes} bytes) — advisory, saindo 0`);
|
||||
}
|
||||
applyRatchet(totalBytes);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();
|
||||
|
||||
@@ -10,13 +10,18 @@
|
||||
// secretFindings=N — número de findings do gitleaks
|
||||
// secretFindings=SKIP reason=binary-absent — gitleaks não está no PATH
|
||||
//
|
||||
// Esta versão é ADVISORY (sai 0 sempre). O ratchet (direction:down) é gerenciado
|
||||
// pelo motor quality-baseline.json no CI (Task 7.18 INT).
|
||||
// Por default é ADVISORY (sai 0 sempre). Passe --ratchet para tornar BLOQUEANTE:
|
||||
// lê metrics.secretFindings.value de config/quality/quality-baseline.json, compara
|
||||
// a contagem MEDIDA e SAI 1 SE — E SOMENTE SE — a medida for MAIOR que o baseline
|
||||
// (regressão real, direction:down). Qualquer SKIP gracioso (binário ausente, nenhum
|
||||
// dir de fonte) SAI 0 mesmo com --ratchet — falta de infraestrutura nunca bloqueia,
|
||||
// só uma regressão medida bloqueia.
|
||||
//
|
||||
// Uso:
|
||||
// node scripts/check/check-secrets.mjs
|
||||
// node scripts/check/check-secrets.mjs --json # imprime JSON bruto do gitleaks
|
||||
// node scripts/check/check-secrets.mjs --quiet # suprime logs de diagnóstico
|
||||
// node scripts/check/check-secrets.mjs --json # imprime JSON bruto do gitleaks
|
||||
// node scripts/check/check-secrets.mjs --quiet # suprime logs de diagnóstico
|
||||
// node scripts/check/check-secrets.mjs --ratchet # falha (exit 1) numa regressão
|
||||
|
||||
import fs from "node:fs";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
@@ -26,7 +31,9 @@ import { pathToFileURL } from "node:url";
|
||||
const ROOT = process.cwd();
|
||||
const QUIET = process.argv.includes("--quiet");
|
||||
const PRINT_JSON = process.argv.includes("--json");
|
||||
const RATCHET = process.argv.includes("--ratchet");
|
||||
const GITLEAKS_CONFIG = path.join(ROOT, ".gitleaks.toml");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/quality-baseline.json");
|
||||
|
||||
// Source directories to scan for secrets. We deliberately scope to the
|
||||
// production/source trees instead of scanning the whole working dir:
|
||||
@@ -105,6 +112,46 @@ export function parseGitleaksJson(gitleaksJson) {
|
||||
return { findingCount, byRule, byFile };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ratchet (direction:down) — exported for tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Avalia a contagem MEDIDA de secrets contra o baseline.
|
||||
* Direction: down (a contagem só pode CAIR — mais secrets = regressão).
|
||||
*
|
||||
* @param {number} current - Contagem de findings medida agora.
|
||||
* @param {number} baseline - Contagem congelada em quality-baseline.json.
|
||||
* @returns {{ regressed: boolean, improved: boolean }}
|
||||
*/
|
||||
export function evaluateSecretsRatchet(current, baseline) {
|
||||
return {
|
||||
regressed: current > baseline,
|
||||
improved: current < baseline,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lê metrics.secretFindings.value do quality-baseline.json.
|
||||
* Retorna null se o arquivo ou a métrica estiverem ausentes (sem baseline não há
|
||||
* ratchet possível — o caller trata como SKIP gracioso, exit 0).
|
||||
*
|
||||
* @param {string} baselinePath
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function readBaselineSecretsValue(baselinePath = BASELINE_PATH) {
|
||||
if (!fs.existsSync(baselinePath)) return null;
|
||||
let baselineJson;
|
||||
try {
|
||||
baselineJson = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const metric = baselineJson?.metrics?.secretFindings;
|
||||
if (!metric || typeof metric.value !== "number") return null;
|
||||
return metric.value;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Binary detection
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -156,7 +203,7 @@ function main() {
|
||||
process.stderr.write(
|
||||
"[check-secrets] SKIP — gitleaks não encontrado no PATH.\n" +
|
||||
"[check-secrets] Instale via: https://github.com/gitleaks/gitleaks\n" +
|
||||
"[check-secrets] ADVISORY — este gate sai 0 (ratchet entra no CI da Fase 7 INT).\n"
|
||||
"[check-secrets] SKIP gracioso — sai 0 mesmo com --ratchet (binário ausente nunca bloqueia).\n"
|
||||
);
|
||||
}
|
||||
process.exitCode = 0;
|
||||
@@ -271,12 +318,57 @@ function main() {
|
||||
} else {
|
||||
process.stderr.write("[check-secrets] Nenhum finding detectado.\n");
|
||||
}
|
||||
process.stderr.write(
|
||||
"[check-secrets] ADVISORY — esta versão não falha pela contagem (ratchet entra no CI).\n"
|
||||
);
|
||||
}
|
||||
|
||||
// Sai 0 sempre nesta versão (advisory)
|
||||
// Medição bem-sucedida → aplica o ratchet (bloqueante só com --ratchet).
|
||||
applyRatchet(findingCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica o ratchet (direction:down) sobre a contagem medida vs o baseline.
|
||||
* Sem --ratchet: advisory (exit 0). Com --ratchet: exit 1 numa regressão real
|
||||
* (medida > baseline). Baseline ausente → SKIP gracioso (exit 0).
|
||||
*
|
||||
* @param {number} findingCount - Contagem MEDIDA (medição bem-sucedida).
|
||||
*/
|
||||
function applyRatchet(findingCount) {
|
||||
if (!RATCHET) {
|
||||
if (!QUIET) {
|
||||
process.stderr.write(
|
||||
"[check-secrets] ADVISORY — não falha pela contagem (passe --ratchet para bloquear regressão).\n"
|
||||
);
|
||||
}
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const baselineValue = readBaselineSecretsValue(BASELINE_PATH);
|
||||
if (baselineValue === null) {
|
||||
if (!QUIET) {
|
||||
process.stderr.write(
|
||||
"[check-secrets] baseline ausente (metrics.secretFindings) — SKIP gracioso, sai 0.\n"
|
||||
);
|
||||
}
|
||||
process.exitCode = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const { regressed } = evaluateSecretsRatchet(findingCount, baselineValue);
|
||||
if (regressed) {
|
||||
process.stderr.write(
|
||||
`[check-secrets] REGRESSÃO — ${findingCount} secret findings > baseline ${baselineValue}\n` +
|
||||
" → Remova o novo secret (ou allowliste em .gitleaks.toml se for falso-positivo legítimo),\n" +
|
||||
" depois re-baseline metrics.secretFindings em config/quality/quality-baseline.json.\n"
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QUIET) {
|
||||
process.stderr.write(
|
||||
`[check-secrets] OK — sem regressão (${findingCount} findings, baseline ${baselineValue}).\n`
|
||||
);
|
||||
}
|
||||
process.exitCode = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,22 @@
|
||||
// zizmorFindings=<n> — findings from zizmor alone
|
||||
//
|
||||
// Exit codes:
|
||||
// 0 — SKIP (binary absent) or all tools passed (0 findings each)
|
||||
// 1 — one or more findings (gate failure; advisory mode = always 0; see --advisory)
|
||||
// 0 — SKIP (binary absent) or all tools passed / no ratchet regression
|
||||
// 1 — gate failure: --strict + any finding, OR --ratchet + zizmorFindings
|
||||
// regression (measured > baseline)
|
||||
//
|
||||
// Ratchet mode (--ratchet): reads metrics.zizmorFindings.value from
|
||||
// config/quality/quality-baseline.json and exits 1 IF — AND ONLY IF — the MEASURED
|
||||
// zizmor count is GREATER than the baseline (real regression, direction:down).
|
||||
// ONLY zizmorFindings is ratcheted; actionlint findings are REPORTED but NOT
|
||||
// ratcheted (use the separate --strict all-or-nothing flag for those). Any graceful
|
||||
// SKIP (binary absent, no workflows) exits 0 even with --ratchet — missing infra
|
||||
// never blocks, only a measured regression does.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/check/check-workflows.mjs # advisory (exit 0 always)
|
||||
// node scripts/check/check-workflows.mjs --strict # fail on any finding
|
||||
// node scripts/check/check-workflows.mjs --ratchet # fail on zizmor regression
|
||||
// node scripts/check/check-workflows.mjs --quiet # suppress progress logs
|
||||
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
@@ -36,8 +46,10 @@ import { pathToFileURL } from "node:url";
|
||||
const ROOT = process.cwd();
|
||||
const WORKFLOWS_DIR = path.join(ROOT, ".github", "workflows");
|
||||
const ZIZMOR_CONFIG = path.join(ROOT, ".zizmor.yml");
|
||||
const BASELINE_PATH = path.join(ROOT, "config/quality/quality-baseline.json");
|
||||
|
||||
const STRICT = process.argv.includes("--strict");
|
||||
const RATCHET = process.argv.includes("--ratchet");
|
||||
const QUIET = process.argv.includes("--quiet");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -129,6 +141,46 @@ export function parseZizmorOutput(stdout) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ratchet (direction:down, zizmorFindings only) — exported for tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Evaluates the MEASURED zizmor finding count against the baseline.
|
||||
* Direction: down (the count may only DROP — more findings = regression).
|
||||
*
|
||||
* @param {number} current - Measured zizmor finding count.
|
||||
* @param {number} baseline - Frozen count in quality-baseline.json.
|
||||
* @returns {{ regressed: boolean, improved: boolean }}
|
||||
*/
|
||||
export function evaluateZizmorRatchet(current, baseline) {
|
||||
return {
|
||||
regressed: current > baseline,
|
||||
improved: current < baseline,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads metrics.zizmorFindings.value from quality-baseline.json.
|
||||
* Returns null when the file or metric is missing (no baseline → no ratchet
|
||||
* possible; the caller treats this as a graceful SKIP, exit 0).
|
||||
*
|
||||
* @param {string} baselinePath
|
||||
* @returns {number|null}
|
||||
*/
|
||||
export function readBaselineZizmorValue(baselinePath = BASELINE_PATH) {
|
||||
if (!fs.existsSync(baselinePath)) return null;
|
||||
let baselineJson;
|
||||
try {
|
||||
baselineJson = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const metric = baselineJson?.metrics?.zizmorFindings;
|
||||
if (!metric || typeof metric.value !== "number") return null;
|
||||
return metric.value;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runner helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -216,7 +268,7 @@ function main() {
|
||||
" Install them to enable workflow linting and security audit:\n" +
|
||||
" • actionlint: https://github.com/rhysd/actionlint\n" +
|
||||
" • zizmor: https://github.com/woodruffw/zizmor\n" +
|
||||
" This gate is advisory — the build is not blocked."
|
||||
" Graceful SKIP — exits 0 even with --ratchet (missing binaries never block)."
|
||||
);
|
||||
process.stdout.write("workflowFindings=SKIP\n");
|
||||
process.exit(0);
|
||||
@@ -226,7 +278,9 @@ function main() {
|
||||
|
||||
if (workflowFiles.length === 0) {
|
||||
if (!QUIET) {
|
||||
console.log(`[check-workflows] No workflow files found in ${WORKFLOWS_DIR} — nothing to check.`);
|
||||
console.log(
|
||||
`[check-workflows] No workflow files found in ${WORKFLOWS_DIR} — nothing to check.`
|
||||
);
|
||||
}
|
||||
process.stdout.write("workflowFindings=0\nactionlintFindings=0\nzizmorFindings=0\n");
|
||||
process.exit(0);
|
||||
@@ -285,16 +339,56 @@ function main() {
|
||||
process.stdout.write(`zizmorFindings=${zizmorCount}\n`);
|
||||
|
||||
if (STRICT && total > 0) {
|
||||
console.error(
|
||||
`\n[check-workflows] FAIL — ${total} workflow finding(s) total (--strict mode).`
|
||||
);
|
||||
console.error(`\n[check-workflows] FAIL — ${total} workflow finding(s) total (--strict mode).`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── ratchet (zizmorFindings only, direction:down) ──────────────────────────
|
||||
// We can only ratchet zizmor when zizmor actually RAN (binary present). If
|
||||
// zizmor is absent we have no comparable measurement → graceful SKIP (exit 0):
|
||||
// a missing binary must never block, only a measured regression does.
|
||||
if (RATCHET) {
|
||||
if (!hasZizmor) {
|
||||
if (!QUIET) {
|
||||
process.stderr.write(
|
||||
"[check-workflows] --ratchet: zizmor absent — SKIP (no measurement, never blocks).\n"
|
||||
);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const baselineValue = readBaselineZizmorValue(BASELINE_PATH);
|
||||
if (baselineValue === null) {
|
||||
if (!QUIET) {
|
||||
process.stderr.write(
|
||||
"[check-workflows] --ratchet: baseline absent (metrics.zizmorFindings) — SKIP, exit 0.\n"
|
||||
);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { regressed } = evaluateZizmorRatchet(zizmorCount, baselineValue);
|
||||
if (regressed) {
|
||||
console.error(
|
||||
`\n[check-workflows] REGRESSION — ${zizmorCount} zizmor finding(s) > baseline ${baselineValue}.\n` +
|
||||
" → Fix the new workflow finding(s), or re-baseline metrics.zizmorFindings in\n" +
|
||||
" config/quality/quality-baseline.json if the rise is a legitimate, justified drift.\n" +
|
||||
" (actionlint findings are reported, not ratcheted — use --strict for those.)"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!QUIET) {
|
||||
process.stderr.write(
|
||||
`[check-workflows] --ratchet OK — ${zizmorCount} zizmor finding(s), baseline ${baselineValue} (no regression).\n`
|
||||
);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (total > 0 && !QUIET) {
|
||||
console.log(
|
||||
`[check-workflows] ADVISORY — ${total} finding(s) detected. ` +
|
||||
"Pass --strict to block the gate."
|
||||
"Pass --strict to block on any finding, or --ratchet to block on a zizmor regression."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user