ci: guard against silently merge-corrupted lockfiles

PR #233's CI failed with "npm ci can only install with an existing
package-lock.json" even though the file existed: a "Merge branch 'main'"
commit had auto-merged backend/package.json and package-lock.json into
invalid JSON with no conflict raised, and npm reports an unparseable
lockfile as if it were missing.

Two guards: .gitattributes marks package-lock.json/bun.lock merge=binary so
concurrent lockfile changes surface as explicit conflicts (resolve by
regenerating, never hand-merging), and CI parse-checks package.json and the
lockfile before npm ci so any corruption that still lands fails with the
real reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
QA Runner 2026-07-22 10:23:15 -07:00
parent 2ec89a7c52
commit e25c2637ec
2 changed files with 20 additions and 0 deletions

8
.gitattributes vendored Normal file
View file

@ -0,0 +1,8 @@
# Lockfiles are generated files. Git's line-level text merge can combine
# both sides' insertions into syntactically invalid JSON *without raising a
# conflict* (this silently broke backend/package.json + package-lock.json in
# PR #233). Merge them as binary so any concurrent change surfaces as an
# explicit conflict; resolve by taking main's copy and regenerating:
# git checkout origin/main -- package-lock.json && npm install
package-lock.json merge=binary
bun.lock merge=binary

View file

@ -32,6 +32,14 @@ jobs:
cache: npm
cache-dependency-path: backend/package-lock.json
# Git's line-level merge can splice both sides of package(-lock).json
# into invalid JSON without a conflict (bit PR #233). npm's own error
# for an unparseable lockfile is the misleading "npm ci can only
# install with an existing package-lock.json" — fail fast with the real
# reason instead.
- name: Validate package.json and lockfile parse
run: node -e "for (const f of ['package.json','package-lock.json']) JSON.parse(require('fs').readFileSync(f, 'utf8'))"
- run: npm ci
# No-ops on a tree without a "test" script (e.g. before the vitest
@ -55,6 +63,10 @@ jobs:
cache: npm
cache-dependency-path: frontend/package-lock.json
# Same silent-merge-corruption guard as the backend job.
- name: Validate package.json and lockfile parse
run: node -e "for (const f of ['package.json','package-lock.json']) JSON.parse(require('fs').readFileSync(f, 'utf8'))"
- run: npm ci
# No-ops on a tree without a "test" script (e.g. before the vitest