diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26a5da1..7008c6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: Check schema run: pnpm check:schema + - name: Check changesets + run: pnpm check:changesets + - name: Unit tests run: pnpm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98aa551..5986173 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,6 +45,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check changesets + run: pnpm check:changesets + - name: Get release info id: release-info run: | diff --git a/.husky/pre-commit b/.husky/pre-commit index d7b967c..c1838ab 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,4 @@ pnpm install --frozen-lockfile --silent pnpm typecheck pnpm lint +pnpm check:changesets diff --git a/AGENTS.md b/AGENTS.md index 65cadd2..dad152f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ During UI tests that require processes to be running, either give the user a pro - `pnpm format` — Biome check with write - `pnpm gen:schema` — regenerate `schema.json` from `extensions/processes/config/types.ts` - `pnpm check:schema` — verify `schema.json` is up to date +- `pnpm check:changesets` — validate `.changeset/*.md` frontmatter, package names, and bump types - `pnpm test` — Unit tests - `pnpm test:e2e` — End-to-end tests - `pnpm changeset` — Add a changeset diff --git a/package.json b/package.json index 727b326..98bc868 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "format": "biome check --write", "gen:schema": "ts-json-schema-generator --path extensions/processes/config/types.ts --type ProcessConfig --no-type-check -o schema.json", "check:schema": "ts-json-schema-generator --path extensions/processes/config/types.ts --type ProcessConfig --no-type-check -o /tmp/pi-processes-schema-check.json && diff -q schema.json /tmp/pi-processes-schema-check.json", + "check:changesets": "node scripts/check-changesets.ts", "check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts", "prepare": "[ -d .git ] && husky || true", "changeset": "changeset", diff --git a/scripts/check-changesets.ts b/scripts/check-changesets.ts new file mode 100644 index 0000000..a065c25 --- /dev/null +++ b/scripts/check-changesets.ts @@ -0,0 +1,80 @@ +#!/usr/bin/env node +// Validates .changeset/*.md files: frontmatter shape, package names, bump types. +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const changesetDir = join(root, ".changeset"); +const validBumps = new Set(["major", "minor", "patch"]); + +const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { + name: string; +}; +const pkgName = pkg.name; +const knownPackages = new Set([pkgName]); + +const files = readdirSync(changesetDir) + .filter((f) => f.endsWith(".md") && f !== "README.md") + .sort(); + +const errors: string[] = []; + +for (const file of files) { + const path = join(".changeset", file); + const content = readFileSync(join(changesetDir, file), "utf8"); + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); + + if (!match) { + errors.push(`${path}: missing or malformed frontmatter block`); + continue; + } + + const [, frontmatter, body] = match; + const lines = frontmatter.split(/\r?\n/).filter((line) => line.trim() !== ""); + + if (lines.length === 0) { + errors.push(`${path}: frontmatter lists no packages`); + } + + for (const line of lines) { + const entry = line.match( + /^\s*(?:"([^"]+)"|'([^']+)'|([^:]+?))\s*:\s*(.+?)\s*$/, + ); + if (!entry) { + errors.push(`${path}: cannot parse frontmatter line: ${line.trim()}`); + continue; + } + + const name = entry[1] ?? entry[2] ?? entry[3]; + const bump = entry[4].replace(/^["']|["']$/g, ""); + + if (!knownPackages.has(name)) { + errors.push( + `${path}: unknown package "${name}" (expected one of: ${[...knownPackages].join(", ")})`, + ); + } + if (!validBumps.has(bump)) { + errors.push( + `${path}: invalid bump "${bump}" for "${name}" (expected major, minor or patch)`, + ); + } + } + + if (body.trim() === "") { + errors.push(`${path}: empty changeset description`); + } +} + +if (errors.length > 0) { + console.error("Invalid changesets:\n"); + for (const error of errors) { + console.error(` ${error}`); + } + console.error(""); + process.exit(1); +} + +console.log( + `Checked ${files.length} changeset${files.length === 1 ? "" : "s"}.`, +); diff --git a/tsconfig.json b/tsconfig.json index fa13cbc..03befb5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "resolveJsonModule": true, "noEmit": true }, - "include": ["src/**/*", "extensions/**/*", "tests/**/*"], + "include": ["src/**/*", "extensions/**/*", "scripts/**/*", "tests/**/*"], // tests/evals has its own tsconfig: it depends on the gitignored vendored // pi-evals harness, so `pnpm typecheck` must stay green without a sync. "exclude": ["node_modules", "tests/evals"]