mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
ci(quality): unblock v3.8.27 release gates (zizmor pin + test-masking allowlist)
- zizmor ratchet (151→139, no regression): SHA-pin every action ref ADDED this cycle — codeql/dast-smoke/semgrep (3 new workflows) + trivy-action (docker-publish) + actions/cache (nightly-mutation). Pre-existing tag refs keep the repo convention. - test-masking: add config/quality/test-masking-allowlist.json + allowlist support in check-test-masking.mjs (exempts ONLY the net-assert-reduction signal; tautology/skip/ deletion still fire). Allowlists 2 verified-legitimate reductions: appearance-widget-settings-schema (#4033 removed showTokenSaverOnEndpoint field) and dashboard-shell-tabs (#3973 tabs→redirect refactor, asserts replaced). +4 gate tests.
This commit is contained in:
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -19,13 +19,13 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: github/codeql-action/init@v3
|
||||
- uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
queries: security-extended
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
- uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
8
.github/workflows/dast-smoke.yml
vendored
8
.github/workflows/dast-smoke.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
if curl -sf http://localhost:20128/api/monitoring/health >/dev/null; then echo up; break; fi
|
||||
sleep 2
|
||||
done
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: pip install schemathesis
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
- name: Stop server
|
||||
if: always()
|
||||
run: kill "$(cat server.pid)" || true
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: always()
|
||||
with:
|
||||
name: dast-smoke-logs
|
||||
|
||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -347,7 +347,7 @@ jobs:
|
||||
- name: Trivy image scan (SARIF, advisory)
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
continue-on-error: true
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||
with:
|
||||
image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
format: sarif
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
# note in docs/security/SUPPLY_CHAIN.md).
|
||||
- name: Trivy CRITICAL gate (blocking)
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||
with:
|
||||
image-ref: ${{ env.GHCR_IMAGE_NAME }}:${{ env.VERSION }}
|
||||
format: table
|
||||
|
||||
2
.github/workflows/nightly-mutation.yml
vendored
2
.github/workflows/nightly-mutation.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Restore Stryker incremental cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: reports/mutation/stryker-incremental.json
|
||||
key: stryker-incremental-${{ github.run_id }}
|
||||
|
||||
4
.github/workflows/semgrep.yml
vendored
4
.github/workflows/semgrep.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run semgrep (advisory)
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
semgrep scan --config p/owasp-top-ten --config p/secrets \
|
||||
--sarif --output semgrep.sarif --metrics off || true
|
||||
python -c "import json; d=json.load(open('semgrep.sarif')); print('semgrepFindings=%d' % len(d['runs'][0]['results']))" || echo "semgrepFindings=SKIP"
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
if: always()
|
||||
with:
|
||||
name: semgrep-sarif
|
||||
|
||||
5
config/quality/test-masking-allowlist.json
Normal file
5
config/quality/test-masking-allowlist.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"_comment": "Anti test-masking allowlist (check-test-masking.mjs). Files here are EXEMPT ONLY from the net-assert-REDUCTION signal, when the reduction is a verified-legitimate refactor or field removal (NOT weakening to go green). New tautologies (assert.ok(true)), new .skip/.todo/.only, and test-file deletions are STILL flagged for these files. Every entry needs a reason with the PR ref. Prune an entry once the release that introduced the reduction has merged to main (the merge-base then already reflects the reduced state, so the signal no longer fires).",
|
||||
"tests/unit/appearance-widget-settings-schema.test.ts": "v3.8.27 #4033: the `showTokenSaverOnEndpoint` schema field was removed (renamed/consolidated into the settings surface); its 2 asserts were removed accordingly — the field no longer exists in source. Verified legitimate, not masking.",
|
||||
"tests/unit/dashboard-shell-tabs.test.ts": "v3.8.27 #3973: settings UI refactored from a tabbed client component to redirect-based routing; 5 old-structure asserts (tabpanel/aria/initialTab) were replaced by 4 new-structure asserts (redirect/resolveSettingsRoute), net -1. Asserts replaced, not weakened. Verified legitimate."
|
||||
}
|
||||
@@ -66,7 +66,9 @@ export function evaluateDeletedFiles(deletedPaths) {
|
||||
const flags = [];
|
||||
for (const f of deletedPaths) {
|
||||
if (TEST_RE.test(f)) {
|
||||
flags.push(`${f}: arquivo de teste deletado — revisão humana obrigatória (mascaramento alto-sinal)`);
|
||||
flags.push(
|
||||
`${f}: arquivo de teste deletado — revisão humana obrigatória (mascaramento alto-sinal)`
|
||||
);
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
@@ -83,7 +85,7 @@ export function evaluateDeletedFiles(deletedPaths) {
|
||||
* Os campos de skip e extTaut são opcionais (default 0) para compatibilidade
|
||||
* com chamadas legadas que só passam baseAsserts/headAsserts/baseTaut/headTaut.
|
||||
*/
|
||||
export function evaluateMasking(perFile) {
|
||||
export function evaluateMasking(perFile, assertReductionAllowlist = new Set()) {
|
||||
const flags = [];
|
||||
for (const f of perFile) {
|
||||
const baseSkips = f.baseSkips ?? 0;
|
||||
@@ -91,14 +93,23 @@ export function evaluateMasking(perFile) {
|
||||
const baseExtTaut = f.baseExtTaut ?? 0;
|
||||
const headExtTaut = f.headExtTaut ?? 0;
|
||||
|
||||
if (f.headAsserts < f.baseAsserts)
|
||||
flags.push(`${f.file}: asserts ${f.baseAsserts} → ${f.headAsserts} (REMOÇÃO de ${f.baseAsserts - f.headAsserts} — enfraquecimento?)`);
|
||||
// The net-assert-REDUCTION signal can be allowlisted per file when the reduction is a
|
||||
// verified-legitimate refactor/field-removal (config/quality/test-masking-allowlist.json).
|
||||
// The tautology / skip / deletion signals below are NEVER allowlisted.
|
||||
if (f.headAsserts < f.baseAsserts && !assertReductionAllowlist.has(f.file))
|
||||
flags.push(
|
||||
`${f.file}: asserts ${f.baseAsserts} → ${f.headAsserts} (REMOÇÃO de ${f.baseAsserts - f.headAsserts} — enfraquecimento?)`
|
||||
);
|
||||
if (f.headTaut > f.baseTaut)
|
||||
flags.push(`${f.file}: nova(s) ${f.headTaut - f.baseTaut} tautologia(s) assert.ok(true)`);
|
||||
if (headSkips > baseSkips)
|
||||
flags.push(`${f.file}: ${headSkips - baseSkips} novo(s) .skip/.todo/.only (asserts silenciados sem remoção)`);
|
||||
flags.push(
|
||||
`${f.file}: ${headSkips - baseSkips} novo(s) .skip/.todo/.only (asserts silenciados sem remoção)`
|
||||
);
|
||||
if (headExtTaut > baseExtTaut)
|
||||
flags.push(`${f.file}: nova(s) ${headExtTaut - baseExtTaut} tautologia(s) estendida(s) (expect(true).toBe(true) / assert.equal(1,1))`);
|
||||
flags.push(
|
||||
`${f.file}: nova(s) ${headExtTaut - baseExtTaut} tautologia(s) estendida(s) (expect(true).toBe(true) / assert.equal(1,1))`
|
||||
);
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
@@ -125,10 +136,7 @@ function main() {
|
||||
}
|
||||
|
||||
// (6A.10 subcheck 1) Arquivos de teste deletados/renomeados via MDR filter
|
||||
const deletedAndRenamed = git([
|
||||
"diff", "--name-only", "--diff-filter=DR", "-M",
|
||||
`${base}...HEAD`,
|
||||
])
|
||||
const deletedAndRenamed = git(["diff", "--name-only", "--diff-filter=DR", "-M", `${base}...HEAD`])
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
@@ -158,7 +166,17 @@ function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const maskingFlags = evaluateMasking(perFile);
|
||||
// Per-file allowlist for verified-legitimate net-assert reductions (refactor/field-removal).
|
||||
// Only exempts the reduction signal; tautology/skip/deletion signals still fire.
|
||||
let assertReductionAllowlist = new Set();
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync("config/quality/test-masking-allowlist.json", "utf8"));
|
||||
assertReductionAllowlist = new Set(Object.keys(raw).filter((k) => !k.startsWith("_")));
|
||||
} catch {
|
||||
// no allowlist file — treat as empty
|
||||
}
|
||||
|
||||
const maskingFlags = evaluateMasking(perFile, assertReductionAllowlist);
|
||||
const allFlags = [...deletedFlags, ...maskingFlags];
|
||||
|
||||
if (allFlags.length) {
|
||||
@@ -171,7 +189,7 @@ function main() {
|
||||
}
|
||||
console.log(
|
||||
`[test-masking] OK — ${changed.length} arquivo(s) de teste modificado(s), ` +
|
||||
`${deletedAndRenamed.length > 0 ? deletedAndRenamed.length + " deletado(s)/renomeado(s) OK" : "nenhum deletado"} — sem enfraquecimento`
|
||||
`${deletedAndRenamed.length > 0 ? deletedAndRenamed.length + " deletado(s)/renomeado(s) OK" : "nenhum deletado"} — sem enfraquecimento`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,18 +21,54 @@ test("countTautologies counts assert.ok(true)", () => {
|
||||
});
|
||||
|
||||
test("net removal of assertions in a changed test file is flagged", () => {
|
||||
const r = evaluateMasking([{ file: "a.test.ts", baseAsserts: 5, headAsserts: 3, baseTaut: 0, headTaut: 0, baseSkips: 0, headSkips: 0, baseExtTaut: 0, headExtTaut: 0 }]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 3,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 0,
|
||||
headSkips: 0,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
]);
|
||||
assert.equal(r.length, 1);
|
||||
assert.match(r[0], /a\.test\.ts/);
|
||||
});
|
||||
|
||||
test("adding assertions is not flagged", () => {
|
||||
const r = evaluateMasking([{ file: "a.test.ts", baseAsserts: 5, headAsserts: 7, baseTaut: 0, headTaut: 0, baseSkips: 0, headSkips: 0, baseExtTaut: 0, headExtTaut: 0 }]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 7,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 0,
|
||||
headSkips: 0,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(r, []);
|
||||
});
|
||||
|
||||
test("new assert.ok(true) tautology is flagged even if assert count is stable", () => {
|
||||
const r = evaluateMasking([{ file: "a.test.ts", baseAsserts: 5, headAsserts: 5, baseTaut: 0, headTaut: 1, baseSkips: 0, headSkips: 0, baseExtTaut: 0, headExtTaut: 0 }]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 5,
|
||||
baseTaut: 0,
|
||||
headTaut: 1,
|
||||
baseSkips: 0,
|
||||
headSkips: 0,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
]);
|
||||
assert.equal(r.length, 1);
|
||||
assert.match(r[0], /tautolog/i);
|
||||
});
|
||||
@@ -84,37 +120,55 @@ test("countSkips returns 0 for clean test file", () => {
|
||||
});
|
||||
|
||||
test("evaluateMasking: net increase in skips is flagged", () => {
|
||||
const r = evaluateMasking([{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5, headAsserts: 5,
|
||||
baseTaut: 0, headTaut: 0,
|
||||
baseSkips: 1, headSkips: 3,
|
||||
baseExtTaut: 0, headExtTaut: 0,
|
||||
}]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 5,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 1,
|
||||
headSkips: 3,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
]);
|
||||
assert.equal(r.length, 1);
|
||||
assert.match(r[0], /skip|todo|only/i);
|
||||
});
|
||||
|
||||
test("evaluateMasking: net decrease in skips (fixes) is not flagged", () => {
|
||||
const r = evaluateMasking([{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5, headAsserts: 5,
|
||||
baseTaut: 0, headTaut: 0,
|
||||
baseSkips: 3, headSkips: 1,
|
||||
baseExtTaut: 0, headExtTaut: 0,
|
||||
}]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 5,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 3,
|
||||
headSkips: 1,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(r, []);
|
||||
});
|
||||
|
||||
test("evaluateMasking: adding .only is flagged (filters rest of suite)", () => {
|
||||
// .only additions are captured by countSkips net increase
|
||||
const r = evaluateMasking([{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 10, headAsserts: 10,
|
||||
baseTaut: 0, headTaut: 0,
|
||||
baseSkips: 0, headSkips: 1,
|
||||
baseExtTaut: 0, headExtTaut: 0,
|
||||
}]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 10,
|
||||
headAsserts: 10,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 0,
|
||||
headSkips: 1,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
]);
|
||||
assert.equal(r.length, 1);
|
||||
});
|
||||
|
||||
@@ -159,24 +213,78 @@ test("countExtendedTautologies: handles whitespace variants", () => {
|
||||
});
|
||||
|
||||
test("evaluateMasking: new extended tautology is flagged", () => {
|
||||
const r = evaluateMasking([{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5, headAsserts: 5,
|
||||
baseTaut: 0, headTaut: 0,
|
||||
baseSkips: 0, headSkips: 0,
|
||||
baseExtTaut: 0, headExtTaut: 1,
|
||||
}]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 5,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 0,
|
||||
headSkips: 0,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 1,
|
||||
},
|
||||
]);
|
||||
assert.equal(r.length, 1);
|
||||
assert.match(r[0], /tautolog/i);
|
||||
});
|
||||
|
||||
test("evaluateMasking: no new extended tautology is not flagged", () => {
|
||||
const r = evaluateMasking([{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5, headAsserts: 5,
|
||||
baseTaut: 0, headTaut: 0,
|
||||
baseSkips: 0, headSkips: 0,
|
||||
baseExtTaut: 1, headExtTaut: 1,
|
||||
}]);
|
||||
const r = evaluateMasking([
|
||||
{
|
||||
file: "a.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 5,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 0,
|
||||
headSkips: 0,
|
||||
baseExtTaut: 1,
|
||||
headExtTaut: 1,
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(r, []);
|
||||
});
|
||||
|
||||
test("evaluateMasking: net reduction is NOT flagged for an allowlisted file", () => {
|
||||
const perFile = [
|
||||
{
|
||||
file: "legit.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 3,
|
||||
baseTaut: 0,
|
||||
headTaut: 0,
|
||||
baseSkips: 0,
|
||||
headSkips: 0,
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
];
|
||||
const flagged = evaluateMasking(perFile);
|
||||
assert.equal(flagged.length, 1, "without allowlist the reduction is flagged");
|
||||
const allowed = evaluateMasking(perFile, new Set(["legit.test.ts"]));
|
||||
assert.deepEqual(allowed, [], "with allowlist the reduction is exempt");
|
||||
});
|
||||
|
||||
test("evaluateMasking: allowlist exempts ONLY reduction — tautology/skip still flagged", () => {
|
||||
const r = evaluateMasking(
|
||||
[
|
||||
{
|
||||
file: "legit.test.ts",
|
||||
baseAsserts: 5,
|
||||
headAsserts: 3, // reduction (exempt)
|
||||
baseTaut: 0,
|
||||
headTaut: 1, // new assert.ok(true) (NOT exempt)
|
||||
baseSkips: 0,
|
||||
headSkips: 1, // new .skip (NOT exempt)
|
||||
baseExtTaut: 0,
|
||||
headExtTaut: 0,
|
||||
},
|
||||
],
|
||||
new Set(["legit.test.ts"])
|
||||
);
|
||||
assert.equal(r.length, 2, "tautology + skip still flagged despite allowlist");
|
||||
assert.ok(r.some((f) => /tautolog/i.test(f)));
|
||||
assert.ok(r.some((f) => /skip/i.test(f)));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user