Over the last few tips, I've shared why we pinned our GitHub Actions to commit hashes, how we audit them, and how we keep them up to date.
The last piece of the puzzle is making this new practice something we can enforce, without relying on anyone remembering it as new actions are added.
First, there's a way to automate the conversion, which reduces a lot of friction. A tool called pinact makes this super easy. You can use it just once during the conversion, and it's not something that is added to your CI or automation.
It rewrites every uses: line in your workflows with the full hash and a version comment.
One thing to know is that it resolves a tag like v4 to the latest v4 release, so you may get a small version bump along with the pin.
Next, I added zizmor to CI, so a new unpinned action fails the build.
# .github/workflows/workflow-security.yml
on:
push:
paths:
- .github/**
schedule:
- cron: "0 13 * * 1"
permissions: {}
jobs:
zizmor:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
advanced-security: false
Just like with Dependabot, running this on a schedule is important. A new advisory can land against an action I haven't touched in months, and I want to hear about it without waiting for my next workflow change.
The advanced-security: false line is a requirement for private repositories where you aren't paying for the Advanced Security feature from GitHub.
I love tools like this that automate a tedious process and keep your team honest about pinning.
Here to help,
Joel
P.S. Not sure where your own project's security gaps are? Talk it through with us and we'll help you figure out what to tackle first.