From 29ec85933db643951a6deb1d9bb4db901cba8045 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 27 Jun 2026 09:33:59 +0200 Subject: [PATCH 1/2] ci: add LIP status consistency check Adds a check that fails when a LIP's status disagrees between its file frontmatter (LIPs/LIP-N.md) and the README index table, or when a status is unknown, a row/file is orphaned, or a status value has stray whitespace. This is the gate that would have caught the half-updated status in #108. Recommended to enable as a required status check in branch protection. Refs #112. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011c4cuAi1T5yQ7WtCbMp3DW --- .github/workflows/lip-status-check.yml | 25 +++++++ scripts/check-lip-status.mjs | 98 ++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 .github/workflows/lip-status-check.yml create mode 100644 scripts/check-lip-status.mjs diff --git a/.github/workflows/lip-status-check.yml b/.github/workflows/lip-status-check.yml new file mode 100644 index 0000000..8a7456d --- /dev/null +++ b/.github/workflows/lip-status-check.yml @@ -0,0 +1,25 @@ +name: LIP status check + +# Fails when a LIP's status disagrees between its file frontmatter and the +# README index table (see scripts/check-lip-status.mjs). Add this as a required +# status check in branch protection to block half-updated status changes. + +on: + pull_request: + paths: + - "LIPs/**" + - "README.md" + - "scripts/check-lip-status.mjs" + - ".github/workflows/lip-status-check.yml" + push: + branches: [master] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: node scripts/check-lip-status.mjs diff --git a/scripts/check-lip-status.mjs b/scripts/check-lip-status.mjs new file mode 100644 index 0000000..14cc982 --- /dev/null +++ b/scripts/check-lip-status.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +// Verifies that each LIP's status is consistent between its file frontmatter +// (LIPs/LIP-N.md) and the README index table. Exits non-zero on any mismatch, +// unknown status, orphan row/file, or trailing whitespace in a status value. +// +// Run: node scripts/check-lip-status.mjs + +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const lipsDir = join(root, "LIPs"); +const readmePath = join(root, "README.md"); + +// Allowed statuses per LIP-1 ("LIP Statuses"). +const VALID = new Set([ + "Draft", + "Last Call", + "Proposed", + "Abandoned", + "Accepted", + "Rejected", + "Final", +]); + +const errors = []; + +// --- Frontmatter status, per LIP file ------------------------------------- +const fileStatus = new Map(); // lip number (string) -> { raw, value } +for (const name of readdirSync(lipsDir)) { + const m = name.match(/^LIP-(\d+)\.md$/); + if (!m) continue; + const num = m[1]; + const text = readFileSync(join(lipsDir, name), "utf8"); + const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!fm) { + errors.push(`LIP-${num}: missing YAML frontmatter block`); + continue; + } + const line = fm[1].split(/\r?\n/).find((l) => /^status:/.test(l)); + if (!line) { + errors.push(`LIP-${num}: frontmatter has no "status:" field`); + continue; + } + const raw = line.replace(/^status:/, "").replace(/\r$/, ""); + const value = raw.trim(); + if (raw !== ` ${value}`) { + errors.push(`LIP-${num}: status has stray/trailing whitespace: "status:${raw}"`); + } + if (!VALID.has(value)) { + errors.push(`LIP-${num}: unknown status "${value}" (frontmatter)`); + } + fileStatus.set(num, value); +} + +// --- README index table ---------------------------------------------------- +const readme = readFileSync(readmePath, "utf8"); +const rowRe = /^\|\s*\[(\d+)\]\(LIPs\/LIP-\d+\.md\)\s*\|[^|]*\|\s*([^|]+?)\s*\|\s*$/gm; +const readmeStatus = new Map(); +for (const m of readme.matchAll(rowRe)) { + const num = m[1]; + const value = m[2].trim(); + if (!VALID.has(value)) { + errors.push(`LIP-${num}: unknown status "${value}" (README)`); + } + readmeStatus.set(num, value); +} + +// --- Cross-check ----------------------------------------------------------- +const all = new Set([...fileStatus.keys(), ...readmeStatus.keys()]); +const rows = []; +for (const num of [...all].sort((a, b) => Number(a) - Number(b))) { + const f = fileStatus.get(num); + const r = readmeStatus.get(num); + if (f === undefined) errors.push(`LIP-${num}: in README index but no LIPs/LIP-${num}.md file`); + else if (r === undefined) errors.push(`LIP-${num}: file exists but missing from README index`); + else if (f !== r) { + errors.push(`LIP-${num}: status mismatch — frontmatter "${f}" vs README "${r}"`); + rows.push([num, f, r]); + } +} + +if (errors.length) { + console.error("LIP status check FAILED:\n"); + for (const e of errors) console.error(" - " + e); + if (rows.length) { + console.error("\nMismatched LIPs:"); + console.error(" LIP frontmatter README"); + for (const [n, f, r] of rows) { + console.error(` ${n.padEnd(5)} ${f.padEnd(18)} ${r}`); + } + } + console.error(`\n${errors.length} problem(s) found.`); + process.exit(1); +} + +console.log(`LIP status check passed: ${all.size} LIPs consistent.`); From 6ba8b5420e5c5e434c5d81af638428282bf3e83d Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Sat, 27 Jun 2026 10:41:58 +0200 Subject: [PATCH 2/2] ci: bump checkout/setup-node/node versions; clarify status set - actions/checkout v4 -> v7, actions/setup-node v4 -> v6, Node 20 -> 24 (latest stable; no breaking changes for a plain checkout on ubuntu-latest). - Document that the allowed-status set is intentionally hardcoded with LIP-1 as the source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011c4cuAi1T5yQ7WtCbMp3DW --- .github/workflows/lip-status-check.yml | 6 +++--- scripts/check-lip-status.mjs | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lip-status-check.yml b/.github/workflows/lip-status-check.yml index 8a7456d..1317555 100644 --- a/.github/workflows/lip-status-check.yml +++ b/.github/workflows/lip-status-check.yml @@ -18,8 +18,8 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" - run: node scripts/check-lip-status.mjs diff --git a/scripts/check-lip-status.mjs b/scripts/check-lip-status.mjs index 14cc982..fb1d5cf 100644 --- a/scripts/check-lip-status.mjs +++ b/scripts/check-lip-status.mjs @@ -13,7 +13,10 @@ const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const lipsDir = join(root, "LIPs"); const readmePath = join(root, "README.md"); -// Allowed statuses per LIP-1 ("LIP Statuses"). +// Allowed statuses, defined by LIP-1 ("LIP Statuses"). LIP-1 is the source of +// truth; this list is intentionally hardcoded rather than parsed from LIP-1's +// prose. The set is governance-level and changes essentially never — if LIP-1 +// ever adds/removes a status, update this set in the same PR. const VALID = new Set([ "Draft", "Last Call",