If It Quacks Like a Package Manager
Source: Hacker News
I spend a lot of time studying package managers, and after a while you develop an eye for things that quack like one. Plenty of tools have registries, version pinning, code that gets downloaded and executed on your behalf. But flat lists of installable things aren’t very interesting.
The quacking that catches my ear is when something develops a dependency graph: your package depends on a package that depends on a package, and now you need resolution algorithms, lockfiles, integrity verification, and some way to answer “what am I actually running and how did it get here?”
Several tools that started as plugin systems, CI runners, and chart‑templating tools have quietly grown transitive dependency trees. Now they walk like a package manager, quack like a package manager, and have all the problems that npm, Cargo, and Bundler have spent years learning to manage—though most of them haven’t caught up on the solutions.
GitHub Actions
| Feature | Status |
|---|---|
| Registry | GitHub repos |
| Lockfile | No |
| Integrity hashes | No |
| Resolution algorithm | Recursive download, no constraint solving |
| Transitive pinning | No |
| Mutable versions | Yes – git tags can be moved. Immutable releases lock tags after publication but can still be deleted. |
I wrote about this at length already. When you write
uses: actions/checkout@v4
you’re declaring a dependency that GitHub resolves, downloads, and executes. The runner’s PrepareActionsRecursiveAsync walks the tree by:
- Downloading each action’s tarball.
- Reading its
action.ymlto find further dependencies. - Recursing up to ten levels deep.
There’s no constraint solving at all. Composite‑in‑composite support was added in 2021, creating the transitive‑dependency problem, and a lockfile was requested (closed as “not planned” in 2022).
You can SHA‑pin the top‑level action, but Palo Alto’s “Unpinnable Actions” research showed that transitive dependencies remain unpinnable regardless. The tj‑actions/changed‑files incident (Mar 2025) started with reviewdog/action-setup (a dependency of a dependency) and cascaded outward when the attacker retagged all existing version tags to point at malicious code that dumped CI secrets to workflow logs, affecting > 23 000 repos.
GitHub has since added SHA‑pinning enforcement policies (top‑level only) — see the announcement.
Ansible Galaxy
| Feature | Status |
|---|---|
| Registry | galaxy.ansible.com |
| Lockfile | No |
| Integrity hashes | Opt‑in |
| Resolution algorithm | resolvelib |
| Transitive pinning | No |
| Mutable versions | Yes, no immutability guarantees |
Ansible collections and roles install via ansible-galaxy from galaxy.ansible.com. Dependencies are declared in meta/requirements.yml. When you install a role, its declared dependencies are installed automatically, and those dependencies can have their own dependencies, forming a real transitive tree.
- The resolver is
resolvelib, the same back‑tracking constraint solver pip uses—more sophisticated than what Terraform or Helm use. - A lockfile was first requested in 2016 (archived) and re‑opened in 2018; the issue remains open.
- The now‑archived Mazer tool implemented
install --lockfilebefore being abandoned in 2020, so the feature existed briefly and then disappeared.
ansible-galaxy collection verify can check checksums against the server, and GPG‑signature verification exists, but both are opt‑in and off by default. Published versions on galaxy.ansible.com can be overwritten by the publisher, and roles sourced from git repos suffer the same mutable‑tag problem as GitHub Actions.
Roles execute with the full privileges of the Ansible process (with optional become escalation). There are long‑standing open issues about the inability to exclude or override transitive role dependencies (see #13215).
Terraform Providers and Modules
| Feature | Status |
|---|---|
| Registry | registry.terraform.io |
| Lockfile | .terraform.lock.hcl |
| Integrity hashes | Yes |
| Resolution algorithm | Greedy, newest match |
| Transitive pinning | Yes for providers; no for modules |
| Mutable versions | Providers immutable; modules use mutable git tags |
Terraform learned from classic package managers:
.terraform.lock.hclrecords exact provider versions and cryptographic hashes (multiple formats).terraform initverifies downloads against those hashes, and providers are GPG‑signed.- Version‑constraint syntax (
~> 4.0,>= 3.1, etc.) is supported.
Note: The lock file only tracks providers, not modules (GitHub issue #31301). Consequently, nested module dependencies require cascading version bumps with no lock‑file protection. Git tags used to pin modules are mutable, meaning a tag‑pinned module can be silently replaced with different content (GitHub issue #29867).
Researchers demonstrated registry typosquatting (hashic0rp/aws with a zero) (BoostSecurity article) and a live supply‑chain‑attack demo at NDC Oslo 2025 (Class Central video) showed this working in practice.
The provider side is solid, but the module side of the transitive tree suffers the same mutable‑reference problems as GitHub Actions.
Helm Charts
| Feature | Details |
|---|---|
| Registry | Chart repos / OCI registries |
| Lockfile | Chart.lock |
| Integrity hashes | Opt‑in |
| Resolution algorithm | Greedy, root precedence |
| Transitive pinning | Yes |
| Mutable versions | Depends on registry; OCI digests are immutable, chart‑repo tags are not |
Kubernetes Helm has more package‑manager DNA than most tools here.
Chart.yamldeclares dependencies with version constraints.Chart.lockrecords the exact resolved versions.- Subcharts can have their own dependencies, building genuine transitive trees.
The resolver (source) picks the newest version matching each constraint, with versions specified closer to the root taking precedence when conflicts arise.
- Chart repositories serve an
index.yamlthat works like a package index. - OCI registries also work, but mutability differs:
- OCI digests – content‑addressed and immutable.
- Traditional chart repos – publishers can overwrite a version by re‑uploading to the same URL;
Chart.lockrecords version numbers, not content hashes, so it won’t catch the change.
Helm supports provenance files for chart signing, though adoption remains low (Helm provenance docs).
Known Limitations & Vulnerabilities
helm dependency buildonly resolves first‑level dependencies (GitHub issue #2247), not transitive ones.- You can’t set values for transitive dependencies without explicitly listing them (GitHub issue #8289).
- There’s no way to disable a transitive subchart’s condition (GitHub issue #12020).
- Symlink attack via
Chart.lockallowed local code execution when runninghelm dependency update; fixed in v3.18.4 (GHSA‑557j‑xg8c‑q2mm). - Malicious Helm charts have been used to exploit Argo CD and steal secrets from deployments (APIiro blog).
If It Has Transitive Execution, It’s a Package Manager
Once a tool develops transitive dependencies, it inherits a specific set of problems—whether it acknowledges them or not:
| Problem | Description |
|---|---|
| Reproducibility | The tree can resolve differently each time; a lockfile is needed to record the exact result. |
| Supply‑chain amplification | A single compromised package deep in the tree can cascade outward through every project that depends on it (example). |
| Override and exclusion | Users need mechanisms to deal with transitive dependencies they didn’t choose and don’t want. |
| Mutable references | Version tags that can be moved, rewritten, or force‑pushed mean the same identifier may point at different code tomorrow. |
| Full‑tree pinning | Pinning direct dependencies is ineffective if their dependencies use mutable references. |
| Integrity verification | You must be able to confirm that what you’re running today is the same thing you ran yesterday. |
If your tool exhibits these issues, it is a package manager. No amount of calling it a “plugin system” or “marketplace” will stop supply‑chain attacks from knocking at your door.