Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXVII · Dependency PinningPackageLockfiles

Package pinning and lockfiles — npm shrinkwrap, pip-tools, go.sum, Cargo.lock

Advanced⏱ ~26 mingit

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

Not yet marked complete on this device.

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:

  1. Identify the upgrade. Read the upstream release notes and advisories.
  2. Refresh the lockfile. Run npm install, pip-compile --upgrade, go mod tidy, or cargo update. The tool records the new versions and the new checksums.
  3. 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.
  4. Run the test suite against the new packages. The suite is the team’s evidence that the upgrade does not break the build.
  5. 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

  1. Commit every lockfile to the repository. package-lock.json, requirements.lock, go.sum, Cargo.lock.
  2. Generate hashes at lockfile creation. --generate-hashes in pip; go mod tidy; the checksum field in Cargo.
  3. Verify the hashes at install time. npm ci, pip install --require-hashes, go mod verify, cargo verify.
  4. Run lockfile updates through review. A lockfile bump is a supply-chain change.
  5. 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

  1. Q1. What is the difference between a range constraint in `package.json` (e.g., `^1.2.0`) and an entry in `package-lock.json`?

  2. Q2. Running `npm install` in CI is equivalent to running `npm ci` in terms of lockfile discipline.

  3. Q3. Explain the role of the integrity hash (SHA-512 in npm, SHA-256 in pip/Go/Cargo) in a lockfile.

  4. 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.