From e25c2637ecec700a2da271b1735673dd50f573a7 Mon Sep 17 00:00:00 2001 From: QA Runner Date: Wed, 22 Jul 2026 10:23:15 -0700 Subject: [PATCH] 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 --- .gitattributes | 8 ++++++++ .github/workflows/ci.yml | 12 ++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d2edaa9 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b14ec8a..c41f910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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