chore(ci): teach package-age check to walk pnpm-lock.yaml (#213) #242

Merged
james merged 1 commit from 213-pnpm-lock-package-ages into main 2026-06-23 13:14:05 +00:00
Owner

Summary

After #181 restructured Carol into a pnpm workspace, the package-age soft check silently degraded to "no new packages" — the wrapper still read package-lock.json at the repo root, which no longer exists. This PR teaches it to walk pnpm-lock.yaml. Policy unchanged (mechanics-only ticket — see ADR-0022).

  • New parsePnpmLockfile(content) next to the existing npm-v3 walker. No YAML lib added: the packages: block is regular enough to read line-by-line (top-level keys at column 0, package keys at 2-space indent), which keeps the CI script dependency-free.
  • newPackages(base, current) now accepts either parsed npm-v3 objects OR raw pnpm-yaml strings and normalises both to Map<name, version>, so the wrapper stays shape-agnostic.
  • Wrapper swaps package-lock.jsonpnpm-lock.yaml for both the current-file read and the git show <baseRef>:… base read.
  • Stale parenthetical dropped from CLAUDE.md; stale // NOTE (ticket #181) block dropped from the wrapper.

Parser shape

Handles all the v9 / v6 / scoped / peer-dep variants present in our lockfile:

foo@1.2.3                              → { foo,                1.2.3 }
'@scope/pkg@1.2.3'                     → { @scope/pkg,         1.2.3 }
acorn-jsx@5.3.2(acorn@8.17.0)          → { acorn-jsx,          5.3.2 }
'@asteasolutions/zod-to-openapi@8.5.0(zod@4.4.3)'
                                       → { @asteasolutions/zod-to-openapi, 8.5.0 }
eslint-config-next@16.2.9(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
                                       → { eslint-config-next, 16.2.9 }
/foo@1.2.3                             → { foo,                1.2.3 }  (v6 fallback)

Cut at the first ( rather than balancing parens — we only need the name+version prefix.

Sample script output against the live lockfile

$ node scripts/ci/check-package-ages.mjs --base-ref origin/main
flag=false                              # no new packages vs main

$ node scripts/ci/check-package-ages.mjs --base-ref 6f626f5   # pre-#181 pnpm baseline
flag=false                              # 466 new packages seen, all >30 days old

$ # parser stats on the live pnpm-lock.yaml:
$ # 1014 unique package names extracted (scoped + plain + peer-dep'd)

Test plan

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api test — 568 passing (35 in the package-ages file: 16 new for the pnpm parser + 2 new for newPackages string-input mode)
  • pnpm -F @carol/api-client test — 16 passing
  • pnpm -F @carol/client test — 29 passing
  • Local run against the live lockfile parses 1014 unique packages with no errors
  • Local run with synthetic base/current pnpm-yaml fixtures correctly reports new packages
  • lefthook gitleaks + conventional-commits hooks green on commit
  • Closes #213
  • See ADR-0022 for the unchanged policy
  • The companion CI workflow already references pnpm-lock.yaml in its checkout comment (#181 follow-up); now the script matches

🤖 Generated with Claude Code

## Summary After #181 restructured Carol into a pnpm workspace, the package-age soft check silently degraded to "no new packages" — the wrapper still read `package-lock.json` at the repo root, which no longer exists. This PR teaches it to walk `pnpm-lock.yaml`. Policy unchanged (mechanics-only ticket — see ADR-0022). - New `parsePnpmLockfile(content)` next to the existing npm-v3 walker. No YAML lib added: the `packages:` block is regular enough to read line-by-line (top-level keys at column 0, package keys at 2-space indent), which keeps the CI script dependency-free. - `newPackages(base, current)` now accepts either parsed npm-v3 objects OR raw pnpm-yaml strings and normalises both to `Map<name, version>`, so the wrapper stays shape-agnostic. - Wrapper swaps `package-lock.json` → `pnpm-lock.yaml` for both the current-file read and the `git show <baseRef>:…` base read. - Stale parenthetical dropped from CLAUDE.md; stale `// NOTE (ticket #181)` block dropped from the wrapper. ## Parser shape Handles all the v9 / v6 / scoped / peer-dep variants present in our lockfile: ``` foo@1.2.3 → { foo, 1.2.3 } '@scope/pkg@1.2.3' → { @scope/pkg, 1.2.3 } acorn-jsx@5.3.2(acorn@8.17.0) → { acorn-jsx, 5.3.2 } '@asteasolutions/zod-to-openapi@8.5.0(zod@4.4.3)' → { @asteasolutions/zod-to-openapi, 8.5.0 } eslint-config-next@16.2.9(@typescript-eslint/parser@8.61.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) → { eslint-config-next, 16.2.9 } /foo@1.2.3 → { foo, 1.2.3 } (v6 fallback) ``` Cut at the first `(` rather than balancing parens — we only need the name+version prefix. ## Sample script output against the live lockfile ``` $ node scripts/ci/check-package-ages.mjs --base-ref origin/main flag=false # no new packages vs main $ node scripts/ci/check-package-ages.mjs --base-ref 6f626f5 # pre-#181 pnpm baseline flag=false # 466 new packages seen, all >30 days old $ # parser stats on the live pnpm-lock.yaml: $ # 1014 unique package names extracted (scoped + plain + peer-dep'd) ``` ## Test plan - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api test` — 568 passing (35 in the package-ages file: 16 new for the pnpm parser + 2 new for `newPackages` string-input mode) - [x] `pnpm -F @carol/api-client test` — 16 passing - [x] `pnpm -F @carol/client test` — 29 passing - [x] Local run against the live lockfile parses 1014 unique packages with no errors - [x] Local run with synthetic base/current pnpm-yaml fixtures correctly reports new packages - [x] lefthook gitleaks + conventional-commits hooks green on commit ## Links - Closes #213 - See [ADR-0022](docs/adr/0022-package-age-policy.md) for the unchanged policy - The companion CI workflow already references `pnpm-lock.yaml` in its checkout comment (#181 follow-up); now the script matches 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chore(ci): teach package-age check to walk pnpm-lock.yaml (#213)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 7s
PR / OSV-Scanner (pull_request) Successful in 1m35s
PR / pnpm audit (pull_request) Successful in 1m52s
PR / OpenAPI (pull_request) Successful in 2m3s
PR / Lint (pull_request) Successful in 2m18s
PR / Static analysis (pull_request) Successful in 2m18s
PR / Client (web export smoke) (pull_request) Successful in 2m28s
PR / Package age policy (soft) (pull_request) Successful in 38s
PR / Test (sqlite) (pull_request) Successful in 2m49s
PR / Typecheck (pull_request) Successful in 2m56s
PR / Build (pull_request) Successful in 3m7s
Secrets / gitleaks (pull_request) Successful in 47s
PR / Test (postgres) (pull_request) Successful in 3m6s
PR / Coverage (soft) (pull_request) Successful in 1m37s
PR / Trivy (image) (pull_request) Failing after 2m1s
27a8beaa41
After the pnpm-workspace restructure in #181 the package-age soft check
silently degraded to "no new packages" — the wrapper still read
`package-lock.json` from the repo root, which no longer exists.

Add a hand-rolled `parsePnpmLockfile` next to the existing npm-v3
walker (no YAML library — the `packages:` block is regular enough to
read line-by-line, keeps the CI script dependency-free). The parser
handles v9 plain keys, v6 leading-slash keys, scoped packages, and the
peer-dep `(…)` suffix including nested parens. `newPackages` now
accepts either a parsed npm-v3 object or a pnpm-yaml string and
normalises to the same `Map<name, version>`, so the wrapper stays
shape-agnostic. The wrapper itself swaps `package-lock.json` ↔
`pnpm-lock.yaml` for both the current-file read and the
`git show <baseRef>:…` base-ref read.

Drop the now-stale parenthetical in CLAUDE.md's package-age bullet and
the `// NOTE (ticket #181)` block in the wrapper. Policy and threshold
unchanged — ADR-0022 is mechanics-only.

Verified locally:
- vitest tests/scripts/package-ages.test.ts: 35 passing (16 new for
  the pnpm parser + a couple of newPackages string-string cases).
- Full @carol/api, @carol/api-client, @carol/client suites green.
- `node scripts/ci/check-package-ages.mjs --base-ref 6f626f5` (the
  #181 merge) finds 466 new packages on the live lockfile, flags
  zero (all >30 days old) — confirms the parser walks the real file.

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 83.0% ≥ 50%
Branches 75.9% ≥ 75%
Functions 91.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 83.0% ✅ | ≥ 50% | | Branches | 75.9% ✅ | ≥ 75% | | Functions | 91.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
james merged commit 425d9cbd7c into main 2026-06-23 13:14:05 +00:00
james deleted branch 213-pnpm-lock-package-ages 2026-06-23 13:14:06 +00:00
Sign in to join this conversation.
No description provided.