Skip to main content

On 14 October GitHub put workflow dependency lockfiles into public preview. The pitch is short and correct: tags and branches can move after you review a workflow, so a later run might execute different code. A lockfile lets you keep the readable reference in the workflow while running the exact commit you reviewed.

We wanted to know how much of the ecosystem that actually changes. So we parsed every workflow file in the 300 most-starred active repositories on GitHub: 246 of them run Actions, between them 3,670 workflow files and 23,601 uses: references. Then we resolved each referenced action’s own definition to see what it pulls in behind your back.

The headline is not that people are careless. The headline is that pinning a commit at the top level does not pin the chain, and that the thing which actually changed under everyone last month was not lockable at all.

TL;DR

  • 7,802 of 20,194 external action references (38.6%) point at a tag or branch, not a commit. 417 of those point at a bare branch name such as main.
  • Every major-version tag we checked has moved. actions/checkout@v4 pointed at one commit in October 2023 and a different one in July 2026, a 33-month drift under a reference that never changed.
  • Pinning the top level is not enough. 389 of the referenced actions are composite actions or reusable workflows with dependencies of their own. 29 repositories SHA-pin a parent action that then runs a floating tag, including 15 repositories that pin every single reference in their own workflows.
  • Pinning without automation rots. 274 of 877 distinct pinned commits are more than a year old, 68 are more than three years old, and 52 repositories carry a stale pin with no Dependabot configuration for actions.
  • A lockfile cannot lock the runtime. 382 referenced actions still declare node12, node16 or node20 across 4,222 call sites. GitHub removed Node 20 from the runners on 23 September. Those actions now execute on Node 24 no matter which commit you locked.

What the lockfile actually does

The gh actions-lock extension generates .github/workflows/actions.lock. Once a workflow is onboarded, GitHub Actions requires every dependency of that workflow to have a lockfile entry and runs the recorded commit even if the tag or branch moves. Crucially, the generator covers transitive dependencies and reusable workflow dependencies, not just the references you typed. There is a Require lockfile policy for enterprise, organisation and repository administrators, and Dependabot regenerates the matching entry when it raises a version bump.

Two caveats worth saying out loud before anyone reorganises their week around this. The preview is scoped to GitHub Enterprise Cloud and GitHub Enterprise Cloud with data residency. And as GitHub puts it, locking does not determine whether the dependency is safe. It makes today’s code reproducible tomorrow. That is a different property from trustworthy.

Finding one: 38.6% of references float

Of the 23,601 uses: lines we parsed, 3,393 are local paths inside the repository and 14 are direct Docker image references. That leaves 20,194 external references, split as follows.

Reference style Count Share of external
Full 40-character commit SHA 12,392 61.4%
Tag (v4, v4.4.0) 7,385 36.6%
Branch (main, master, dev) 417 2.1%

At repository level the picture is sharply three-way. 82 repositories SHA-pin every external reference they make. 92 pin none of them. 72 are somewhere in between, which is the most dangerous posture of the three because it reads as deliberate when it is usually just the residue of whichever contributor touched the file last.

Finding two: the tags really do move

The argument for lockfiles rests on the claim that a major-version tag is mutable. It is easy to assert and easy to verify, so we did. For eight of the most heavily used first-party actions we compared the commit the floating major tag points at today with the commit its .0.0 patch tag points at.

Reference First commit under that major Commit it points at now Now identical to
actions/checkout@v4 17 Oct 2023 16 Jul 2026 v4.4.0
actions/checkout@v5 11 Aug 2025 16 Jul 2026 v5.1.0
actions/upload-artifact@v4 14 Dec 2023 19 Mar 2025 v4.6.2
actions/download-artifact@v4 14 Dec 2023 24 Apr 2025 v4.3.0
actions/cache@v4 16 Jan 2024 24 Sep 2025 v4.3.0
actions/setup-node@v4 23 Oct 2023 2 Apr 2025 v4.4.0
actions/setup-python@v5 5 Dec 2023 24 Apr 2025 v5.6.0
actions/github-script@v7 13 Nov 2023 6 Jun 2025 v7.1.0

Eight out of eight have moved. This is not misbehaviour, it is the documented contract for major-version tags, and it is exactly why reviewing a workflow tells you nothing durable about what will run next Tuesday.

Finding three: you pinned one link in the chain

This is the finding we did not expect to be as clean as it is. We resolved the definition of all 1,389 distinct references in the corpus. 924 are JavaScript actions, 71 are Docker actions, 301 are composite actions and 88 are reusable workflows. Those last two categories contain uses: lines of their own: 863 further external references, 265 of which point at a floating tag.

The result is that 44 of the 246 repositories reach a moving tag through a dependency they never declared. In 29 of them the parent was SHA-pinned, meaning the maintainer did the work, read the diff, wrote out the forty hex characters, and still handed control to a tag one level down.

The cleanest example is first-party. actions/upload-pages-artifact is a composite action published by GitHub, and ten repositories in our sample pin it to a commit. Two of them, google-gemini/gemini-cli and mermaid-js/mermaid, pin the commit 56afc609, whose definition ends like this:

    - name: Upload artifact
      id: upload-artifact
      uses: actions/upload-artifact@v4

