Pin CI actions and pipeline steps to commit SHAs safely
For developers maintaining CI/CD pipelines that call third-party actions, templates, or remote scripts. This shows exactly how to replace floating tags with immutable commit SHAs, verify what changed, and catch the common breakages before they hit production.
TL;DR — Replace floating references like
@v4,:latest, or remote script URLs with immutable commit SHAs in your CI config. The single biggest fix is: resolve the exact upstream commit you already trust, update the pipeline to that full SHA, and verify the run still succeeds before merging. Reading time: ~5 min
Goal
When you finish, your CI/CD configuration will reference third-party actions, reusable workflows, templates, or fetched scripts by full commit SHA instead of mutable tags or branches, and a pipeline run will complete successfully using those pinned revisions.
Prerequisites
- Write access to the repository containing the CI/CD config
- Permission to trigger CI runs on a branch or pull request
- Git installed — check with:
git --version
- A shell with
grep,sed, andcurlavailable - If you use GitHub-hosted actions:
ghCLI installed and authenticated — check with:
gh --version
gh auth status
- The current CI config file paths at hand, typically one or more of:
.github/workflows/*.yml
.gitlab-ci.yml
.circleci/config.yml
azure-pipelines.yml
Jenkinsfile
- The upstream repo names for any third-party actions or templates you already use
Steps
Step 1: Find floating third-party references in your pipeline
Run these searches from the repository root:
grep -RInE 'uses: .+@([A-Za-z][A-Za-z0-9._/-]*|v[0-9]+(\.[0-9]+)*)$' .github/workflows . 2>/dev/null || true
grep -RInE '(curl|wget).*(raw|githubusercontent|gitlab|bitbucket)' .github/workflows .gitlab-ci.yml .circleci config* Jenkinsfile 2>/dev/null || true
grep -RInE 'image: .+:(latest|main|master|stable|edge)$' .gitlab-ci.yml .circleci/config.yml docker-compose.yml 2>/dev/null || true
You should see file paths and line numbers for references like uses: owner/action@v4, curl https://.../install.sh, or image: vendor/tool:latest.
Step 2: Resolve each action tag or branch to a full commit SHA
For GitHub Actions, resolve the currently used ref to a commit SHA before changing anything:
gh api repos/actions/checkout/commits/v4 --jq '.sha'
gh api repos/docker/login-action/commits/v3 --jq '.sha'
Example output shape:
0ad4b8fadaa221de15dcec353f45205ec38ea70b
65b78e6e13532edd9afa3aa52ac7964289d1a9c1
If you do not use gh, use the GitHub API directly:
curl -fsSL https://api.github.com/repos/actions/checkout/commits/v4 | sed -n 's/ "sha": "\([a-f0-9]\{40\}\)",/\1/p' | head -n1
You should get a 40-character hexadecimal SHA. If you get 404 Not Found, the repo or ref is wrong.
Step 3: Replace floating refs with full SHAs in CI config
Edit the workflow and replace the tag with the resolved SHA. Example GitHub Actions change:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b
- uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
If you use reusable workflows, pin those too:
jobs:
deploy:
uses: org/shared-workflows/.github/workflows/deploy.yml@8f4b7f848e6e2c6f2d4d4f3d9a7e8f1b2c3d4e5f
If you fetch a remote script, stop fetching a moving target. Replace a branch URL with a commit URL:
curl -fsSLo /tmp/install.sh https://raw.githubusercontent.com/vendor/tool/3a1f6b0c2b4d8e9f0a1b2c3d4e5f678901234567/install.sh
bash /tmp/install.sh
You should see the config contain full 40-character SHAs and no @main, @master, @v4, or latest for third-party dependencies.
⚠️ Pinning to a bad commit can break builds immediately. Commit the changes on a branch and run CI there before merging to the default branch.
Step 4: Verify the pinned commit matches the upstream content you expect
For each changed action, inspect the upstream commit metadata:
gh api repos/actions/checkout/commits/0ad4b8fadaa221de15dcec353f45205ec38ea70b --jq '{sha: .sha, date: .commit.author.date, message: .commit.message}'
Example output shape:
{"date":"2026-02-14T18:22:11Z","message":"Update checkout internals","sha":"0ad4b8fadaa221de15dcec353f45205ec38ea70b"}
If the action publishes release notes, compare the SHA against the release tag in the upstream repo before trusting it.
Step 5: Commit the pinning change and trigger a pipeline run
Create a branch, commit, and push:
git checkout -b chore/pin-ci-dependencies
git add .github/workflows .gitlab-ci.yml .circleci/config.yml azure-pipelines.yml Jenkinsfile 2>/dev/null || true
git commit -m "Pin third-party CI actions and pipeline steps to commit SHAs"
git push -u origin chore/pin-ci-dependencies
You should see normal Git output ending with your branch name on origin.
Step 6: Fail the build if new floating refs are introduced
Add a guard job or script. For a repository using GitHub Actions, create .github/workflows/enforce-pinning.yml:
name: enforce-pinning
on:
pull_request:
push:
branches:
- main
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b
- name: Reject floating action refs
run: |
set -euo pipefail
if grep -RInE 'uses: .+@([A-Za-z][A-Za-z0-9._/-]*|v[0-9]+(\.[0-9]+)*)$' .github/workflows; then
echo 'Found non-SHA action reference' >&2
exit 1
fi
- name: Reject remote scripts from moving refs
run: |
set -euo pipefail
if grep -RInE '(raw.githubusercontent.com/.+/(main|master)/|@latest|:latest)' .github/workflows; then
echo 'Found moving third-party dependency reference' >&2
exit 1
fi
You should see a separate CI job named enforce-pinning; it passes when no floating refs remain and exits with code 1 when one is introduced.
Verify it works
Run these checks locally:
grep -RInE 'uses: .+@[a-f0-9]{40}$' .github/workflows
grep -RInE 'uses: .+@(main|master|v[0-9]+|latest)$' .github/workflows && echo "unexpected floating refs found"
Expected result: the first command prints your pinned action lines; the second prints nothing.
If you use remote scripts, verify the URL contains a commit SHA and not a branch:
grep -RInE 'raw.githubusercontent.com/.+/[a-f0-9]{40}/' .github/workflows .gitlab-ci.yml .circleci/config.yml 2>/dev/null
Expected result: matching lines include a 40-character SHA in the path.
Then verify the CI run end-to-end in your provider UI: the branch or pull request pipeline turns green using the updated config. If a job fails due to a bad pin, the error usually looks like one of these:
Error: Unable to resolve action `owner/action@v4`, unable to find version `v4`
fatal: reference is not a tree: 0ad4b8fadaa221de15dcec353f45205ec38ea70b
curl: (22) The requested URL returned error: 404
Common pitfalls
Using a short SHA instead of the full 40-character SHA
Mistake: pinning @0ad4b8f because it looks cleaner.
Symptom: policy checks or security scanners still flag the workflow, or the CI platform rejects the ref format.
Fix: replace it with the full SHA, for example @0ad4b8fadaa221de15dcec353f45205ec38ea70b.
Pinning the wrong repository or wrong ref
Mistake: resolving v4 from a similarly named repo or from a fork.
Symptom: the workflow fails at runtime with missing inputs, missing files, or unexpected behavior after a seemingly valid pin.
Fix: run gh api repos/OWNER/REPO/commits/REF --jq '.sha' against the exact upstream repo already referenced in your config.
Leaving reusable workflows or remote scripts unpinned
Mistake: pinning uses: action steps but missing jobs.<job>.uses: or curl https://.../main/install.sh.
Symptom: security review still fails, or your build remains vulnerable to upstream changes even though action steps are pinned.
Fix: search for all of uses:, jobs:.*uses:, curl, wget, and :latest, then replace each moving ref with a commit SHA or immutable digest.
Confusing commit SHA pinning with container image pinning
Mistake: changing action refs to SHAs but leaving image: vendor/tool:latest in the same pipeline.
Symptom: builds still change unexpectedly between runs.
Fix: replace mutable image tags with immutable digests, for example image: vendor/tool@sha256:...; commit SHAs do not secure container pulls.
Trusting a SHA without checking what tag it came from
Mistake: pinning whatever commit the API returns for main today.
Symptom: the build works, but you pinned an unreviewed upstream change instead of the release you intended.
Fix: resolve the SHA from the release tag you already trust, such as v4, then inspect the commit metadata before committing the change.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI