Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
Expand All @@ -26,7 +28,16 @@ jobs:
run: npm run format:check
- name: Install test-app dependencies
run: npm ci --prefix test-app
- name: Guard against a raised ESLint warning baseline
if: github.event_name == 'pull_request'
run: |
git show "${{ github.event.pull_request.base.sha }}:eslint-baseline.json" > /tmp/base-eslint-baseline.json 2>/dev/null || echo '{}' > /tmp/base-eslint-baseline.json
node scripts/check-baseline-not-increased.mjs /tmp/base-eslint-baseline.json eslint-baseline.json
- name: Lint library and documentation app
run: npm run lint
- name: Guard root ESLint warning baseline
run: npm run lint:baseline
Comment thread
fpigeonjr marked this conversation as resolved.
- name: Lint test app
run: npm --prefix test-app run lint
- name: Guard test-app ESLint warning baseline
run: npm --prefix test-app run lint:baseline
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ npm-debug.log
# Coverage #
/coverage/

# ESLint report artifacts generated by lint:report / lint:baseline
eslint-report.json
test-app/eslint-report.json
.eslintcache
test-app/.eslintcache

# Typing #
/src/typings/tsd/
/typings/
Expand Down
4 changes: 4 additions & 0 deletions eslint-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"root": 1631,
"test-app": 4
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "ng lint",
"lint:report": "ng lint --format json --output-file eslint-report.json",
"lint:baseline": "npm run lint:report && node scripts/check-lint-baseline.mjs root eslint-report.json",
"lint:baseline:bump": "npm run lint:report && node scripts/check-lint-baseline.mjs --bump root eslint-report.json",
"validate:publish": "node scripts/validate-publish-package.mjs"
},
"peerDependencies": {
Expand Down
83 changes: 83 additions & 0 deletions scripts/check-baseline-not-increased.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
/**
* Guards against the ESLint warning baseline being raised directly in
* eslint-baseline.json rather than earned via --bump.
*
* The per-workspace gate (check-lint-baseline.mjs) only evaluates the
* baseline committed in the branch being checked, so a contributor could
* raise a ceiling (e.g. bumping "root" from 1631 to 5000) in the same PR
* that introduces new warnings, and both guard steps would pass. This script
* closes that hole by comparing the baseline on the PR branch against the
* baseline on the trusted base branch and failing if any shared workspace's
* ceiling increased.
*
* New workspace keys that don't exist on the base branch are allowed (they
* can't be "increased" if there's nothing to compare against). Any decrease
* or unchanged value is allowed, matching the ratchet-only semantics of
* --bump.
*
* Usage:
* node scripts/check-baseline-not-increased.mjs <base-baseline.json> <head-baseline.json>
*/
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

const [baseArg, headArg] = process.argv.slice(2);

if (!baseArg || !headArg) {
console.error(
"Usage: node scripts/check-baseline-not-increased.mjs <base-baseline.json> <head-baseline.json>"
);
process.exit(1);
}

function loadBaselines(label, path) {
try {
return JSON.parse(readFileSync(resolve(path), "utf8"));
} catch (error) {
console.error(`✖ Could not read ${label} baselines at ${path}`);
console.error(` ${error.message}`);
process.exit(1);
}
}

const base = loadBaselines("base-branch", baseArg);
const head = loadBaselines("pull-request", headArg);

let hasIncrease = false;

for (const workspace of Object.keys(head)) {
if (!(workspace in base)) {
// New workspace entry — nothing to compare against, so it can't be an
// increase over a previously-trusted value.
continue;
}

const baseValue = base[workspace];
const headValue = head[workspace];

if (!Number.isFinite(baseValue) || !Number.isFinite(headValue)) {
console.error(
`✖ ${workspace}: invalid baseline value (base: ${baseValue}, head: ${headValue})`
);
hasIncrease = true;
continue;
}

if (headValue > baseValue) {
console.error(
`✖ ${workspace}: baseline increased ${baseValue} → ${headValue}. ` +
"The accepted warning ceiling can only go down (via --bump), never up. " +
"Revert this change to eslint-baseline.json."
);
hasIncrease = true;
}
}

if (hasIncrease) {
process.exit(1);
}

console.log(
"✓ eslint-baseline.json: no workspace's warning ceiling increased vs. the base branch."
);
123 changes: 123 additions & 0 deletions scripts/check-baseline-not-increased.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env node
/**
* Tests for scripts/check-baseline-not-increased.mjs — the CI-only guard
* that compares eslint-baseline.json on a PR branch against the base
* branch's version and fails if any shared workspace's ceiling increased.
*
* Run: node --test scripts/check-baseline-not-increased.test.mjs
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const scriptDir = dirname(fileURLToPath(import.meta.url));
const script = join(scriptDir, "check-baseline-not-increased.mjs");

function run(args) {
try {
const stdout = execFileSync("node", [script, ...args], {
encoding: "utf8",
});
return { status: 0, stdout, stderr: "" };
} catch (error) {
return {
status: error.status ?? 1,
stdout: error.stdout?.toString() ?? "",
stderr: error.stderr?.toString() ?? "",
};
}
}

function withTempDir(fn) {
const dir = mkdtempSync(join(tmpdir(), "baseline-guard-"));
try {
return fn(dir);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

function writeBaseline(dir, name, data) {
const path = join(dir, name);
writeFileSync(path, JSON.stringify(data));
return path;
}

test("passes when the baseline is unchanged", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", { root: 10, "test-app": 4 });
const head = writeBaseline(dir, "head.json", { root: 10, "test-app": 4 });
const { status } = run([base, head]);
assert.equal(status, 0);
});
});

test("passes when a baseline is lowered", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", { root: 10, "test-app": 4 });
const head = writeBaseline(dir, "head.json", { root: 6, "test-app": 4 });
const { status } = run([base, head]);
assert.equal(status, 0);
});
});

test("fails when a baseline is raised", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", { root: 10, "test-app": 4 });
const head = writeBaseline(dir, "head.json", { root: 20, "test-app": 4 });
const { status, stderr } = run([base, head]);
assert.equal(status, 1);
assert.match(stderr, /baseline increased 10 → 20/);
});
});

test("fails when any of multiple workspaces is raised", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", { root: 10, "test-app": 4 });
const head = writeBaseline(dir, "head.json", {
root: 10,
"test-app": 9,
});
const { status, stderr } = run([base, head]);
assert.equal(status, 1);
assert.match(stderr, /test-app: baseline increased 4 → 9/);
});
});

test("allows a brand-new workspace entry not present on the base branch", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", { root: 10 });
const head = writeBaseline(dir, "head.json", { root: 10, "new-pkg": 50 });
const { status } = run([base, head]);
assert.equal(status, 0);
});
});

test("treats a missing base-branch baseline file as an empty baseline", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", {});
const head = writeBaseline(dir, "head.json", { root: 10 });
const { status } = run([base, head]);
assert.equal(status, 0);
});
});

test("fails on a non-numeric baseline value", () => {
withTempDir((dir) => {
const base = writeBaseline(dir, "base.json", { root: 10 });
const head = writeBaseline(dir, "head.json", { root: "ten" });
const { status, stderr } = run([base, head]);
assert.equal(status, 1);
assert.match(stderr, /invalid baseline value/);
});
});

test("exits non-zero with usage when arguments are missing", () => {
const { status, stderr } = run([]);
assert.equal(status, 1);
assert.match(stderr, /Usage:/);
});
Loading
Loading