Their workflow says a commit. The action it runs says v4. And as the table above shows, v4 is 15 months further along than it was when that composite action was written.

The sting is that GitHub fixed this. The current v5 of actions/upload-pages-artifact pins its own child to a SHA. But a commit pin is permanent, so the two repositories above are locked onto a version of the action from before the fix, and will stay there until someone bumps them. Pinning preserves hygiene and bad hygiene with equal fidelity. A lockfile would have recorded that floating v4 as a concrete commit, which is better, and would still have frozen the repositories on an action their upstream has since moved past.

It goes deeper than one level. Resolving those 863 children found 54 more composite actions and 7 more reusable workflows beneath them, carrying 99 references at the third level, 17 of them still floating. Whatever you think your CI dependency graph is, it has at least three layers and you have read one.

Finding four: pinning without automation is just freezing

A commit SHA is not a safety property on its own. We resolved the commit date of every distinct pinned SHA in the corpus. The median pin is 202 days old. 274 of 877 are more than a year old, 138 more than two years, and 68 more than three years. The oldest points at a commit from 1 October 2019. Weighted by call site, 1,547 of 12,359 SHA references execute code that was written more than a year ago.

Dependabot is the mechanism that is meant to stop this, and the numbers say the people who most need it are the ones who have it:

Pinning posture Repositories Dependabot configured for actions
Every reference SHA-pinned 82 52 (63%)
Mixed 72 31 (43%)
No SHA pinning at all 92 9 (10%)

That still leaves 101 repositories carrying at least one pin older than a year, 52 of them with no Dependabot configuration for actions at all. And some of those pins point somewhere that no longer exists in the form the author expected: 20 of the 540 referenced action repositories are archived (88 references across 23 repositories, led by actions-rs/toolchain and actions/upload-release-asset), two return 404 outright, and 13 now redirect to a different repository name, in most cases under a different owner. The workflow file still says the old name. GitHub’s rename redirect quietly resolves it. Nobody gets a notification.

We have made the same argument about npm: an attestation that proves where a package was built is worth more than a version number you trust, and a licence can change under a pinned range without anyone noticing. Provenance and licence drift are the same shape of problem one ecosystem over.

Finding five: the lockfile cannot lock the runtime

Here is the part no lockfile fixes, and it happened three weeks ago.

On 23 September GitHub removed Node 20 from the Actions runners. JavaScript actions now execute on Node 24 regardless of what their action.yml declares, and the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION opt-out is gone. In our corpus, 382 of the 1,297 resolved action definitions still declare a retired runtime: 249 say node20, 86 say node16 and 47 say node12. Between them that is 4,222 call sites.

Every one of those runs today on a runtime its author never tested against. Lock the commit as hard as you like: the interpreter underneath it is not in the lockfile, and it changed anyway. Reproducibility in CI is always partial, and knowing which half you have is the actual skill. We found the same when we timed 141 CI jobs and discovered a quarter of the runner time went on setup nobody had ever looked at.

What to do this week

  1. Count your floating references before you argue about them. grep -rhoE 'uses:\s*\S+' .github/workflows | sort | uniq -c | sort -rn takes ten seconds and settles the question.
  2. Resolve one level down. For every action you have pinned, open its action.yml and check whether runs.using is composite. If it is, read its uses: lines. That is where the 29 repositories above lost control.
  3. Turn on Dependabot for github-actions before you pin anything. Pinning without it converts a moving-target problem into a frozen-vulnerability problem, and the second one is quieter.
  4. Audit for archived and renamed actions. A redirect is not a guarantee. If an action you depend on has been archived or transferred, that is a decision for you to make, not a thing to discover during an incident.
  5. If you are on GitHub Enterprise Cloud, trial the lockfile on one workflow. The gh-actions-lock extension is public and MIT-licensed, version 0.2.0 landed on 12 October, and onboarding is per workflow by design. Start with the workflow that holds your deployment credentials.

Method and caveats

Sample: the 300 most-starred non-archived repositories on GitHub as of 14 October 2026, all above 67,000 stars. 246 have a .github/workflows directory. We parsed uses: lines with a regular expression rather than a YAML parser so that malformed and templated workflows still counted; this is why 138 references resolving to ${{ github.repository }}/.github/actions/... were reclassified as local rather than external. Action definitions were fetched at the exact referenced ref, so a composite action’s children reflect that pinned version and not its current main. Four definitions could not be retrieved and are excluded.

The sample is deliberately biased towards large, well-resourced projects. If anything, that flatters the ecosystem: these are the repositories most likely to have a security team. Figures for commit age are calculated against 14 October 2026 and will drift. Everything here is reproducible from the public GitHub REST API with a standard token.

Where REPTILEHAUS comes in

Most of the CI pipelines we inherit from clients have never had their dependency graph read out loud. They were assembled by three different people over four years, each of whom copied a snippet that worked. That is not negligence, it is how build systems accrete, and it is precisely why a measurement beats an opinion.

Our DevOps work starts with exactly the audit above: resolve every reference, read one level down, find the composite actions that nobody knew were there, then decide what to pin and what to automate. We build the pipelines too, but the audit is usually the part that changes somebody’s mind. If your workflows have deployment credentials in them and nobody can tell you which commits ran last Tuesday, get in touch.

📷 Photo by JJ Ying on Unsplash