The lockfile your pipeline never had
Package managers got lockfiles a decade ago; CI workflows did not, which is how one moved tag reached 23,000 repositories. Would your build survive the marketplace vanishing?
A package.json without a lockfile would not pass review at any company I have worked with, and a workflow file that pulls a dozen third-party actions by tag passes every day. The two are the same mistake. A tag is a pointer that its owner can move, and in March 2025 one of the most widely used actions had its tags moved to a commit that dumped every secret in the workflow's environment into the build logs. The Habits app builds in GitHub Actions in a public repository and SocialSure's platform deploys through CI/CD, so the incident was not abstract, and the test I built afterwards is the subject of this post.
One moving tag, twenty-three thousand repositories
The compromise of tj-actions/changed-files, tracked as CVE-2025-30066, rewrote the action's version tags, from v1 through v45.0.7, to point at a malicious commit on 14 and 15 March 2025. Any workflow referencing the action by tag, which was nearly all of them, ran the malicious code on its next trigger; the action was used in over 23,000 repositories. The chain behind it, per CISA's alert, ran through a second compromised action, reviewdog/action-setup, on 11 March, itself reached through the maintainers' access to the SpotBugs project. Three hops, each through a credential the previous hop exposed, and the final hop multiplied by every repository that trusted a tag.
Workflows that referenced the action by its full commit SHA were unaffected, because a SHA cannot be moved. That is the entire lesson, and it is the one every package manager learned in the previous decade: a name resolves to whatever the name's owner says today, and a hash resolves to one thing forever.
Why a tag floats and a SHA does not
A git tag is a name for a commit, and unlike a commit it can be deleted and recreated to point somewhere else. The Actions ecosystem built a convention on that: a major-version tag such as v4 is meant to move, so that users get patch releases without editing their workflows, and even the exact-version tags are moved occasionally by maintainers fixing a bad release. The convenience is real and it is exactly the property the attackers used. A full-length commit SHA is the hash of the commit's content and history, so pointing at it means pointing at one immutable thing, which is why GitHub's own security hardening guidance recommends pinning third-party actions to a full commit SHA rather than a tag.
The objection is maintenance: a SHA does not tell you which version it is, and nobody wants to update forty hexadecimal strings by hand. The answer that the package ecosystems settled on years ago applies unchanged. The version lives in a comment next to the SHA, a bot such as Dependabot opens a pull request when a new release moves the SHA forward, and a human reads the diff before merging. Updates still happen; they just happen through review rather than through a tag moving under you overnight.
What the roadmap adds
GitHub's 2026 security roadmap for Actions is, in effect, the lockfile arriving: a dependencies section in the workflow file that records every action the workflow uses, direct and transitive, pinned to a commit SHA, in the way a language lockfile records every package. It arrives alongside rulesets that control who may trigger a workflow and which events may run one, and an evaluation mode that shows what a policy would block before it is enforced. Once the lockfile is generally available, the question changes from "did you pin your actions" to "could your build run if the marketplace were gone", and the second question is the one worth asking now, because a team can answer it today with the tools it already has.
The vanished-marketplace test
Here is the test. Imagine that every third-party action repository and the GitHub Marketplace disappear tonight. Would your workflow still run tomorrow? A step passes if it is first-party, meaning the action is published by GitHub itself; or vendored, meaning the action's source is copied into a repository your organisation owns and referenced from there; or pinned to a commit SHA that you have mirrored, meaning a copy of that exact commit exists somewhere you control. A step referencing a third-party action by tag, branch or unmirrored SHA fails, and a workflow fails if any step does.
The test is deliberately stricter than "pin to a SHA". A SHA pin protects against a moved tag, which is the March 2025 attack, and it does not protect against the repository being deleted, renamed, or taken private, which has happened to actions more than once and which turns a pinned SHA into a build that cannot start. Mirroring the commit, or vendoring the action outright, is what makes the SHA resolvable on the night the marketplace is gone. For the handful of actions a small team actually uses, the vendoring is an afternoon.
What pinning does and does not cover
Even a workflow that passes the test has dependencies the test does not see, and it is worth being honest about the boundary. An action pinned to a SHA may itself use other actions, and the roadmap's lockfile is the first thing that pins those transitively; until then, the test applies to the actions you vendor, whose own uses lines you can read. An action may pull a container image by tag, or run an npm install of its own with a floating range, and neither is pinned by the SHA of the action's repository. And a runner image changes under you on GitHub's schedule.
That boundary is why the test is about vendoring and mirroring rather than about pinning. A vendored action can be read, and its own uses lines, image tags and install commands can be pinned in the copy. A pinned but unvendored action is a black box whose insides float.
The workflow, before and after
The Habits pipeline is small: check out, set up a JDK, cache Gradle, build, run tests, sign, upload. Before the test, four of those steps referenced third-party actions by major version tag, which is the reference that moved in March 2025. After the test, the first-party steps stayed as they were, the two third-party actions that were genuinely useful were vendored into an organisation repository with their internal dependencies pinned, and one action was replaced by a dozen lines of shell that did the same thing without a dependency. The workflow got a few lines longer and the test passes. The signing step, which is the one that handles a credential, references nothing outside the organisation.
A last word on fail-closed. The test is only useful if the pipeline enforces it, and the cheapest enforcement is a check, run on every workflow change, that fails when a uses line references anything outside the allowed set. Then a colleague adding a convenient action by tag gets a red build and a link to this rule, which is a better outcome than the action working perfectly until the day its tags move. The roadmap will make the check native. Until it does, a shell script over the workflow files is the lockfile your pipeline never had.
Get new posts by email
Occasional essays on engineering, AI, and building for the people technology leaves behind.
Subscribe with RSS