Keeping lockfiles conflict-free during bulk updates Jump to heading
A lockfile conflict has no correct manual resolution. The file is a serialised resolution of a dependency graph; picking lines from two versions of it produces a graph that neither branch tested and that may not even be internally consistent. Yet the default merge machinery cheerfully offers exactly that, and hours disappear into conflict markers in a file nobody wrote. The fix is to stop treating it as source, which is a three-part change to automated dependency updates.
When to use this approach Jump to heading
- Several dependency branches are open at once and conflict with each other.
- Someone has resolved a lockfile by hand and the build broke afterwards.
- Reviews are dominated by thousands of generated lines nobody reads.
- You maintain a monorepo where one lockfile serves many manifests.
- If you have a single update branch at a time, a plain rebase is enough and the tooling below is overhead.
Step 1 β Declare the file generated Jump to heading
Two attributes change how Git and the review interface treat it.
# .gitattributes
cat >> .gitattributes <<'ATTR'
package-lock.json -diff linguist-generated=true
pnpm-lock.yaml -diff linguist-generated=true
poetry.lock -diff linguist-generated=true
Cargo.lock -diff linguist-generated=true
go.sum -diff linguist-generated=true
ATTR # Verification: Git now reports it as binary in diffs, which is the point
git check-attr -a package-lock.json
git diff --stat HEAD~1 -- package-lock.json What changed: the file no longer produces a line-by-line diff, so nobody is invited to resolve it by hand, and reviews collapse it automatically. The same reasoning applies to other build artefacts β see keeping generated files out of review diffs.
Step 2 β Resolve by regenerating, never by merging Jump to heading
The resolution procedure is the same for every ecosystem: take one side wholesale, then rebuild from the manifests.
# During a rebase or merge conflict on the lockfile
git checkout --theirs package-lock.json # or --ours; the content is discarded anyway
npm install --package-lock-only # rebuild from package.json
git add package-lock.json
git rebase --continue # or git merge --continue # The equivalents, for reference
pnpm install --lockfile-only
poetry lock --no-update
cargo generate-lockfile
go mod tidy # Verification: an install from the rebuilt lockfile is clean and reproducible
npm ci --dry-run >/dev/null && echo "lockfile agrees with the manifest" Step 3 β Automate the resolution with a merge driver Jump to heading
Doing the above by hand every time is reliable and tedious. A custom merge driver does it automatically, and Git will call it whenever the file conflicts.
# Define the driver: take one side, then regenerate
git config merge.lockfile.name "regenerate the lockfile from the manifest"
git config merge.lockfile.driver \
'cp %B %A && npm install --package-lock-only --silent && true'
# Point the file at it
echo 'package-lock.json merge=lockfile' >> .gitattributes # Verification: force a conflict in a scratch branch and confirm it resolves itself
git switch -c /tmp-lock-test && npm install --no-save --package-lock-only [email protected]
git commit -am "test: bump lodash" && git switch - && git merge /tmp-lock-test
git branch -D /tmp-lock-test Because the driver is defined in local configuration rather than in the repository, every clone needs it β ship it through the mechanism in shipping a team gitconfig with includeIf. The general pattern is covered in a custom merge driver for lockfile conflicts.
Step 4 β Verify in CI that the lockfile matches the manifest Jump to heading
A regenerated lockfile can still be wrong if someone edits the manifest without rebuilding. One job catches it.
# .github/workflows/lockfile.yml
name: lockfile
on: [pull_request]
jobs:
consistent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm install --package-lock-only --ignore-scripts
- name: The lockfile must already match the manifest
run: git diff --exit-code package-lock.json # Verification locally, before pushing
npm install --package-lock-only && git diff --exit-code package-lock.json \
&& echo "in sync" || echo "commit the regenerated lockfile" SAFETY WARNING β regeneration resolves the version graph from your manifests and the registry as it is right now, which can pull in a newer transitive version than either branch tested. That is usually what you want, but it means the merged result is not identical to either side. Always let the pipeline run on the merged tree; never regenerate and merge without a test run.
Step 5 β Schedule a full refresh so transitive versions do not rot Jump to heading
Direct bumps only touch the packages named in them. Everything else in the graph stays at whatever version it resolved to years ago.
{
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 5am on the first day of the month"],
"commitMessageAction": "refresh",
"automerge": false
}
} # Verification: how old is the oldest thing in the tree?
npm ls --all --json 2>/dev/null | jq -r '.. | .version? // empty' | sort -u | head Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Is merge=ours on a lockfile safe? Jump to heading
Only as one half of a procedure whose other half is regeneration. On its own it silently discards the incoming branchβs resolution while keeping its manifest changes, so the lockfile and the manifest disagree and the next clean install resolves something untested. Pair it with a driver or a pipeline step that rebuilds.
Why not just commit no lockfile at all? Jump to heading
Because then every install resolves differently and nothing is reproducible β the exact problem lockfiles exist to solve. The answer is not to remove the file but to stop treating it as something humans edit.
What about ecosystems where the lockfile contains hashes? Jump to heading
Regeneration recomputes them, which is the point: hand-merged hashes are how you end up with an integrity failure at install time in a branch that looked fine in review. Let the tool write them.
Related Jump to heading
- Automated Dependency Updates β the parent topic and why bulk updates collide.
- A Custom Merge Driver for Lockfile Conflicts β the merge-driver mechanism in full.
- Configuring Renovate for a Monorepo β where one lockfile serves a dozen manifests.