GitHub Actions Workflow Lockfile: Pin Transitive Deps
GitHub just made workflow lockfiles generally available, and the gh actions-lock command finally answers a question that SHA pinning never could: what about the actions your actions call? A GitHub Actions workflow lockfi
GitHub just made workflow lockfiles generally available, and the gh actions-lock command finally answers a question that SHA pinning never could: what about the actions your actions call? A GitHub Actions workflow lockfile records exact commits for actions and their transitive dependencies, while your YAML keeps its readable @v4 style references. That's the release. The harder part is rolling it out without freezing CI, and knowing what it still doesn't prove.
TL;DR
- A lockfile fixes which commit runs, including the nested actions you never see in your YAML. It's a real upgrade over hand pinning the top level only.
- Roll it out one workflow at a time, and add a guard that fails a PR when a
uses:line changes without the lockfile changing. - It says nothing about how an action was built or whether your repo is configured safely. SLSA provenance and OpenSSF Scorecard cover those, and you still need both questions answered.
Dependency risk in workflow YAML
Here's the workflow most of us have in a repo right now:
# .github/workflows/ci.yml
name: ci
on: [pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: your-org/setup-toolchain@v2
- run: npm test
Two uses: lines, and they look harmless. But @v2 is a Git tag, and a tag is a pointer that someone with write access can move. If setup-toolchain is a composite action, it has its own uses: lines inside its action.yml, and those have their own tags. You reviewed two lines. You're running a tree.
The old fix was replacing every tag with a 40 character SHA. It works for the top level, it makes the YAML unreadable, and it doesn't reach the nested layer unless the action's author pinned too. You can't pin someone else's action.yml from your repo. That gap is the footgun, and it's the specific thing the lockfile closes.
This matters more once your pipelines do real work on their own. If you're moving toward production AI agent workflows, CI is where those agents get tokens, and an action you never reviewed runs with the same job permissions as the ones you did.
gh actions-lock migration
Before you generate anything, find out what you have. This script needs only the Python standard library. It reads your workflows and any local composite actions, then sorts each reference by how it's pinned:
#!/usr/bin/env python3
# audit_uses.py: run from the repo root
import collections
import pathlib
import re
USES = re.compile(r"""^\s*(?:-\s*)?uses:\s*['"]?([^'"\s#]+)""")
SHA = re.compile(r"^[0-9a-f]{40}$")
def classify(ref: str) -> str:
if ref.startswith("./"):
return "local"
if ref.startswith("docker://"):
return "docker"
if "@" not in ref:
return "unknown"
return "sha" if SHA.match(ref.rsplit("@", 1)[1]) else "mutable"
root = pathlib.Path(".github")
files = [p for p in root.rglob("*") if p.suffix in {".yml", ".yaml"}]
found = collections.defaultdict(set)
for path in files:
for line in path.read_text(encoding="utf-8").splitlines():
match = USES.match(line)
if match:
found[classify(match[1])].add(f"{match[1]} ({path})")
for kind in sorted(found):
print(f"\n{kind}: {len(found[kind])}")
for item in sorted(found[kind]):
print(f" {item}")
The mutable bucket is your migration list. Now resist doing the whole org in one PR. Here's the order I'd use:
- Audit with the script above and save the output, so you've got a before picture.
-
Lock one workflow that's low stakes but runs often. A docs build is ideal. Run
gh actions-lock, commit what it generates, and read the result before you merge. I'm leaving flags out on purpose: the changelog entry andgh actions-lock --helpare the source of truth for a tool this new. - Review the transitive entries. This first read is where you'll learn what your actions actually depend on.
- Expand to the workflows that hold deploy credentials once a few normal PRs have passed through.
Transitive pins and review process
A lockfile only helps if it can't quietly drift from the YAML. So add a guard. This job fails a PR when a uses: line changes and the lockfile doesn't. Set ACTIONS_LOCKFILE_PATH as a repo variable pointing at the file your generator wrote:
# .github/workflows/lockfile-guard.yml
name: lockfile-guard
on:
pull_request:
paths:
- ".github/**"
permissions:
contents: read
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require a lockfile update when uses lines change
env:
LOCKFILE: ${{ vars.ACTIONS_LOCKFILE_PATH }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if git diff "$BASE_SHA"...HEAD -- .github | grep -E '^[-+][^-+].*uses:'; then
if git diff --quiet "$BASE_SHA"...HEAD -- "$LOCKFILE"; then
echo "uses lines changed but $LOCKFILE did not. Regenerate it."
exit 1
fi
fi
Then decide how humans read that diff, because a bump to one action can change many transitive commits at once. My opinion: treat a lockfile change like a package-lock.json change, with one difference. A new top level action needs a named reviewer. A new transitive source gets the same scrutiny, because it runs with the same permissions, even though nobody chose it on purpose.
Three questions I'd put in the PR template for any lockfile diff:
- Which owners are new in this diff, at any depth?
- Does this bump change anything inside a job that holds a write token or a deploy secret?
- Does the release behind the new commit exist, and does it match what the tag claims?
The trap on the other side is letting nobody update the lockfile because review feels expensive. Stale pins age badly too. Schedule the bumps so they arrive in small diffs someone will actually read. If you're running this kind of change across a whole team's process, the design questions look a lot like the ones in workflow design for AI automation: who owns the step, what's the failure path, and who gets paged.
What SLSA provenance does separately
A pin tells you the bytes are the same bytes you saw last time. It doesn't tell you those bytes were any good, or where they came from. Don't let a green lockfile check stand in for those answers.
SLSA is the framework for the build question. Its v1.1 spec defines Build L1 as provenance existing, Build L2 as adding hosted build requirements, and Build L3 as hardened builds. One housekeeping note: the v1.1 page is now marked retired in favor of v1.2, so check the current version before you write it into a policy.
OpenSSF Scorecard covers a third area: repository hygiene. It documents independent checks such as dangerous workflows, token permissions and pinned dependencies, with remediation advice. It's a quick way to see whether the lockfile is sitting inside a repo that's otherwise wide open:
scorecard --repo=github.com/your-org/your-repo \
--checks=Pinned-Dependencies,Token-Permissions,Dangerous-Workflow
| Control | Question it answers | What it won't tell you |
|---|---|---|
| Workflow lockfile | Which exact commit runs, at every depth? | Whether that commit is safe |
| SLSA provenance | How and where was this built? | Whether your workflow picked the right artifact |
| OpenSSF Scorecard | Is this repo configured with good hygiene? | Whether a specific run was tampered with |
Reach for all three when a workflow can publish, deploy or spend money. For an internal docs build, the lockfile plus tight permissions is enough.
CI rollout checklist
- [ ] Run the audit and save the
mutablelist - [ ] Lock one low stakes workflow and read the output
- [ ] Add the guard job on
pull_request - [ ] Name a reviewer for new transitive owners
- [ ] Set
permissionsexplicitly on every workflow - [ ] Schedule small, regular lockfile bumps
FAQ
How does a GitHub Actions workflow lockfile work?
gh actions-lock records the exact commits for your actions and their transitive dependencies. Your workflow keeps readable references, while the recorded commits say what's actually meant to run.
What changes for teams adopting a GitHub Actions workflow lockfile?
The review surface moves. You gain a diff that shows nested dependency changes you couldn't see before, so someone needs to own reading it, and CI should stop a uses: change that arrives without a matching lockfile update.
Does a lockfile replace SHA pinning?
For the goal of fixing what runs, that's the promise, and you keep readable tags in the YAML. Test it on one workflow and confirm the behavior yourself before you strip existing pins from anything that deploys.
Does it replace SLSA or Scorecard?
No. They answer different questions, as the table above shows.
Related reading: why AI code review leaderboards disagree and Next.js security hardening patterns that hold up.
If you want agent pipelines and CI wired up so this kind of control is the default, that's the work I do in agentic AI consulting.
What's in your mutable bucket? I'm curious how deep the transitive layer goes in real repos, so drop your count in the comments.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.


