Adds repo-root CHANGELOG.md in Keep-a-Changelog format with a single [v0.0.0-beta] section that catalogues every shipped feature across Fases 0–16 (58 bullets, [new]/[fix]/[tweak] prefixes, grouped by area). The companion linter at scripts/check-changelog.mjs enforces (a) the Keep-a-Changelog header, (b) the `## [vX.Y.Z] — YYYY-MM-DD` heading shape on every release, and (c) the three-prefix rule on every bullet. Wired into `just check` so a malformed CHANGELOG fails the check gate without blocking commit hooks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
110 lines
3.2 KiB
JavaScript
Executable File
110 lines
3.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Fase 17.4.3 — CHANGELOG.md linter.
|
|
*
|
|
* Validates three things and exits non-zero on the first failure:
|
|
*
|
|
* 1. Keep-a-Changelog header line is present.
|
|
* 2. Every release heading matches `## [vX.Y.Z] — YYYY-MM-DD` or the
|
|
* placeholder `## [Unreleased]`. Pre-release / build-metadata
|
|
* suffixes are allowed on the version (e.g. `v0.0.0-beta`,
|
|
* `v1.0.0-rc.0`).
|
|
* 3. Every bullet inside a release section starts with one of the
|
|
* three prefixes `[new] / [fix] / [tweak]`. Bullets that introduce
|
|
* a sub-grouping header (lines ending in `:`) are exempted so the
|
|
* area-by-area structure of the beta backfill can use plain bullets
|
|
* as section titles.
|
|
*
|
|
* Wired into `just check` and runnable standalone:
|
|
* node scripts/check-changelog.mjs [path]
|
|
*
|
|
* Exit codes:
|
|
* 0 — file passes all three checks
|
|
* 1 — usage error (missing file)
|
|
* 2 — validation failure (one or more issues printed to stderr)
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const repoRoot = resolve(__filename, '..', '..');
|
|
|
|
const target = resolve(process.argv[2] ?? resolve(repoRoot, 'CHANGELOG.md'));
|
|
|
|
let raw;
|
|
try {
|
|
raw = readFileSync(target, 'utf8');
|
|
} catch (err) {
|
|
console.error(`check-changelog: cannot read ${target}: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const lines = raw.split('\n');
|
|
const errors = [];
|
|
|
|
// Check 1 — Keep-a-Changelog header.
|
|
if (!raw.includes('Keep a Changelog')) {
|
|
errors.push('Missing Keep-a-Changelog reference in the header.');
|
|
}
|
|
|
|
// Walk through lines. Track when we are inside a release section.
|
|
const RELEASE_HEADING = /^## \[(.+?)\](?:\s+—\s+(.+))?$/;
|
|
const VERSION_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
|
const BULLET = /^[-*]\s+(.*)$/;
|
|
const ALLOWED_PREFIXES = ['[new]', '[fix]', '[tweak]'];
|
|
|
|
let insideRelease = false;
|
|
let currentRelease = null;
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const m = line.match(RELEASE_HEADING);
|
|
if (m) {
|
|
const [, label, rest] = m;
|
|
currentRelease = label;
|
|
insideRelease = true;
|
|
if (label === 'Unreleased') {
|
|
// no further checks
|
|
continue;
|
|
}
|
|
if (!VERSION_RE.test(label)) {
|
|
errors.push(
|
|
`Line ${i + 1}: release label "${label}" does not match vX.Y.Z (semver-with-optional-prerelease).`
|
|
);
|
|
}
|
|
if (!rest || !DATE_RE.test(rest)) {
|
|
errors.push(
|
|
`Line ${i + 1}: release heading "${label}" is missing the "— YYYY-MM-DD" date segment.`
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!insideRelease) continue;
|
|
|
|
const bm = line.match(BULLET);
|
|
if (!bm) continue;
|
|
|
|
const body = bm[1].trim();
|
|
if (!body) continue;
|
|
|
|
// Sub-grouping bullets end with ":" and carry no prefix (e.g. "- Auth + identity:").
|
|
if (body.endsWith(':')) continue;
|
|
|
|
const ok = ALLOWED_PREFIXES.some((p) => body.toLowerCase().startsWith(p));
|
|
if (!ok) {
|
|
errors.push(
|
|
`Line ${i + 1} (in "${currentRelease}"): bullet missing [new]/[fix]/[tweak] prefix — "${body.slice(0, 80)}".`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
for (const e of errors) console.error(`check-changelog: ${e}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
console.log(`check-changelog: ${target} OK`);
|