Git, CI/CD & GitOpsLXVII · Dependency PinningPackageLockfiles
Package pinning and lockfiles — npm shrinkwrap, pip-tools, go.sum, Cargo.lock
What you'll learn
- Pin npm packages with `npm shrinkwrap` or `package-lock.json` and commit the lockfile to the repository
- Compile Python requirements with `pip-tools` and commit `requirements.txt` plus the lockfile
- Pin Go modules with `go.sum` and verify checksums with `go mod verify`
- Pin Rust crates with `Cargo.lock` and verify checksums with `cargo verify`
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Application and tooling packages are pulled from registries at
build time. Range constraints — ^1.2 in npm, >=2.0 in pip,
~3.6 in Cargo — let the registry publish a new satisfying version
between two consecutive builds. Lockfiles pin every direct and
transitive dependency to a specific version and checksum; the
lockfile is committed to the repository and verified at install
time. A build without a lockfile is a build whose transitive
bytes are chosen by the registry at install time. The discipline
is the same across ecosystems: range constraints declare intent;
the lockfile freezes selection.
npm: package-lock.json
npm generates package-lock.json on every npm install. The
lockfile records the exact version and integrity hash of every
direct and transitive dependency. The lockfile is committed to
the repository; the install command resolves against the lockfile
rather than against the registry’s range constraint.
# Generate or refresh the lockfile
npm install
# Commit
git add package-lock.json
git commit -m "chore: refresh package-lock.json"
// package-lock.json (excerpt)
{
"name": "app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5T6nhjs7..."
}
}
}
The integrity field is the SHA-512 checksum of the tarball. npm
verifies the checksum at install time; a mismatch fails the
install. npm shrinkwrap produces a publish-time lockfile
(/npm-shrinkwrap.json) that overrides the consumer’s
package-lock.json; the consumer-side discipline is to commit
package-lock.json and run npm ci in CI rather than npm install.
# CI: install exactly the versions in the lockfile, fail on drift
npm ci
npm ci deletes node_modules and reinstalls from the lockfile
exactly; npm install resolves against package.json and may
update the lockfile. The CI command is npm ci; the local
development command is npm install.
flowchart LR
A["Range ^ ^1.2"] --> B["Registry resolves"]
B["Registry resolves"] -. "new minor" .-> C["Different bytes today"]
D["package-lock.json"] --> E["Exact versions + checksums"]
E["Exact versions + checksums"] --> F["Same bytes forever"]
pip and pip-tools
pip alone resolves against the index at install time and does not
generate a lockfile by default. The production discipline is to
use pip-tools to compile a requirements.txt (the direct
constraints) into a requirements.lock (the resolved versions and
checksums) and commit both.
# Compile the direct constraints into a lockfile with hashes
pip-compile --generate-hashes --output-file=requirements.lock requirements.in
# Install exactly the versions in the lockfile
pip install --require-hashes -r requirements.lock
The --generate-hashes flag adds a SHA-256 line for every package
in the lockfile; --require-hashes verifies the hashes at install
time. A pip install without hashes is a pip install that trusts
the index’s bytes without verifying them.
# requirements.lock (excerpt)
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e2653b6e74f9c5e8c2c4d2e1f0a3b5c8d9e0f1a2b3c4d5e6
urllib3==2.0.7 \
--hash=sha256:abc123...
Go: go.sum
Go modules use go.mod for the direct constraints and go.sum
for the lockfile. go.sum records the SHA-256 hash of every
module’s go.mod and content; the hashes are verified by the
go command at every build.
# Verify the integrity of every module in the build
go mod verify
# -> all modules verified
go.sum is generated automatically when go mod tidy adds or
updates a dependency. The file is committed to the repository; a
build that finds a hash mismatch in go.sum fails.
# go.sum (excerpt)
github.com/stretchr/testify v1.8.4 h1:xxxxx...
github.com/stretchr/testify v1.8.4/go.mod h1:yyyyy...
The go.sum database is also checked against the public checksum
database maintained by the Go team; the database is a
content-addressable store of every module’s hashes. A module
whose hash does not match the database fails the build.
Cargo: Cargo.lock
Cargo generates Cargo.lock automatically on the first build
that resolves dependencies. The lockfile records the exact
version and checksum of every direct and transitive dependency.
The lockfile is committed to the repository for binaries; for
libraries, the convention is to commit Cargo.lock for
applications and to omit it for libraries consumed by other
crates.
# Verify the integrity of every crate in the build
cargo verify
# Generate or refresh the lockfile
cargo update
# Cargo.lock (excerpt)
[[package]]
name = "serde"
version = "1.0.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1c2b4149902647c7cb5bbf1d3f5fea9e195213cf8db8c4f0349184b57f4a32"
The checksum field is the SHA-256 of the crate’s .crate file
from crates.io. Cargo verifies the checksum at install time; a
mismatch fails the build. cargo verify re-runs the verification
across the entire dependency tree; the production discipline is
to run cargo verify in CI.
Re-pinning as a deliberate upgrade
A lockfile update is a supply-chain change. The workflow is:
- Identify the upgrade. Read the upstream release notes and advisories.
- Refresh the lockfile. Run
npm install,pip-compile --upgrade,go mod tidy, orcargo update. The tool records the new versions and the new checksums. - Review the diff. The diff shows the old version, the new version, and the new checksums for every changed package. The review is the human link.
- Run the test suite against the new packages. The suite is the team’s evidence that the upgrade does not break the build.
- Commit the lockfile update in a pull request. The PR contains the lockfile diff, the review notes, and a link to the upstream release.
A Dependabot or Renovate bot can automate step 2 and open the PR in step 5; the team still performs steps 3 and 4. A lockfile update merged without review is the same supply-chain hole as a range constraint: the bytes changed under the team’s audit trail without a human link.
Production discipline
- Commit every lockfile to the repository.
package-lock.json,requirements.lock,go.sum,Cargo.lock. - Generate hashes at lockfile creation.
--generate-hashesin pip;go mod tidy; thechecksumfield in Cargo. - Verify the hashes at install time.
npm ci,pip install --require-hashes,go mod verify,cargo verify. - Run lockfile updates through review. A lockfile bump is a supply-chain change.
- Never disable the lockfile in CI. A CI command that resolves against the registry rather than the lockfile has lost reproducibility.
Cross-course references
- Git, CI/CD & GitOps — Part LXVII-01 (Pinning Discipline) defines the discipline that motivates lockfile pinning.
- Git, CI/CD & GitOps — Part LXV-03 (Dependency Trust) covers the SBOM layer lockfile pinning leaves open.
- Git, CI/CD & GitOps — Part LXV-05 (Artifact Trust) covers the content-addressing model every lockfile relies on.
- Linux for Production Sysadmins — Part XXXIV (ConfigMgmt) covers the OS-level analogue: apt/yum/dnf lockfiles.
Quiz
Knowledge check · 4 questions
Q1. What is the difference between a range constraint in `package.json` (e.g., `^1.2.0`) and an entry in `package-lock.json`?
Q2. Running `npm install` in CI is equivalent to running `npm ci` in terms of lockfile discipline.
Q3. Explain the role of the integrity hash (SHA-512 in npm, SHA-256 in pip/Go/Cargo) in a lockfile.
Q4. Identify the gap in the team's lockfile discipline and the rule that closes it.
Team T runs 12 Node services. Each service's `package.json` uses range constraints (`^1.2.0`, `~3.6`). The team commits `package-lock.json` to each repository. The CI pipelines run `npm install` instead of `npm ci`. A new transitive dependency of a popular logging library is published with a typo-squat name (`crossenv` instead of `cross-env`); the malicious package contains a credential-stealing payload. The team's `package-lock.json` records the legitimate version, but the CI pipeline's `npm install` resolves against `package.json` and picks up the malicious version because the lockfile is not enforced.
Passing score: 75%. Answers are checked in this browser.