Add scripts/check-changeset-format.mjs: - Validates structured changeset schema across .changeset/*.md - Legacy changesets warn (exit 0) in transition mode - Structured changesets with missing/invalid fields error (exit 1) - --strict flag fails on legacy changesets Wire into package.json (check:changesets, pretest, test:gate) and pr-checks.yml lint job. Fix parser to not default category on structured changesets missing the field. 10 linter tests + 18 schema tests all passing.
187 lines
4.8 KiB
JavaScript
187 lines
4.8 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { scanChangesets } from "../check-changeset-format.mjs";
|
|
import {
|
|
mkdirSync,
|
|
writeFileSync,
|
|
rmSync,
|
|
existsSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
|
|
function createTempChangesetDir(changesets) {
|
|
const dir = mkdtempSync(join(tmpdir(), "changeset-lint-test-"));
|
|
for (const [name, content] of Object.entries(changesets)) {
|
|
writeFileSync(join(dir, name), content);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
const validStructured = `---
|
|
"@runfusion/fusion": minor
|
|
---
|
|
|
|
summary: Add a new dashboard widget.
|
|
category: feature
|
|
dev: Uses the widget framework.
|
|
`;
|
|
|
|
const validMinimal = `---
|
|
"@runfusion/fusion": patch
|
|
---
|
|
|
|
summary: Fix a typo.
|
|
category: fix
|
|
`;
|
|
|
|
const legacyFreeform = `---
|
|
"@runfusion/fusion": patch
|
|
---
|
|
|
|
Fix ntfy test notifications to honor unsaved Settings form config so users can test before saving.
|
|
`;
|
|
|
|
const missingCategory = `---
|
|
"@runfusion/fusion": minor
|
|
---
|
|
|
|
summary: Add something.
|
|
`;
|
|
|
|
const invalidCategory = `---
|
|
"@runfusion/fusion": minor
|
|
---
|
|
|
|
summary: Add something.
|
|
category: enhancement
|
|
`;
|
|
|
|
const overLengthSummary = `---
|
|
"@runfusion/fusion": minor
|
|
---
|
|
|
|
summary: ${"a".repeat(121)}
|
|
category: feature
|
|
`;
|
|
|
|
const emptyBody = `---
|
|
"@runfusion/fusion": minor
|
|
---
|
|
|
|
`;
|
|
|
|
test("valid structured changeset passes with no errors", () => {
|
|
const dir = createTempChangesetDir({ "valid.md": validStructured });
|
|
try {
|
|
const { errors, warnings } = scanChangesets(dir);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(warnings.length, 0);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("legacy freeform changeset passes with warning in transition mode", () => {
|
|
const dir = createTempChangesetDir({ "legacy.md": legacyFreeform });
|
|
try {
|
|
const { errors, warnings } = scanChangesets(dir);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(warnings.length, 1);
|
|
assert.ok(warnings[0].includes("legacy"));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("missing category fails with error", () => {
|
|
const dir = createTempChangesetDir({ "no-cat.md": missingCategory });
|
|
try {
|
|
const { errors } = scanChangesets(dir);
|
|
assert.equal(errors.length, 1);
|
|
assert.ok(errors[0].includes("missing required `category`"));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("invalid category value fails with error", () => {
|
|
const dir = createTempChangesetDir({ "bad-cat.md": invalidCategory });
|
|
try {
|
|
const { errors } = scanChangesets(dir);
|
|
assert.equal(errors.length, 1);
|
|
assert.ok(errors[0].includes("invalid"));
|
|
assert.ok(errors[0].includes("enhancement"));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("over-length summary fails with error", () => {
|
|
const dir = createTempChangesetDir({ "long.md": overLengthSummary });
|
|
try {
|
|
const { errors } = scanChangesets(dir);
|
|
assert.equal(errors.length, 1);
|
|
assert.ok(errors[0].includes("exceeds max length"));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("empty body fails with error", () => {
|
|
const dir = createTempChangesetDir({ "empty.md": emptyBody });
|
|
try {
|
|
const { errors } = scanChangesets(dir);
|
|
assert.equal(errors.length, 1);
|
|
assert.ok(errors[0].includes("empty or unparseable"));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("empty directory passes with no errors or warnings", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "changeset-lint-test-"));
|
|
try {
|
|
const { errors, warnings } = scanChangesets(dir);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(warnings.length, 0);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("nonexistent directory passes with no errors or warnings", () => {
|
|
const { errors, warnings } = scanChangesets("/nonexistent/path");
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(warnings.length, 0);
|
|
});
|
|
|
|
test("mixed valid and invalid changesets report all errors", () => {
|
|
const dir = createTempChangesetDir({
|
|
"valid.md": validStructured,
|
|
"no-cat.md": missingCategory,
|
|
"bad-cat.md": invalidCategory,
|
|
"legacy.md": legacyFreeform,
|
|
});
|
|
try {
|
|
const { errors, warnings } = scanChangesets(dir);
|
|
assert.equal(errors.length, 2);
|
|
assert.equal(warnings.length, 1);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("valid minimal structured changeset (no dev) passes", () => {
|
|
const dir = createTempChangesetDir({ "minimal.md": validMinimal });
|
|
try {
|
|
const { errors, warnings } = scanChangesets(dir);
|
|
assert.equal(errors.length, 0);
|
|
assert.equal(warnings.length, 0);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|