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
@@ -3,13 +3,22 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
import {
|
||||
parseSizeLimitResults,
|
||||
measureViaFileStat,
|
||||
runSizeLimit,
|
||||
evaluateBundleSizeRatchet,
|
||||
readBaselineBundleSizeValue,
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
} from "../../../scripts/check/check-bundle-size.mjs";
|
||||
|
||||
type RatchetVerdict = { regressed: boolean; improved: boolean };
|
||||
const evaluateSize = evaluateBundleSizeRatchet as (
|
||||
current: number,
|
||||
baseline: number
|
||||
) => RatchetVerdict;
|
||||
const readSizeBaseline = readBaselineBundleSizeValue as (p?: string) => number | null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseSizeLimitResults
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -123,7 +132,10 @@ test("measureViaFileStat: soma múltiplos arquivos existentes", () => {
|
||||
});
|
||||
|
||||
test("measureViaFileStat: config ausente retorna allMissing=true e total=0", () => {
|
||||
const { total, entries, allMissing } = measureViaFileStat("/tmp/nonexistent/.size-limit.json", "/tmp");
|
||||
const { total, entries, allMissing } = measureViaFileStat(
|
||||
"/tmp/nonexistent/.size-limit.json",
|
||||
"/tmp"
|
||||
);
|
||||
assert.equal(total, 0);
|
||||
assert.equal(allMissing, true);
|
||||
assert.deepEqual(entries, []);
|
||||
@@ -143,3 +155,75 @@ test("runSizeLimit: lança com code SL_NO_BIN quando binário não existe", () =
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// evaluateBundleSizeRatchet — ratchet direction:down (Etapa 2: flip to blocking)
|
||||
// Regression when measured > baseline; baseline=5601 (gzip via @size-limit/file).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("evaluateBundleSizeRatchet: medido == baseline passa (5601 vs 5601)", () => {
|
||||
const r = evaluateSize(5601, 5601);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, false);
|
||||
});
|
||||
|
||||
test("evaluateBundleSizeRatchet: um byte a mais que o baseline é regressão", () => {
|
||||
const r = evaluateSize(5602, 5601);
|
||||
assert.equal(r.regressed, true, "any size increase must block");
|
||||
assert.equal(r.improved, false);
|
||||
});
|
||||
|
||||
test("evaluateBundleSizeRatchet: menor que o baseline é melhoria", () => {
|
||||
const r = evaluateSize(5000, 5601);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, true);
|
||||
});
|
||||
|
||||
test("evaluateBundleSizeRatchet: comparação inteira estrita — qualquer aumento regride", () => {
|
||||
assert.equal(evaluateSize(5602, 5601).regressed, true);
|
||||
assert.equal(evaluateSize(5601, 5601).regressed, false);
|
||||
assert.equal(evaluateSize(5600, 5601).regressed, false);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// readBaselineBundleSizeValue — leitura tolerante do quality-baseline.json
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function withTmpBundleBaseline(content: string | null, fn: (p: string) => void) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-baseline-"));
|
||||
const p = path.join(dir, "quality-baseline.json");
|
||||
if (content !== null) fs.writeFileSync(p, content);
|
||||
try {
|
||||
fn(p);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("readBaselineBundleSizeValue: lê metrics.bundleSize.value", () => {
|
||||
withTmpBundleBaseline(JSON.stringify({ metrics: { bundleSize: { value: 5601 } } }), (p) => {
|
||||
assert.equal(readSizeBaseline(p), 5601);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineBundleSizeValue: arquivo ausente retorna null (SKIP gracioso)", () => {
|
||||
assert.equal(readSizeBaseline("/tmp/does-not-exist-77777/quality-baseline.json"), null);
|
||||
});
|
||||
|
||||
test("readBaselineBundleSizeValue: métrica ausente retorna null", () => {
|
||||
withTmpBundleBaseline(JSON.stringify({ metrics: {} }), (p) => {
|
||||
assert.equal(readSizeBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineBundleSizeValue: value não-numérico retorna null", () => {
|
||||
withTmpBundleBaseline(JSON.stringify({ metrics: { bundleSize: { value: "5601" } } }), (p) => {
|
||||
assert.equal(readSizeBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineBundleSizeValue: JSON inválido retorna null (não lança)", () => {
|
||||
withTmpBundleBaseline("not json at all", (p) => {
|
||||
assert.equal(readSizeBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,21 +6,34 @@
|
||||
// - parseGitleaksJson() — parses gitleaks findings array
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
import { parseGitleaksJson } from "../../../scripts/check/check-secrets.mjs";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
parseGitleaksJson,
|
||||
evaluateSecretsRatchet,
|
||||
readBaselineSecretsValue,
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
} from "../../../scripts/check/check-secrets.mjs";
|
||||
|
||||
type RatchetVerdict = { regressed: boolean; improved: boolean };
|
||||
const evaluate = evaluateSecretsRatchet as (current: number, baseline: number) => RatchetVerdict;
|
||||
const readBaseline = readBaselineSecretsValue as (p?: string) => number | null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures — synthetic gitleaks --report-format json output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Helper to build a minimal gitleaks finding. */
|
||||
function makeFinding(overrides: {
|
||||
ruleId?: string;
|
||||
file?: string;
|
||||
description?: string;
|
||||
startLine?: number;
|
||||
secret?: string;
|
||||
} = {}) {
|
||||
function makeFinding(
|
||||
overrides: {
|
||||
ruleId?: string;
|
||||
file?: string;
|
||||
description?: string;
|
||||
startLine?: number;
|
||||
secret?: string;
|
||||
} = {}
|
||||
) {
|
||||
return {
|
||||
Description: overrides.description ?? "GitHub Personal Access Token",
|
||||
StartLine: overrides.startLine ?? 42,
|
||||
@@ -178,11 +191,9 @@ test("parseGitleaksJson: suporta campo file (camelCase) como fallback", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("parseGitleaksJson: entradas null dentro do array são ignoradas", () => {
|
||||
const findings = [
|
||||
makeFinding(),
|
||||
null,
|
||||
makeFinding({ ruleId: "aws-access-key" }),
|
||||
] as (ReturnType<typeof makeFinding> | null)[];
|
||||
const findings = [makeFinding(), null, makeFinding({ ruleId: "aws-access-key" })] as (ReturnType<
|
||||
typeof makeFinding
|
||||
> | null)[];
|
||||
const result = parseGitleaksJson(findings as unknown as ReturnType<typeof makeFinding>[]);
|
||||
assert.equal(result.findingCount, 2, "null entries should be skipped");
|
||||
});
|
||||
@@ -240,3 +251,81 @@ test("parseGitleaksJson: findingCount == soma de todos os byFile values", () =>
|
||||
const sumByFile = Object.values(result.byFile).reduce((s, n) => s + n, 0);
|
||||
assert.equal(result.findingCount, sumByFile, "findingCount must equal sum of byFile counts");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// evaluateSecretsRatchet — ratchet direction:down (Etapa 2: flip to blocking)
|
||||
// Regression when measured > baseline; baseline=3 → 4+ findings block, 3 passes.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("evaluateSecretsRatchet: medida == baseline passa (3 vs 3)", () => {
|
||||
const r = evaluate(3, 3);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, false);
|
||||
});
|
||||
|
||||
test("evaluateSecretsRatchet: uma a mais que o baseline é regressão (4 vs 3)", () => {
|
||||
const r = evaluate(4, 3);
|
||||
assert.equal(r.regressed, true, "a single new secret finding must block");
|
||||
assert.equal(r.improved, false);
|
||||
});
|
||||
|
||||
test("evaluateSecretsRatchet: menos que o baseline é melhoria", () => {
|
||||
const r = evaluate(1, 3);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, true);
|
||||
});
|
||||
|
||||
test("evaluateSecretsRatchet: zero contra baseline não-zero é melhoria máxima", () => {
|
||||
const r = evaluate(0, 5);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, true);
|
||||
});
|
||||
|
||||
test("evaluateSecretsRatchet: comparação inteira estrita — qualquer aumento regride", () => {
|
||||
assert.equal(evaluate(6, 5).regressed, true);
|
||||
assert.equal(evaluate(5, 5).regressed, false);
|
||||
assert.equal(evaluate(4, 5).regressed, false);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// readBaselineSecretsValue — leitura tolerante do quality-baseline.json
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function withTmpBaseline(content: string | null, fn: (p: string) => void) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "secrets-baseline-"));
|
||||
const p = path.join(dir, "quality-baseline.json");
|
||||
if (content !== null) fs.writeFileSync(p, content);
|
||||
try {
|
||||
fn(p);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("readBaselineSecretsValue: lê metrics.secretFindings.value", () => {
|
||||
withTmpBaseline(JSON.stringify({ metrics: { secretFindings: { value: 3 } } }), (p) => {
|
||||
assert.equal(readBaseline(p), 3);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineSecretsValue: arquivo ausente retorna null (SKIP gracioso)", () => {
|
||||
assert.equal(readBaseline("/tmp/does-not-exist-99999/quality-baseline.json"), null);
|
||||
});
|
||||
|
||||
test("readBaselineSecretsValue: métrica ausente retorna null", () => {
|
||||
withTmpBaseline(JSON.stringify({ metrics: {} }), (p) => {
|
||||
assert.equal(readBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineSecretsValue: value não-numérico retorna null", () => {
|
||||
withTmpBaseline(JSON.stringify({ metrics: { secretFindings: { value: "3" } } }), (p) => {
|
||||
assert.equal(readBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineSecretsValue: JSON inválido retorna null (não lança)", () => {
|
||||
withTmpBaseline("{ not valid json", (p) => {
|
||||
assert.equal(readBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,23 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
import {
|
||||
parseActionlintOutput,
|
||||
parseZizmorOutput,
|
||||
collectWorkflowFiles,
|
||||
isBinaryAvailable,
|
||||
evaluateZizmorRatchet,
|
||||
readBaselineZizmorValue,
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
} from "../../../scripts/check/check-workflows.mjs";
|
||||
|
||||
type RatchetVerdict = { regressed: boolean; improved: boolean };
|
||||
const evaluateZizmor = evaluateZizmorRatchet as (
|
||||
current: number,
|
||||
baseline: number
|
||||
) => RatchetVerdict;
|
||||
const readZizmorBaseline = readBaselineZizmorValue as (p?: string) => number | null;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// parseActionlintOutput
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -52,7 +61,7 @@ test("parseActionlintOutput: one finding line returns count=1", () => {
|
||||
|
||||
test("parseActionlintOutput: multiple finding lines returns correct count", () => {
|
||||
const stdout = [
|
||||
".github/workflows/ci.yml:5:1: \"on\" is the key of workflow trigger. Use quoted \"on\" [syntax-check]",
|
||||
'.github/workflows/ci.yml:5:1: "on" is the key of workflow trigger. Use quoted "on" [syntax-check]',
|
||||
".github/workflows/ci.yml:42:9: event name 'pull_request' is not available for 'workflow_dispatch' [events]",
|
||||
".github/workflows/deploy.yml:8:5: unknown key 'runs-ons' in step config [syntax-check]",
|
||||
].join("\n");
|
||||
@@ -91,7 +100,11 @@ test("parseZizmorOutput: JSON with empty diagnostics array returns count=0", ()
|
||||
|
||||
test("parseZizmorOutput: JSON { diagnostics: [...] } counts correctly", () => {
|
||||
const diagnostics = [
|
||||
{ id: "unpinned-uses", severity: "medium", message: "uses: actions/checkout@v4 is not pinned to a SHA" },
|
||||
{
|
||||
id: "unpinned-uses",
|
||||
severity: "medium",
|
||||
message: "uses: actions/checkout@v4 is not pinned to a SHA",
|
||||
},
|
||||
{ id: "script-injection", severity: "high", message: "Untrusted input in run step" },
|
||||
];
|
||||
const result = parseZizmorOutput(JSON.stringify({ diagnostics }));
|
||||
@@ -223,3 +236,75 @@ test("isBinaryAvailable: node is available (sanity check for test environment)",
|
||||
// node must be in PATH for this test suite to even run.
|
||||
assert.equal(isBinaryAvailable("node"), true);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// evaluateZizmorRatchet — ratchet direction:down, zizmorFindings ONLY (Etapa 2)
|
||||
// Regression when measured > baseline. actionlint is reported, not ratcheted.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("evaluateZizmorRatchet: measured == baseline passes (192 vs 192)", () => {
|
||||
const r = evaluateZizmor(192, 192);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, false);
|
||||
});
|
||||
|
||||
test("evaluateZizmorRatchet: one more than baseline is a regression (193 vs 192)", () => {
|
||||
const r = evaluateZizmor(193, 192);
|
||||
assert.equal(r.regressed, true, "a single new zizmor finding must block");
|
||||
assert.equal(r.improved, false);
|
||||
});
|
||||
|
||||
test("evaluateZizmorRatchet: fewer than baseline is an improvement (190 vs 192)", () => {
|
||||
const r = evaluateZizmor(190, 192);
|
||||
assert.equal(r.regressed, false);
|
||||
assert.equal(r.improved, true);
|
||||
});
|
||||
|
||||
test("evaluateZizmorRatchet: strict integer comparison — any increase regresses", () => {
|
||||
assert.equal(evaluateZizmor(193, 192).regressed, true);
|
||||
assert.equal(evaluateZizmor(192, 192).regressed, false);
|
||||
assert.equal(evaluateZizmor(191, 192).regressed, false);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// readBaselineZizmorValue — tolerant read of quality-baseline.json
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function withTmpBaseline(content: string | null, fn: (p: string) => void) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "workflows-baseline-"));
|
||||
const p = path.join(dir, "quality-baseline.json");
|
||||
if (content !== null) fs.writeFileSync(p, content);
|
||||
try {
|
||||
fn(p);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("readBaselineZizmorValue: reads metrics.zizmorFindings.value", () => {
|
||||
withTmpBaseline(JSON.stringify({ metrics: { zizmorFindings: { value: 192 } } }), (p) => {
|
||||
assert.equal(readZizmorBaseline(p), 192);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineZizmorValue: missing file returns null (graceful SKIP)", () => {
|
||||
assert.equal(readZizmorBaseline("/tmp/does-not-exist-88888/quality-baseline.json"), null);
|
||||
});
|
||||
|
||||
test("readBaselineZizmorValue: missing metric returns null", () => {
|
||||
withTmpBaseline(JSON.stringify({ metrics: {} }), (p) => {
|
||||
assert.equal(readZizmorBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineZizmorValue: non-numeric value returns null", () => {
|
||||
withTmpBaseline(JSON.stringify({ metrics: { zizmorFindings: { value: "192" } } }), (p) => {
|
||||
assert.equal(readZizmorBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
test("readBaselineZizmorValue: invalid JSON returns null (does not throw)", () => {
|
||||
withTmpBaseline("{ broken", (p) => {
|
||||
assert.equal(readZizmorBaseline(p), null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user