ci: pin third-party GitHub Actions to full commit SHAs#3776
Conversation
Pin every external action reference in .github/workflows to its full 40-character commit SHA, keeping the human-readable version as a trailing comment (the OpenSSF-recommended form): uses: actions/checkout@<sha> # v6 A mutable tag like @v6 can be force-moved to point at different code after review, so a compromised or hijacked action tag can ship arbitrary code into CI. A pinned commit SHA is immutable and removes that class of risk. Each SHA was resolved from the exact commit the current tag points to, so behavior is unchanged. Dependabot continues to update these because the version label is preserved in the trailing comment. Local reusable action references (./test-infra/...) and commented-out steps are left as-is. No permissions or other workflow changes are included. Signed-off-by: arpitjain099 <arpitjain099@gmail.com>
|
@arpitjain099 how did you produced all the sha values? |
There was a problem hiding this comment.
Pull request overview
This PR hardens the repository’s GitHub Actions workflows by replacing mutable third-party uses: references (tags/branches) with immutable 40-character commit SHA pins, keeping the prior version as a trailing comment for auditability and automated updates.
Changes:
- Pinned
actions/checkout,astral-sh/setup-uv,codecov/codecov-action,nick-fields/retry, and other third-party actions to full commit SHAs across CI/test/release workflows. - Pinned automation workflows (triage + Discord notifications) action references to SHAs.
- Pinned release and Docker workflows’ third-party action references to SHAs.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| .github/workflows/unit-tests.yml | Pins checkout/uv/retry/codecov and MNIST download action to SHAs for unit-test CI hardening. |
| .github/workflows/typing-checks.yml | Pins checkout/uv to SHAs for typing workflow supply-chain hardening. |
| .github/workflows/triage.yml | Pins checkout and labeler actions to SHAs for triage workflow hardening. |
| .github/workflows/tpu-tests.yml | Pins checkout/setup-python/cache/retry/codecov and MNIST download action to SHAs. |
| .github/workflows/stable-release-pypi.yml | Pins checkout and setup-uv to SHAs for PyPI release workflow hardening. |
| .github/workflows/stable-release-anaconda.yml | Pins checkout and setup-miniconda to SHAs for conda release workflow hardening. |
| .github/workflows/pytorch-version-tests.yml | Pins checkout/uv/retry and MNIST download action to SHAs for scheduled version-test workflow hardening. |
| .github/workflows/mps-tests.yml | Pins checkout/codecov and MNIST download action to SHAs for macOS MPS workflow hardening. |
| .github/workflows/hvd-tests.yml | Pins checkout/uv/retry/codecov and MNIST download action to SHAs for Horovod workflow hardening. |
| .github/workflows/gpu-tests.yml | Pins checkout/retry/codecov to SHAs for GPU workflow hardening. |
| .github/workflows/gpu-hvd-tests.yml | Pins checkout/codecov to SHAs for GPU+Horovod workflow hardening. |
| .github/workflows/docs.yml | Pins checkout/uv/retry and gh-pages deploy action to SHAs for docs workflows hardening. |
| .github/workflows/docker-build.yml | Pins checkout and changed-files action to SHAs for Docker workflow hardening (local reusable actions unchanged). |
| .github/workflows/discord_pull_requests.yaml | Pins discuss-on-discord action to a SHA for Discord PR notifications hardening. |
| .github/workflows/discord_issues.yml | Pins discuss-on-discord action to a SHA for Discord issue notifications hardening. |
| .github/workflows/code-style-checks.yml | Pins checkout/uv to SHAs for style-check workflow hardening. |
| .github/workflows/binaries-nightly-release.yml | Pins checkout/setup-miniconda/create-an-issue actions to SHAs for nightly release workflow hardening. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@vfdev-5 Resolved each one against the tag the workflow already uses, via the GitHub API: |
vfdev-5
left a comment
There was a problem hiding this comment.
@arpitjain099 you can annotate all the sha like in the comments to ensure that we point out to correct tags, this would help with the review. Thanks!
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 |
There was a problem hiding this comment.
https://github.com/actions/checkout/commit/df4cb1c069e1874edd31b4311f1884172cec0e10
|
|
||
| - name: Setup Miniconda | ||
| uses: conda-incubator/setup-miniconda@v4 | ||
| uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4 |
There was a problem hiding this comment.
|
Thanks @vfdev-5. Each ref already has the tag it was resolved from as a trailing comment, e.g. If it helps the review, I'm happy to change those comments to the exact release tag instead of the floating major, e.g. |
Update the trailing comment on each SHA-pinned third-party action from the floating major tag (e.g. # v6) to the exact immutable release tag whose commit equals that SHA (e.g. # v6.0.3). This lets reviewers verify each pin maps to a real release by checking that <action>@<tag> resolves to the pinned commit. Verified every SHA against the upstream tag list via the GitHub tags API. pytorch-ignite/download-mnist-github-action stays # master since that repo has no release tags and is pinned to the master branch tip. Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
|
Done, thanks for the nudge. Updated every pin to the exact release tag its SHA resolves to (e.g. One to flag: |
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 |
There was a problem hiding this comment.
@arpitjain099 how can I check easily that this sha points to v6.0.3 tag?
There are tens of sha hashes, how can we ensure that they are not already pointing to some incorrect release?
|
Good question - here's the check, and I re-verified every pin in this PR. To verify any single pin, resolve its tag to a commit: The I re-ran that for all of them and every SHA matches its tag: actions/checkout v6.0.3, actions/setup-python v6.2.0, actions/labeler v6.1.0, actions/cache v5.0.5, astral-sh/setup-uv v7.6.0, codecov/codecov-action v6.0.1, conda-incubator/setup-miniconda v4.0.1, nick-fields/retry v4.0.0, peaceiris/actions-gh-pages v4.1.0, JasonEtco/create-an-issue v2.9.2, EndBug/discuss-on-discord v1.1.0, umani/changed-files v4.2.0. One note: For ongoing assurance rather than checking by hand, the usual setup is a tool that verifies/updates pins automatically: pinact, ratchet, or StepSecurity's pin-github-action; zizmor can also flag unpinned ones in CI. Dependabot understands the |
|
@vfdev-5 Code I used to verify shas: import subprocess
import sys
ACTIONS = [
("actions/checkout", "v6.0.3", "df4cb1c069e1874edd31b4311f1884172cec0e10"),
("conda-incubator/setup-miniconda", "v4.0.1", "8ee1f361103df19b6f8c8655fd3967a8ecb162d5"),
("JasonEtco/create-an-issue", "v2.9.2", "1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5"),
("astral-sh/setup-uv", "v7.6.0", "37802adc94f370d6bfd71619e3f0bf239e1f3b78"),
("EndBug/discuss-on-discord", "v1.1.0", "bff2908cbd855fff72c1007aed386e09144727e1"),
("umani/changed-files", "v4.2.0", "138acc60bcaa548e0c194fc69ed36321ee8466d2"),
("peaceiris/actions-gh-pages", "v4.1.0", "84c30a85c19949d7eee79c4ff27748b70285e453"),
("nick-fields/retry", "v4.0.0", "ad984534de44a9489a53aefd81eb77f87c70dc60"),
("codecov/codecov-action", "v6.0.1", "e79a6962e0d4c0c17b229090214935d2e33f8354"),
("actions/setup-python", "v6.2.0", "a309ff8b426b58ec0e2a45f0f869d46889d02405"),
("actions/cache", "v5.0.5", "27d5ce7f107fe9357f9df03efb73ab90386fccae"),
("actions/labeler", "v6.1.0", "f27b608878404679385c85cfa523b85ccb86e213"),
("pytorch-ignite/download-mnist-github-action", "master", "622fc8c4ff50b24322819f54f48624f26932892b"),
]
def verify_action(repo, ref, pinned_sha):
url = f"https://github.com/{repo}"
if ref == "master":
ref_path = "refs/heads/master"
ref_deref = None
else:
ref_path = f"refs/tags/{ref}"
ref_deref = f"refs/tags/{ref}^{{}}"
cmd = ["git", "ls-remote", url, ref_path]
if ref_deref:
cmd.append(ref_deref)
output = subprocess.check_output(cmd).decode("utf-8").strip()
lines = [line.split() for line in output.split("\n") if line.strip()]
resolved_sha = None
if len(lines) == 2:
resolved_sha = lines[1][0]
elif len(lines) == 1:
resolved_sha = lines[0][0]
if not resolved_sha:
return "ERROR", f"Could not resolve reference {ref}"
if resolved_sha == pinned_sha:
return "SAFE", resolved_sha
else:
return "MISMATCH", f"Remote is {resolved_sha}, PR has {pinned_sha}"
def main():
print(f"{'ACTION':<45} | {'TAG/BRANCH':<12} | {'STATUS':<8} | {'DETAILS'}")
print("-" * 110)
failed = False
for repo, ref, sha in ACTIONS:
status, detail = verify_action(repo, ref, sha)
print(f"{repo:<45} | {ref:<12} | {status:<8} | {detail}")
if status != "SAFE":
failed = True
if failed:
sys.exit(1)
else:
print("\nAll queried action SHAs have been successfully verified as safe!")
if __name__ == "__main__":
main() |
Pin third-party GitHub Actions to full commit SHAs
This follows up on #3754. As @vfdev-5 suggested there (#3754 (comment)), the higher-value hardening for these workflows is pinning the action references rather than touching permissions, so this PR does exactly that and nothing else.
Every external action
uses:reference in.github/workflows/that was on a tag or branch (actions/checkout@v6,astral-sh/setup-uv@v7,codecov/codecov-action@v6, thetriage.yml@v*refs, etc.) is now pinned to the full 40-character commit SHA, with the version kept as a trailing comment in the OpenSSF-recommended form:Why
A tag like
@v6is mutable: the action author (or someone who compromises their account) can force-move it to point at different code at any time, and your CI would pick up that code on the next run with no diff in this repo. That is the mechanism behind thetj-actions/changed-filesincident in March 2025, where a moved tag exposed secrets across thousands of repositories. A commit SHA is immutable, so pinning to it removes that class of supply-chain risk while still running the same code the tag points at today.Details
./test-infra/.github/actions/...) are intentionally left unpinned, since they resolve from the checked-out tree, not an external tag.permissions:blocks or any other workflow content were changed; this PR is only SHA pinning.Happy to adjust the comment style or split this per-file if you would prefer smaller diffs.