26 March 2026 · 5 min read

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.

The chain from SpotBugs to 23,000 repositories Four boxes left to right: SpotBugs, a static analysis project whose workflow leaked a token; reviewdog/action-setup, compromised on 11 March 2025; tj-actions/changed-files, whose tags were moved on 14 and 15 March; and over 23,000 repositories whose workflows referenced those tags. Arrows show each hop reached through a credential the previous one exposed. Three hops and a multiplier SpotBugs token via workflow reviewdog 11 March 2025 changed-files tags moved in March 23,000+ repos on next trigger Each hop used a credential the previous one exposed; the last was a trusted tag. a workflow pinned to a commit SHA did not move
Source: the Wiz analysis and the CISA alert.

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 vanished-marketplace test as a decision flow over one workflow step A flow for each uses line in a workflow: is the action first-party, published by GitHub? If yes, pass. If no, is it vendored into a repository the organisation owns? If yes, pass. If no, is it pinned to a commit SHA that is mirrored? If yes, pass. Otherwise fail, and the workflow fails. For every uses line First-party? published by GitHub no Vendored? copied into your org no SHA, mirrored? a copy you control no fail yes yes yes pass: the step runs with the marketplace gone A workflow passes only if every step does. A tag, a branch, or an unmirrored SHA fails. the mirrored-SHA branch is the one that catches a moved tag
Illustrative: the test as a flow; it turns supply-chain hygiene into a yes-or-no check on a single file.

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.

What a SHA pin on the action covers, and what it leaves floating A table of five dependency kinds with whether a SHA pin on the action covers them: the action's own code, yes; actions it uses transitively, no, until the lockfile; container images it pulls by tag, no; packages it installs with floating ranges, no; the runner image, no, GitHub's schedule. Filled squares mark covered. The pin's reach DEPENDENCY COVERED BY A SHA PIN The action's own code yes: a SHA cannot move Actions it uses in turn not until the lockfile pins them Container images it pulls by tag no: pin the digest separately Packages it installs no: the action's own lockfile The runner image no: GitHub's schedule
Illustrative: the boundary of what pinning the action covers; the second row is the one the 2026 roadmap's lockfile is designed to close.

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.

GitHub ActionsSupply ChainCI/CD
All writing

Written by Mohd Shayan

Get new posts by email

Occasional essays on engineering, AI, and building for the people technology leaves behind.

One email per new post. Unsubscribe any time.

Subscribe with RSS