Dependency Pinning¶
Dependency pinning is the practice of locking dependency versions to specific packages, tags, or digests. By explicitly specifying the versions of libraries a project uses, developers ensure reproducible builds, reduce the risk of unexpected breaking changes, and mitigate supply-chain security risks.
1. Category¶
1.1. Versioning Specifiers¶
Version specifiers use a combination of symbols and operators to define an acceptable range of package versions, based on the Major.Minor.Patch (X.Y.Z) format of Semantic Versioning.
1.1.1. Label Tags¶
Label tags (latest, stable, main) are mutable symbolic pointers to an underlying version or commit to specify dependencies. Labels may be reassigned to different versions or commits over time, making them inherently unreliable for ensuring build reproducibility and posing significant risks to security and stability.
-
Concepts and Components
-
Mutable Tags
Note
Label tags are symbolic references that can be reassigned to different commits or versions over time, making them unreliable for consistent builds. This mutability allows dependencies to change unexpectedly, potentially introducing bugs, vulnerabilities, or breaking changes without notice.
-
-
Features and Benefits
-
Simplicity
Label tags are easy to use and understand, making them accessible for developers who may not be familiar with versioning concepts.
-
Flexibility
They allow developers to always use the most recent version of a dependency without needing to update version numbers manually.
-
Risk
Using label tags can lead to unpredictable builds, as the underlying version may change without warning. This can introduce bugs, vulnerabilities, or breaking changes into a project.
-
-
Examples and Explanations
-
latestRefers to the most recent stable release of a package, which can change frequently.
-
stableRefers to a release that is considered stable and ready for production use, but the exact commit it points to can change.
-
mainRefers to the main development branch, which is frequently updated and may contain unstable or experimental code.
-
1.1.2. Version Tags and Ranges¶
1.1.2.1. Version Tags¶
Version tags specify an exact version of a dependency, ensuring that the same version is used every time the project is built. This method reduces the risk of unexpected changes but still relies on the integrity of the package source.
-
Concepts and Components
-
Semantic Versioning (SemVer)
A versioning scheme that uses
MAJOR.MINOR.PATCHincrements to convey the nature of changes in a release. -
Mutable Tags
Note
Version tags are generally intended to be immutable identifiers for specific releases, but they can be overwritten (e.g., via Git force-push), allowing the underlying code to change without updating the tag name. This risks introducing vulnerabilities or breaking changes into builds that pin to the tag, unlike hash-based pinning which ensures true immutability. Stronger integrity is achieved by combining version tags with lockfiles, signed releases, cryptographic checksums/digests, or pin to an exact commit or artifact digest.
-
-
Features and Benefits
-
Reproducible Builds
Pinning to a specific version tag ensures that the same code is used every time, leading to consistent and reliable builds.
-
Stability
Using version tags helps avoid unexpected breaking changes that can occur with mutable tags.
-
Security
By specifying exact versions, developers can avoid inadvertently pulling in versions with known vulnerabilities.
-
Risk
Since version tags can be overwritten, there is a risk that the code associated with a tag may change, potentially introducing vulnerabilities or breaking changes.
-
-
Examples and Explanations
-
1.2.3Specifies exactly version
1.2.3of a dependency.
-
1.1.2.2. Version Ranges¶
Version ranges allow developers to specify a set of acceptable versions for a dependency. This provides flexibility to receive minor updates and patches while avoiding major version changes that could introduce breaking changes. However, it still relies on the integrity of the package source.
-
Concepts and Components
-
Caret (
^), Tilde (~), InequalitiesCommon syntaxes used to define version ranges in package managers.
-
Mutable Tags
Note
Version ranges allow automatic updates within the specified bounds (e.g., minor or patch versions), making the resolved dependency inherently mutable over time. Builds performed at different times can resolve to different concrete versions (depending on registry state and transitive updates), which reduces reproducibility and may silently introduce vulnerabilities or regressions.
-
-
Features and Benefits
-
Flexibility
Version ranges allow projects to automatically receive non-breaking updates, such as bug fixes and new features, without manual intervention.
-
Reduced Maintenance
Developers do not need to frequently update dependency versions, as the package manager will handle updates within the specified range.
-
Risk
Because version ranges can resolve to different versions over time, they may introduce unexpected bugs, vulnerabilities, or breaking changes if not carefully managed.
-
-
Examples and Explanations
-
~1.2.3Allows patch-level changes if a minor version is specified on the comparator. Allows minor-level changes if not.
-
>=1.2.3Allows any version greater than or equal to
1.2.3. -
^1.2.3Allows changes that do not modify the left-most non-zero digit in the version.
-
1.1.3. Transitive Dependencies¶
Transitive dependencies are dependencies of dependencies. Pinning transitive dependencies can help ensure that a project remains stable and secure, as changes in these dependencies can introduce bugs or vulnerabilities.
-
Concepts and Components
-
Lockfiles
Files that record the exact versions of dependencies and transitive dependencies used in a project. Lockfiles ensure reproducible builds by locking down the entire dependency tree.
-
Immutable
Pinning transitive dependencies records the fully resolved dependency graph for consistency across builds. Stronger integrity is achieved by combining lockfiles with cryptographic digests.
-
-
Features and Benefits
-
Dependency Drift
Pinning transitive dependencies prevent dependency drift, where indirect dependencies change unexpectedly, potentially causing build failures or introducing vulnerabilities.
-
Reproducible Builds
Lockfiles ensure that all developers and CI systems use the same versions of dependencies, leading to consistent and reliable builds.
-
Security
By locking transitive dependencies, projects can avoid inadvertently pulling in versions with known vulnerabilities.
-
Maintenance
Lockfiles can be updated periodically to incorporate security patches and bug fixes in a controlled manner.
-
-
Examples and Explanations
-
package-lock.jsonA lockfile used by npm to pin the exact versions of dependencies and transitive dependencies.
-
1.1.4. Hash Digest¶
Hash digest pinning is the most secure method of dependency management. It identifies a dependency by the cryptographic hash of its contents rather than a version number or label. This ensures that the code being used is bit-for-bit identical to what was expected, effectively neutralizing risks associated with tag overwrites or compromised registries.
-
Concepts and Components
-
Cryptographic Hash Functions (SHA-256)
Algorithms that produce a fixed-size hash value from input data, ensuring data integrity and authenticity.
Note
Although accidental and intentional hash collisions are theoretically possible, the probability is negligible with modern algorithms like SHA-256.
-
Immutable
The hash digest identifies the exact bytes of the artifact to provides immutability of the referenced package contents, ensuring reproducible builds and strong integrity checks.
-
-
Features and Benefits
-
Data Integrity
Hash digests ensure that the content of a dependency has not been altered, providing strong guarantees against tampering or corruption.
-
Reproducible Builds
Pinning dependencies by hash digest guarantees that the exact same code is used every time, leading to consistent and reliable builds.
-
Security
Using hash digests mitigates risks associated with compromised package registries or overwritten tags, as the hash will not match if the content has changed.
-
Maintenance
While hash digest pinning provides strong security, it may require more effort to update dependencies, as developers must obtain the new hash for each update.
-
-
Examples and Explanations
-
Lockfile Entry
A SHA-256 hash digest that uniquely identifies the exact content of a dependency package.
-
Dockerfile
Container file defining a immutable base image using specific image digest.
-
1.2. Versioning Syntax¶
Versioning syntax expresses the rules for specifying acceptable versions of dependencies using symbols and operators.
-
Symbols
Versioning symbols provide shorthand notations to define version ranges based on Semantic Versioning principles.
Symbol Example Name Defined Range Practice ^ ^1.2.3 Caret \ge 1.2.3 and < 2.0.0 Allow minor and patch updates (new features and bug fixes) while avoiding major breaking changes. ~ ~1.2.3 Tilde \ge 1.2.3 and < 1.3.0 Allow only patch updates (bug fixes) while strictly locking features to a specific minor version. * 1.2.* Wildcard \ge 1.2.0 and < 1.3.0 Matches any number in the specified position. Typically used like the tilde to allow patch updates (1.2.*). None 1.2.3 Exact = 1.2.3 Locks the package to a single, specific version. -
Operators
Versioning operators provide precise control over acceptable versions by defining inequalities and ranges.
Operator Example Name Meaning Practice > >1.2.3 Greater Than Must be strictly later than 1.2.3. Used to set a minimum floor, often without an upper limit. < <2.0.0 Less Than Must be strictly earlier than 2.0.0. Used to set an upper ceiling, preventing major updates. >= >=1.2.3 Greater Than or Equal To Must be 1.2.3 or any later version. Common way to define a starting point for an open range. <= <=1.2.3 Less Than or Equal To Must be 1.2.3 or any earlier version. Locks to a specific version or older. = =1.2.3 Equal To Must be exactly 1.2.3. Same as using no symbol (the Exact match). >/< >=1.2.3 <1.5.0 Comparison Between 1.2.3 (inclusive) and 1.5.0 (exclusive). Used to define precise version ranges. - 1.2.3 - 2.0.0 Hyphen Between 1.2.3 and 2.0.0, inclusive. Simplified syntax for defining inclusive ranges.
1.3. Versioning Strategies¶
Versioning strategies combine different pinning methods to balance reproducibility, security, and flexibility.
| Strategy | Syntax | Reproducibility | Security | Practice |
|---|---|---|---|---|
| Exact pinning + lockfile + hashes | package==1.2.3 + lockfile with SHA256 |
100% (Immutable) | Highest | Production, CI, releases |
| Git / URL pinning | git+https://...#commit=abc123 |
100% (Immutable, unless repo deleted) | Highest | Critical deps, security |
| Exact pinning + lockfile (no hashes) | package==1.2.3 + conan.lock/yarn.lock |
High (Registry dependent) | Very High | Most real-world projects |
| Exact pinning only (no lockfile) | package==1.2.3 in requirements.txt |
Medium (Transitive drift risk) | Medium | Small scripts |
| Pessimistic tilde | ~=1.2.3 → ≥1.2.3, <1.3.0 |
Low (Allows patch updates) | Medium | Python (Poetry default) |
Tilde (~) |
~1.2.3 → ≥1.2.3, <1.3.0 |
Low (Allows patch updates) | Medium | Recommended for minor flexibility |
Caret (^) |
^1.2.3 → ≥1.2.3, <2.0.0 |
Very Low (Allows minor & patch updates) | Low | Risky for reproducibility |
| Loose inequalities | >=1.2.3 or * |
None (Unpredictable) | Lowest | Dev only, never commit |
2. References¶
- arXiv Dependency Pinning page.
- Renovate Dependency Pinning page.
- Sentenz Versioning article.
- Sentenz Dependency Manager article.