Dependency Management
Selecting, pinning, and auditing third-party packages with intent.
Overview
Dependency management covers adding, auditing, pinning, and updating third-party libraries. A well-managed dependency graph minimises security surface area, keeps builds reproducible, and prevents version conflicts. npm, Yarn, Bundler, and pip all provide lockfiles that pin transitive dependency trees to exact versions. Regular audits and automated updates (Dependabot, Renovate) are essential practices.
Origin
Maven (2002) introduced centralised package repositories and transitive dependency resolution to Java. Bundler (Carl Lerche and Yehuda Katz, 2009) brought lockfile-based reproducibility to Ruby. npm (Isaac Schlueter, 2010) scaled the model to JavaScript. npm security incidents (left-pad, 2016; event-stream, 2018) demonstrated the supply chain risks of shallow dependencies on untrusted packages.
Examples
npm audit and Renovate configuration
// .github/renovate.json - automated dependency updates
// {
// "$schema": "https://docs.renovatebot.com/renovate-schema.json",
// "extends": ["config:base"],
// "schedule": ["before 8am on Monday"],
// "prConcurrentLimit": 5,
// "packageRules": [
// {
// "matchUpdateTypes": ["patch", "minor"],
// "matchPackagePatterns": ["*"],
// "automerge": true,
// "automergeType": "pr",
// "requiredStatusChecks": ["ci/test"]
// },
// {
// "matchUpdateTypes": ["major"],
// "automerge": false,
// "labels": ["dependencies", "major-update"]
// },
// {
// "matchPackageNames": ["@types/*"],
// "automerge": true
// }
// ]
// }
// package.json security practices:
// - Lock exact versions for security-sensitive packages
// - Audit regularly: npm audit --audit-level=high
// - Check for known malicious packages: npm install @socket.dev/cli
// - Pin Node.js engine: "engines": { "node": ">=20.0.0 <21.0.0" }Renovate auto-merges patch and minor updates that pass CI, dramatically reducing dependency lag. Major updates require human review. automergeType: "pr" merges via a PR, keeping the audit trail. Contrast with direct push automerge which bypasses branch protection.
Bundler Gemfile best practices with groups
# Gemfile
source 'https://rubygems.org'
ruby '3.3.0' # Pin Ruby version for reproducible builds
# Core dependencies: no version wildcards for security-sensitive gems
gem 'rails', '~> 7.1.0'
gem 'pg', '~> 1.5'
gem 'puma', '~> 6.4'
gem 'redis', '~> 5.0'
gem 'sidekiq', '~> 7.2'
# ~> (pessimistic operator) allows patch updates but not minor or major
gem 'stripe', '~> 10.0'
gem 'dry-validation', '~> 1.10'
group :development, :test do
gem 'rspec-rails', '~> 6.1'
gem 'factory_bot_rails', '~> 6.4'
gem 'faker', '~> 3.2'
end
group :development do
gem 'rubocop', '~> 1.60', require: false
gem 'rubocop-rails', '~> 2.23', require: false
gem 'solargraph', require: false
end
group :test do
gem 'simplecov', require: false
gem 'vcr', '~> 6.2' # Record and replay HTTP interactions
endGroup isolation ensures development/test gems are not loaded in production (bundle install --without development test). ruby '3.3.0' in the Gemfile triggers an error if the wrong Ruby version is active, catching environment mismatches early.
Use Cases
- 01Lockfile commitment ensures every developer and CI environment installs the exact same dependency tree, eliminating "works on my machine" issues from transitive dependency drift
- 02Dependency auditing in CI (npm audit, bundle-audit) catches known CVEs before deployment and blocks the build on high-severity vulnerabilities
- 03Private package registries (Verdaccio, GitHub Packages) for internal libraries that should not be published to the public npm registry
- 04Supply chain security: pinning SHA hashes instead of version ranges for critical dependencies prevents dependency confusion attacks
When Not to Use
- //Do not commit node_modules to the repository; use the lockfile and npm ci in CI for reproducible installs without the bloat
- //Do not use * version ranges in production Gemfiles or package.json; it makes builds non-reproducible and hides upstream breaking changes until they break your build
- //Do not ignore npm audit warnings because they are "only in devDependencies"; malicious packages in dev dependencies can exfiltrate credentials via postinstall scripts
Technical Notes
- npm ci (clean install) installs exactly what is in package-lock.json and fails if the lockfile is out of sync with package.json, making it the correct install command for CI pipelines
- Semantic versioning (SemVer) promises: patch (1.0.x) for backwards-compatible bug fixes, minor (1.x.0) for new backwards-compatible features, major (x.0.0) for breaking changes. In practice many packages do not honour this contract consistently
- bundle-audit (ruby-advisory-db) checks Gemfile.lock against the Ruby advisory database. Running bundle audit check --update in CI catches gems with disclosed CVEs before deployment
- Yarn Plug'n'Play (PnP, Yarn 2+) eliminates node_modules entirely; packages are stored in a zip archive and resolved via a generated .pnp.cjs loader. This drastically reduces disk usage and install time but requires tooling support (some tools rely on node_modules paths)