GitHub Actions lockfiles pin the commit and leave your tags alone
Tomás Vega
You reviewed the action. You approved the pull request. Then someone moved the tag, and your next run executed code nobody on your team has ever read. (Tags are sticky notes. Nobody ever promised they were signatures.) GitHub's answer is workflow dependency lockfiles, now in public preview. You keep @v1 in your YAML, and the run uses the commit you actually reviewed.
What shipped
The preview covers GitHub Enterprise Cloud, Enterprise Cloud with data residency, Team and Free. GitHub Enterprise Server is not on that list.
The work happens in a GitHub CLI extension, gh actions-lock. It generates .github/workflows/actions.lock, which records reviewable commits for each action, for that action's transitive dependencies, and for reusable workflow dependencies. You onboard workflows one at a time. Once a workflow is onboarded, every one of its dependencies must have a lockfile entry, and Actions runs the recorded commit even if the tag or branch has since moved. The extension can also check whether the lockfile is stale without modifying anything, and GitHub says that check can run in CI.
Two pieces sit around it. A Require lockfile policy, also in preview, lets enterprise, organization and repository administrators make lockfiles mandatory within their scope. Dependabot, when it opens a version update for an action in an onboarded workflow, regenerates the matching lockfile entry in the same pull request.
Couldn't I just SHA-pin everything?
You could. Plenty of us do. But anyone who has maintained it knows what it costs: 40 characters of hex on every uses: line, a # v1 comment that quietly drifts out of sync with the SHA, and diffs nobody reads. Worse, your pin covers one edge of the graph. A composite action you pinned by SHA can still call another action by a floating tag, and you never see that line because it lives in someone else's repository.
The lockfile closes both gaps. The readable reference stays in the workflow, the commit moves into a file you can diff, and the transitive dependencies get locked too. Same trick package-lock.json and go.sum pulled years ago, finally applied to the layer of the stack that holds your deploy credentials.
Rolling it out
Install the extension from the public github/gh-actions-lock repository:
gh extension install github/gh-actions-lock
Then generate the lockfile, review it, and commit it together with the workflow changes the tool produces. Start with the workflows that hold secrets or deploy permissions. Add the stale-lockfile check to CI so a drifted lockfile fails a pull request. Turn on Require lockfile only after the important workflows are onboarded, unless you enjoy a red Monday. The exact subcommands are in GitHub's workflow dependency lockfiles documentation, and I am not going to guess them for you.
What a lockfile will not do
GitHub says this plainly in the changelog: locking "does not determine whether the dependency itself is safe, so review dependencies before locking them." Lock a backdoored commit and you get a backdoored workflow that runs exactly the same way every time. Reproducible. Still compromised.
The Dependabot integration deserves the same suspicion. When a bot PR updates both the reference and the locked commit, that PR becomes your trust boundary. Rubber-stamp it and you have automated the tag move with extra steps.
Then there is scope. The changelog describes locking commits for actions, their transitive dependencies and reusable workflows. It says nothing about container images or binaries an action downloads at runtime, so assume those are still yours to pin by digest ($REGISTRY/image@sha256:<digest>). And until the policy is on, any workflow you have not onboarded still floats.
How other CI systems handle the same problem
Every CI system has this problem. Here is how pinning looks in three of them.
- GitLab CI/CD lets you pin
include:references and CI/CD components to a commit SHA. That is pinning per reference: you write each SHA yourself, one line at a time. - CircleCI orbs are referenced by version. A full semantic version points at a published release, while a major-only reference such as
@1picks up newer releases automatically. - Jenkins shared libraries load via
@Library('name@ref'), where the ref can be a branch, a tag or a commit. Same model: safe if you choose a commit, floating if you choose a branch.
What GitHub adds is transitive coverage and an admin policy to enforce it, at least on paper. It is still a preview, built with community contributors that GitHub credits by name, including Andrew Nesbitt and William Woodruff, so expect rough edges.
Verdict
Adopt it, starting with your most privileged workflows, and wire up the stale check on day one. This is the pinning model Actions should have shipped with: readable references, locked commits, transitive coverage. Just don't confuse a lockfile with a review. It records what you decided to trust. It has no opinion on whether you were right.
Source: GitHub Changelog (github.blog)