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:
Diego Rodrigues de Sa e Souza
2026-06-16 02:33:19 -03:00
committed by GitHub
parent 81a37b67ed
commit 4d21044ba5
9 changed files with 642 additions and 64 deletions

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});