diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 154978aa..1ff65968 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Check package-lock.json version sync + run: npm run check:lockfile + - name: Type check run: npm run typecheck diff --git a/CLAUDE.md b/CLAUDE.md index be25cd14..78fe54fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,10 +49,11 @@ When user says "COM": CHANGESET ``` Replace `patch` with `minor` or `major` as needed. Include `"xterm-zerolag-input": patch` on a separate line if that package changed too. -3. **Consume the changeset**: `npm run version-packages` (auto-bumps versions in `package.json` files and auto-updates `CHANGELOG.md` — never hand-edit `CHANGELOG.md`) -4. **Sync `package-lock.json`**: `npm install --package-lock-only` (changesets does NOT touch the lockfile; skipping this leaves `package-lock.json` stuck on an old version and breaks `npm ci`) -5. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json` -6. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web` +3. **Consume the changeset**: `npm run version-packages` (auto-bumps `package.json` files, updates `CHANGELOG.md`, runs `npm install --package-lock-only`, and verifies lockfile sync via `scripts/check-lockfile-sync.mjs` — all in one command; never hand-edit `CHANGELOG.md` or `package-lock.json` versions) +4. **Sync CLAUDE.md version**: Update the `**Version**` line below to match the new version from `package.json` +5. **Commit and deploy**: `git add -A && git commit -m "chore: version packages" && git push && npm run build && systemctl --user restart codeman-web` + +CI runs `npm run check:lockfile` on every push/PR, so lockfile drift fails the build even if the `version-packages` script is bypassed. **Version**: 0.6.0 (must match `package.json`) diff --git a/package.json b/package.json index 549c1613..1e3584dd 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "format:check": "prettier --check 'src/**/*.ts'", "capture:subagents": "node scripts/capture-subagent-screenshots.mjs", "changeset": "changeset", - "version-packages": "changeset version", + "version-packages": "changeset version && npm install --package-lock-only && node scripts/check-lockfile-sync.mjs", + "check:lockfile": "node scripts/check-lockfile-sync.mjs", "release": "changeset publish" }, "workspaces": [ diff --git a/scripts/check-lockfile-sync.mjs b/scripts/check-lockfile-sync.mjs new file mode 100755 index 00000000..20b5d2e4 --- /dev/null +++ b/scripts/check-lockfile-sync.mjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node +// Fails if package-lock.json's version fields don't match package.json. +// Changesets bumps package.json but NOT the lockfile — this catches that drift +// (the top-level `version` in lockfiles is metadata, so `npm ci` won't flag it). + +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const pkg = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8')); +const lock = JSON.parse(readFileSync(resolve(repoRoot, 'package-lock.json'), 'utf8')); + +const expected = pkg.version; +const rootVersion = lock.version; +const selfVersion = lock.packages?.['']?.version; + +const mismatches = []; +if (rootVersion !== expected) mismatches.push(` package-lock.json#.version = ${rootVersion} (expected ${expected})`); +if (selfVersion !== expected) mismatches.push(` package-lock.json#.packages[""].version = ${selfVersion} (expected ${expected})`); + +if (mismatches.length > 0) { + console.error(`\nLockfile version drift detected (package.json is ${expected}):`); + console.error(mismatches.join('\n')); + console.error('\nFix: run `npm install --package-lock-only` and commit the updated package-lock.json.\n'); + process.exit(1); +} + +console.log(`Lockfile in sync with package.json (${expected}).`);