Floating versions like :latest, ^, and ~ promise convenience but deliver broken builds, hidden regressions, and supply chain risks. Here we explain why they undermine reproducibility and security and shows how to pin GitHub Actions, Docker images, and dependencies safely.
Every engineer has seen it: no code changes, yet your CI pipeline fails. Or worse: the deploy "works," but today's container image isn't the same as yesterday's.
The culprit? :latest
.
It feels convenient, but in reality :latest
means: "trust the internet not to change under you." That's a gamble no production system should make.
Why People Use :latest (and Floating Versions)
Well, it feels convenient. Why pin when you can just grab the newest?
It works… until it doesn't.
The Reliability Problem
The most common failure mode is when the build breaks for no reason. You didn't touch the repo, but the upstream maintainers did.
GitHub Actions: Maintainers push a breaking change to
@v4
. Suddenly your CI is red.Docker images:
python:3.11
tag moves to a new Debian release; your build breaks onpsycopg2
.Packages: A patch release includes a regression; your app misbehaves in prod.
You end up debugging someone else's Tuesday.
The Security Problem (a.k.a. Supply Chain Attacks)
Convenience is one thing. But :latest
is also a security risk. When you don't pin, you're effectively giving strangers commit access to your pipeline.

Take a recent example: in September 2025, attackers phished the npm credentials of a maintainer (qix) and published malicious updates to 18 popular packages, including debug
, chalk
, and ansi-styles
. Together, these libraries see 2.6 billion weekly downloads. The injected malware hooked into browser APIs (fetch
, XMLHttpRequest
, window.ethereum
) to steal cryptocurrency. The poisoned versions were online for only about two hours — but that was long enough for thousands of builds worldwide to ship compromised code silently.
The risks of trusting upstream aren't theoretical. The infamous SolarWinds breach in 2020 compromised the build system of a major software vendor, inserting backdoors into signed updates that were then shipped to 18,000+ customers, including U.S. government agencies and Fortune 500 companies. While SolarWinds wasn't about :latest
tags or lockfiles, the lesson is the same: when your pipeline pulls unpinned, mutable code from the internet, you're outsourcing trust to whoever controls that supply chain today. And if they get compromised, so do you.

:latest
doesn't just mean surprises. It means you're running code you didn't audit, pushed by whoever controlled the account this morning.
How to Pin Everything
So what's the fix? The answer isn't to stop using Actions, Docker, or packages. It’s to take control over what you’re running. Instead of pulling moving targets from the internet, you pin dependencies to exact, immutable versions and let automation help you keep them fresh.
Here are the main areas to watch — and how to pin them safely:
1. GitHub Actions → Pin by Commit SHA
Tags are mutable; SHAs aren't. Use Dependabot or Renovate to bump SHAs with reviewable PRs.
2. Docker Images → Pin by Digest
You can fetch the digest with docker buildx imagetools inspect
. Renovate can keep digests fresh with safe PRs.
3. Packages → Use Lockfiles and Exact Versions
Commit your yarn.lock
, package-lock.json
, or poetry.lock
. Install with --frozen-lockfile
or npm ci
so CI fails if the lock drifts.
And don't allow code or scripts that bypass the lockfile entirely. A raw command like pip install requests
or npm install -g foobar
skips your dependency policy and reintroduces drift.
If it's not pinned in a manifest + lockfile, it shouldn't be in your pipeline.
4. Enforce It in CI
People forget; CI doesn't.
actionlint: fail if Actions aren't pinned to SHAs.
hadolint: flag
:latest
in Dockerfiles.pre-commit hooks: grep for unpinned refs, example:
static analysis: forbid untracked installs like
pip install …
ornpm install -g …
5. Automate Safe Updates
There's no excuse not to pin these days. Tools like Dependabot and Renovate make it painless. They open PRs that:
bump Action SHAs
refresh Docker digests
update lockfiles
Pinning doesn't mean you freeze forever: it means you upgrade on your terms. Every update is explicit and reviewable, no surprises.
And here's another best practice: don't merge non-security updates immediately. Wait a couple of days. If there's a bad regression, the community will find it first. Security patches? Apply fast. Everything else? Let it bake.

Two Real-World Examples
Action drift:
actions/setup-node@v4
changed cache behavior. Our CI times ballooned. Pinned to a SHA, upgrades only through reviewed PRs. No more surprises.Docker tag swap:
python:3.11
moved to a new Debian point release. Brokepsycopg2
builds on arm64. Pinning to digest fixed it; Renovate now refreshes weekly.
The Lesson
Reproducibility isn’t a nice-to-have: it’s a safety feature. Security isn’t optional; it’s a feature.
Floating versions (:latest
, ^
, ~
) give you neither.
At Mergify, we've stopped trusting anything that isn't pinned: Actions by SHA, Docker by digest, dependencies by lockfile. Updates still happen — but on our terms, not upstream's.
If you're still running on :latest
, you're not just saving time — you're borrowing risk.
Pin everything. Sleep better.