Validating lockfile consistency before push Jump to heading
A pull request bumps a version in package.json but not in package-lock.json. Locally it works, because node_modules already has the new version installed. CI runs npm ci, which refuses because the two files disagree, and the build fails ten minutes in — or, worse, CI uses npm install, quietly resolves a different tree, and tests pass against dependencies nobody reviewed. The same class of problem exists in every ecosystem: pyproject.toml and poetry.lock, go.mod and go.sum, Cargo.toml and Cargo.lock. Package managers can verify consistency in seconds without installing anything. Running that verification before push turns a slow, confusing CI failure into an instant, specific local message. This page builds the check for common ecosystems, within pre-push validation rules.
When to use this approach Jump to heading
- CI regularly fails at the install step with lockfile mismatch errors.
- Pull requests sometimes change a manifest without its lockfile, or the reverse.
- Someone occasionally commits a lockfile from the wrong package manager —
yarn.lockin an npm project. - Lockfiles are a frequent source of conflicts too; that side is covered in reducing lockfile churn in a busy repository.
Step 1 — Know the verification command for each ecosystem Jump to heading
Each package manager has a mode that checks the lockfile against the manifest without installing or modifying anything.
npm ci --dry-run --ignore-scripts >/dev/null # npm
pnpm install --frozen-lockfile --lockfile-only # pnpm
poetry check --lock # Poetry
uv lock --check # uv
go mod tidy -diff # Go 1.23+
cargo metadata --locked --format-version 1 >/dev/null # Cargo Step 2 — Run only when manifests or lockfiles are in the push Jump to heading
The check is quick, but there is no reason to run it on pushes that touch neither file. Look at the files changed by the commits being pushed, using the helper from finding the new commits in a pre-push hook.
#!/bin/sh
# scripts/hooks/check-lockfiles.sh — reads pushed commit IDs on stdin
set -eu
changed=$(while read -r c; do git diff-tree --no-commit-id --name-only -r "$c"; done | sort -u)
need() { printf '%s\n' "$changed" | grep -Eq "$1"; }
fail=0
if need '(^|/)package(-lock)?\.json$'; then
npm ci --dry-run --ignore-scripts >/dev/null 2>&1 || { echo "✗ package.json and package-lock.json disagree — run npm install"; fail=1; }
fi
if need '(^|/)(pyproject\.toml|poetry\.lock)$'; then
poetry check --lock >/dev/null 2>&1 || { echo "✗ poetry.lock is out of date — run poetry lock"; fail=1; }
fi
if need '(^|/)go\.(mod|sum)$'; then
go mod tidy -diff >/dev/null 2>&1 || { echo "✗ go.mod/go.sum not tidy — run go mod tidy"; fail=1; }
fi
exit $fail Step 3 — Reject lockfiles from the wrong package manager Jump to heading
A stray yarn.lock in an npm project, or package-lock.json in a pnpm project, means someone ran the wrong tool. Builds may pick the wrong file, and the stray file goes stale. Refuse them explicitly.
pm=$(sed -n 's/.*"packageManager": *"\([a-z]*\)@.*/\1/p' package.json)
case "$pm" in
pnpm) bad='(^|/)(package-lock\.json|yarn\.lock)$' ;;
yarn) bad='(^|/)(package-lock\.json|pnpm-lock\.yaml)$' ;;
*) bad='(^|/)(yarn\.lock|pnpm-lock\.yaml)$' ;;
esac
git ls-files | grep -E "$bad" && { echo "✗ lockfile from another package manager is tracked"; exit 1; } || true Step 4 — Check monorepo workspaces as a whole Jump to heading
In workspaces, a single root lockfile covers every package. A version change in packages/api/package.json requires the root lockfile to change. Run the check from the root, and treat any workspace manifest as triggering it.
need '(^|/)package\.json$' && ( cd "$(git rev-parse --show-toplevel)" && pnpm install --frozen-lockfile --lockfile-only >/dev/null ) \
|| { echo "✗ workspace lockfile out of date — run pnpm install at the root"; fail=1; } Step 5 — Enforce the same check in CI Jump to heading
The pre-push check is fast feedback; CI is the enforcement. Run the identical script in CI so a push with --no-verify still fails before merge, and use the frozen install modes there as well.
jobs:
lockfiles:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: git rev-list "origin/${{ github.base_ref }}..HEAD" | sh scripts/hooks/check-lockfiles.sh
- run: npm ci # fails on mismatch rather than silently resolving Never use npm install in CI: it updates the lockfile to match the manifest, hiding exactly the mismatch you want to catch.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does npm ci --dry-run contact the registry? Jump to heading
It may fetch metadata to resolve the tree, but it does not install packages or run scripts. On a machine with a warm cache it is quick; offline, it can fail for reasons unrelated to consistency, so keep the CI check as the authority.
Why does the lockfile change when I run install, even though I changed nothing? Jump to heading
Usually a different package manager version writes the file differently. Pin the version with packageManager and Corepack, or the equivalent for your ecosystem, so everyone produces identical lockfiles.
Should dependency bots be exempt? Jump to heading
No. Bot pull requests should keep lockfiles consistent too, and the CI check catches a misconfigured bot quickly. See keeping lockfiles conflict-free during bulk updates.
Related Jump to heading
- Pre-Push Validation Rules — the parent topic.
- A Custom Merge Driver for Lockfile Conflicts — resolving lockfile conflicts by regeneration.
- Caching Dependencies Keyed on the Lockfile — why a consistent lockfile also makes CI faster.
- Using Husky with pnpm and Yarn — package-manager pinning for hooks.