Repository navigation
RFC: gh actions pin #13314
Description
gh actions pin — Actions dependency pinning CLI
Covering: Onboarding workflows to SHA-pinning with inline lockfile, day-to-day upgrades via edit-and-repin, change detection when upstream refs move, CI integration with JSON output and exit codes
Note
We're using an extension to prototype, we want to land these commands under gh actions sub-command.
Link to issue
Public:
- https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/
- https://github.com/orgs/community/discussions/190621
Internal:
- https://github.com/github/c2c-actions/pull/10106
- https://github.com/github/actions-dispatch/issues/535
Problem
SHA-pinning Actions dependencies is an established best practice. However, we don't yet offer a good experience to manage pinned dependencies at scale.
- Manual pinning is tedious: look up the SHA for each action, replace the ref, remember which tag it corresponded to.
- SHA-only refs are unreadable: once pinned, you can't tell what version you're on without cross-referencing GitHub. SHAs are not human readable.
- Upgrades are blind: Dependabot can bump SHAs but can't tell you what version changed or show meaningful diffs
- No change detection: if a tag moves (routine patch or otherwise), existing SHA pins silently diverge from the ref with no signal to the developer.
Proposed design
gh actions-pin is a gh CLI extension that manages a lockfile (dependencies: top-level YAML key) appended to each workflow file. The lockfile records the mapping between action refs and their resolved commit SHAs, preserving version context and enabling readable upgrades, change detection, and Dependabot compatibility. It will be used as the core for various developer tooling integrations (commit hooks, linting actions) and as a reference implementation for other scenarios, such as updating pinned dependencies when editing a workflow on github.com.
| Command | Description |
|---|---|
gh actions-pin [files...] |
Verify & fix — bare command defaults to check |
gh actions-pin upgrade [files...] |
Upgrade action versions and re-lock dependencies |
Lockfile dialect: The exact syntax of the
dependencies:block may evolve as we gather and integrate feedback on the approach. The CLI's UX patterns — verify, fix interactively, auto-fix in CI, detect changes — are designed to be stable regardless of the underlying lockfile format.
gh actions-pin [check] --help
Verify that every action dependency in your workflows is locked to
an immutable commit SHA and that the lock is still valid.
Scans all workflows under .github/workflows/ by default, or pass
specific paths. Checks both direct and transitive dependencies
(composite actions that reference other actions).
When run interactively (TTY), offers to fix issues inline.
Non-interactive mode (--no-interactive or CI) auto-fixes
deterministic issues and exits non-zero if anything remains
that requires human judgment.
With --json, structured results go to stdout and progress to stderr:
gh actions-pin check --json 2>/dev/null | jq .valid
Issue types:
REF_MOVED - locked SHA no longer matches upstream (expected for mutable tags like v4)
MISSING - action in workflow has no lock entry
STALE - lock entry references an action no longer in the workflow
REF_CHANGED - workflow ref was edited; lock needs updating
MISLEADING_SHA - ref looks like a SHA but resolves to a different commit
IMPOSTER_COMMIT - locked SHA is not in the ref's history
Usage:
actions-pin check [<workflow-path>...] [flags]
Examples:
# Verify all workflows
$ gh actions-pin check
# Report-only mode (no prompts, no changes)
$ gh actions-pin check --no-interactive
# JSON output for CI
$ gh actions-pin check --json=valid,findings
Flags:
--hostname string GitHub hostname (defaults to GH_HOST or github.com)
--json fields Output JSON (valid,findings,workflows,dependencies)
--no-interactive Auto-fix deterministic issues; fail on ambiguous ones
gh actions-pin upgrade --help
Upgrade action refs to newer versions and re-lock all direct and
transitive dependencies with their new commit SHAs.
Pass --action to target specific actions. Each action is upgraded to
its latest stable tag by default. Use --version to target a specific
version, or specify it inline as owner/repo@ref. Use --from to limit
upgrades to actions currently on a specific ref.
Changes are applied by default. Pass --write=false to preview only.
Usage:
actions-pin upgrade [<workflow-path>...] [flags]
Examples:
# Upgrade checkout to the latest stable tag
$ gh actions-pin upgrade --action actions/checkout
# Upgrade to a specific version
$ gh actions-pin upgrade --action actions/checkout --version v5
# Upgrade only checkout refs currently on v5 to v6
$ gh actions-pin upgrade --action actions/checkout --from v5 --version v6
# Use inline target refs for mixed upgrades
$ gh actions-pin upgrade --action actions/checkout@v6 --action actions/setup-go@v6
# Preview without writing
$ gh actions-pin upgrade --action actions/checkout --write=false
Flags:
--action action Upgrade the specified action (owner/repo or owner/repo/path).
Append @ref to target a specific version.
--diff Show the full dependency diff vs existing pins (default true)
--from ref Only upgrade actions currently on this ref
--hostname string GitHub hostname (defaults to GH_HOST or github.com)
--json fields[="updated"] Output JSON with the specified fields (updated)
--version version Target version/ref for all selected actions;
defaults to the latest stable tag
--write Write the upgraded refs and dependencies back
to the workflow file
How the lockfile works
The lockfile is a dependencies: top-level YAML key appended to the bottom of each workflow file:
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
dependencies:
- actions/checkout@v4:sha1-abc1234567890abcdef1234567890abcdef123456
- actions/setup-node@v4:sha1-def4567890abcdef1234567890abcdef123456789
# Transitive: pulled in by actions/setup-node composite
- actions/cache@v4:sha1-789abcdef1234567890abcdef1234567890abcdef1Try it yourself
Every scenario below uses checked-in fixtures in demo/ and can be reproduced locally:
gh extension install github/gh-actions-pin # or: go install from source
demo/try-it.sh # list scenarios
demo/try-it.sh check-autofix # run one
demo/try-it.sh all # run all non-interactive scenariosFixtures are reset to clean state before each run. Interactive scenarios (sha-as-ref, unreleased-commit) present real prompts. Install the extension: gh extension install github/gh-actions-pin and checkout the github/gh-actions-pin repo to get the fixtures used in this demo.
Table of Contents
gh actions-pin [check]
| Category | Scenario |
|---|---|
| Default | Pin and verify |
| Day-to-day | Edit and re-pin |
| Change detection | Ref moved — routine update |
| Change detection | Imposter commit — fork injection detected |
| Interactive | Bare SHA without tag ref |
| Interactive | Unreleased commit |
| CI / Automation | CI / lint mode |
| CI / Automation | JSON output |
gh actions-pin upgrade
| Category | Scenario |
|---|---|
| Upgrade | Upgrade to latest |
| Upgrade | Upgrade to specific version |
try-it.sh scenarios
| try-it.sh scenario | Section | Interactive? |
|---|---|---|
check-autofix |
Pin and verify | No |
ci-failure |
CI / lint mode | No |
sha-as-ref |
Bare SHA | Yes |
unreleased-commit |
Unreleased commit | Yes |
upgrade-latest |
Upgrade to latest | No |
upgrade-version |
Upgrade to specific version | No |
edit-repin |
Edit and re-pin | No |
ref-moved |
Ref moved | No |
imposter-commit |
Imposter commit | No |
json-output |
JSON output | No |
gh actions-pin [check]
Scenarios
- Default: Bare command validates pins & auto-fixes deterministic cases (tag-based refs)
- Day-to-day: Edit-and-repin flow for Dependabot or manual version bumps
- Change detection: Detect when upstream tags move — distinguish routine updates from fork-injection attacks
- Interactive: Tag picker for bare-SHA refs and unreleased commits
- CI / Automation: Lint mode (
--write=false) for CI gates; JSON output with structured findings and exit codes
Pin and verify (default)
The most common entry point. Run the bare command on any workflow - it validates existing pins and auto-fixes what it can. When every action uses a tag ref or the pinned sha can be resolved to a tag, pinning is fully deterministic. No flags needed.
Input:
$ gh actions-pin demo/workflows-check/ci.ymlOutput:
Full terminal session
$ echo "# Unpinned workflow — 3 actions using mutable tags"
# Unpinned workflow — 3 actions using mutable tags
$ grep uses: demo/workflows-check/ci.yml
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v6
$ echo "# Pin all actions (bare command auto-fix)"
# Pin all actions (bare command auto-fix)
$ gh actions-pin demo/workflows-check/ci.yml
✗ 1 of 1 workflow failed: 3 not_pinned
! 1 workflow not yet pinned — resolving below
demo/workflows-check/ci.yml
! 3 actions found but not pinned
v4 → v4.3.1 (pinning to patch version)
v5 → v5.6.0 (pinning to patch version)
v6 → v6.5.2 (pinning to patch version)
✓ Pinned 3 dependencies in demo/workflows-check/ci.yml
✓ 1 issue fixed
$ echo "# Lockfile written — check the result"
# Lockfile written — check the result
$ tail -6 demo/workflows-check/ci.yml
# Automatically generated and managed by gh-actions-pin
dependencies:
- github.com/actions/checkout@v4.3.1:sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
- github.com/actions/setup-go@v5.6.0:sha1-40f1582b2485089dde7abd97c1529aa768e1baff
- github.com/golangci/golangci-lint-action@v6.5.2:sha1-55c2c1448f86e01eaae002a5a3a9624417608d84
$ gh actions-pin check demo/workflows-check/ci.yml
✓ All 1 workflow valid
Expected effects:
dependencies:block appended to workflow file with SHAs and hash algorithm prefixes- Subsequent
checkpasses cleanly (exit 0) try-it.sh check-autofixreproduces this scenario
Edit and re-pin
A workflow author edits the uses: ref, then check regenerates the lockfile.
Input:
$ sed -i '' 's/checkout@v4.2.0/checkout@v5/g' workflow.yml # Dependabot bumps the ref
$ gh actions-pin check --no-interactive workflow.yml # Re-pinOutput:
Full terminal session
$ grep checkout demo/workflows-upgrade/ci.yml
- uses: actions/checkout@v4.2.0
- uses: actions/checkout@v4.2.0
- github.com/actions/checkout@v4.2.0:sha1-d631b412a1b64f6193a3f0e66939e2ee43570fdc
$ echo "# Edit the ref"
# Edit the ref
$ sed -i '' 's/checkout@v4.2.0/checkout@v5/g' demo/workflows-upgrade/ci.yml
$ echo "# Remove stale lockfile so check can re-resolve"
# Remove stale lockfile so check can re-resolve
$ sed -i '' '/# Automatically generated/,$d' demo/workflows-upgrade/ci.yml
$ echo "# Re-pin with check"
# Re-pin with check
$ gh actions-pin check --no-interactive demo/workflows-upgrade/ci.yml
✗ 1 of 1 workflow failed: 3 not_pinned
! 1 workflow not yet pinned — resolving below
demo/workflows-upgrade/ci.yml
! 3 actions found but not pinned
v5 → v5.0.1 (pinning to patch version)
✓ Pinned 3 dependencies in demo/workflows-upgrade/ci.yml
✓ 1 issue fixed
$ echo "# New lockfile written"
# New lockfile written
$ tail -5 demo/workflows-upgrade/ci.yml
# Automatically generated and managed by gh-actions-pin
dependencies:
- github.com/actions/checkout@v5.0.1:sha1-93cb6efe18208431cddfb8368fd83d5badbf9bfd
- github.com/actions/setup-go@v5.3.0:sha1-f111f3307d8850f501ac008e886eec1fd1932a34
- github.com/golangci/golangci-lint-action@v6.3.0:sha1-e60da84bfae8c7920a47be973d75e15710aa8bd7
Expected effects:
checkre-resolves all actions from their new refs- New
dependencies:block written with updated SHAs try-it.sh edit-repinreproduces this scenario
Ref moved — routine update
The lockfile records the SHA resolved at pin time. When a tag moves forward (e.g. a maintainer patches a release), check detects the mismatch and provides a compare link so you can review what changed before accepting.
Input:
$ gh actions-pin check demo/workflows-pwned/5-pinned-before-update.ymlOutput:
Full terminal session
$ echo "# Tag moved forward — routine upstream release"
# Tag moved forward — routine upstream release
$ grep -E "uses:| -" demo/workflows-pwned/5-pinned-before-update.yml
- uses: actions/checkout@v4
- uses: nodeselector/actions-test-fixtures/nested-composite@updated
- github.com/actions/checkout@v4:sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
- github.com/nodeselector/actions-test-fixtures/nested-composite@updated:sha1-38b3412adcb7afb4a061c519513e45cbaf4a1cec
$ echo "# Check detects the tag now points to a newer commit"
# Check detects the tag now points to a newer commit
$ gh actions-pin check --no-interactive demo/workflows-pwned/5-pinned-before-update.yml
! REF_MOVED nodeselector/actions-test-fixtures/nested-composite@updated
pinned 38b3412adcb7 but ref now resolves to ea53476fdc17
Validate the change is a legitimate update before accepting.
→ https://github.com/nodeselector/actions-test-fixtures/compare/38b3412adcb7afb4a061c519513e45cbaf4a1cec...updated
→ https://github.com/nodeselector/actions-test-fixtures/releases
✗ 1 of 1 workflow failed: 1 ref_moved
demo/workflows-pwned/5-pinned-before-update.yml
✗ nodeselector/actions-test-fixtures/nested-composite@updated: pinned 38b3412adcb7 but ref now resolves to ea53476fdc17
Ref has moved — investigate before updating. Use `gh actions-pin upgrade` manually.
! 1 issue need manual attention
✗ 1 action require interactive resolution — run `gh actions-pin` locally:
Expected effects:
- Check fails with REF_MOVED — the tag now points to a different SHA
- Lineage is intact — the new SHA is a descendant of the pinned SHA (no imposter warning)
- Compare link lets the developer review exactly what changed
- Developer runs
gh actions-pin upgradeto accept the new version try-it.sh ref-movedreproduces this scenario
Imposter commit — fork injection detected
When a tag moves and the new SHA has no shared ancestry with the pinned SHA, this signals a possible fork-network injection attack (as seen in the tj-actions/changed-files incident). The tool escalates the warning accordingly.
Input:
$ gh actions-pin check demo/workflows-pwned/1-pinned-before-hijack.ymlOutput:
Full terminal session
$ echo "# Workflow was pinned to a legitimate SHA"
# Workflow was pinned to a legitimate SHA
$ grep -E "uses:| -" demo/workflows-pwned/1-pinned-before-hijack.yml
- uses: actions/checkout@v4
- uses: nodeselector/actions-test-fixtures/nested-composite@tampered
- github.com/actions/checkout@v4:sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
- github.com/nodeselector/actions-test-fixtures/nested-composite@tampered:sha1-ea53476fdc172d8552df5af9658a45a367e4f41d
$ echo "# Attacker moved the tag to an orphan commit (fork injection)"
# Attacker moved the tag to an orphan commit (fork injection)
$ gh actions-pin check --no-interactive demo/workflows-pwned/1-pinned-before-hijack.yml
! REF_MOVED nodeselector/actions-test-fixtures/nested-composite@tampered
pinned ea53476fdc17 but ref now resolves to 614a37a63d1a
SHA ea53476fdc17 is NOT reachable from ref — possible fork injection
⚠ The new commit has no shared history with your pinned SHA.
Do NOT accept without reviewing the diff for malicious changes.
→ https://github.com/nodeselector/actions-test-fixtures/compare/ea53476fdc172d8552df5af9658a45a367e4f41d...tampered
→ https://github.com/nodeselector/actions-test-fixtures/releases
✗ 1 of 1 workflow failed: 1 ref_moved
demo/workflows-pwned/1-pinned-before-hijack.yml
✗ nodeselector/actions-test-fixtures/nested-composite@tampered: pinned ea53476fdc17 but ref now resolves to 614a37a63d1a
Ref has moved — investigate before updating. Use `gh actions-pin upgrade` manually.
✗ SHA ea53476fdc17 is NOT reachable from ref — possible fork injection
This may indicate a fork-network injection attack. Do not auto-fix.
! 2 issues need manual attention
✗ 2 actions require interactive resolution — run `gh actions-pin` locally:
Expected effects:
- Check fails with both REF_MOVED (SHA mismatch) and IMPOSTER_COMMIT (no shared ancestry)
- Elevated warning: "The new commit has no shared history with your pinned SHA"
- Compare link provided but with explicit "Do NOT accept" guidance
- Will not auto-accept — requires manual investigation
try-it.sh imposter-commitreproduces this scenario
The lockfile detects changes from a known-good baseline. For the full security model, threat boundaries, and relationship to future runner-side enforcement, see the workflow dependency pinning ADR.
Bare SHA without tag ref
Some workflows pin actions to a raw commit SHA instead of a tag. The tool detects this and presents a tag picker with available releases so the developer can select the right version.
Input:
$ gh actions-pin check demo/workflows-interactive/sha-as-ref.ymlOutput:
Full terminal session (interactive)
$ echo "# Action uses a bare SHA instead of a tag"
# Action uses a bare SHA instead of a tag
$ grep uses: demo/workflows-interactive/sha-as-ref.yml
- uses: actions/checkout@v4
- uses: nodeselector/actions-test-fixtures/nested-composite@ea53476fdc172d8552df5af9658a45a367e4f41d
$ echo "# The tool detects this and offers tag suggestions"
# The tool detects this and offers tag suggestions
$ gh actions-pin check demo/workflows-interactive/sha-as-ref.yml
✗ 1 of 1 workflow failed: 2 not_pinned
! 1 workflow not yet pinned — resolving below
! 1 action is pinned to a bare SHA without a tag ref
↳ resolving below
demo/workflows-interactive/sha-as-ref.yml
! nodeselector/actions-test-fixtures@ea53476fdc17: pinned to a bare SHA without a tag ref — weakens supply-chain security
┃ Pin nodeselector/actions-test-fixtures to which tag? (public)
┃ > v1.0.1 📌 current (recommended)
┃ v1.0.0 📌 current
┃ Show all tags
┃ Skip this action
↑ up • ↓ down • / filter • enter submit
[user selects v1.0.1, confirms with y]
v4 → v4.3.1 (pinning to patch version)
✓ Pinned 5 dependencies in demo/workflows-interactive/sha-as-ref.yml
✓ 1 issue fixed
Expected effects:
- Tags containing the pinned SHA listed, ranked by semver (highest patch first)
📌 currentbadge on tags pointing directly at this commit- User selects a tag, workflow ref updated, lockfile written
try-it.sh sha-as-refreproduces this scenario (interactive)
Unreleased commit
When a workflow pins an action to a commit that doesn't belong to any tagged release, the tool warns and presents a release picker.
Input:
$ gh actions-pin check demo/workflows-interactive/unreleased-commit.ymlOutput:
Full terminal session (interactive)
$ echo "# Action pinned to a commit with no tagged release"
# Action pinned to a commit with no tagged release
$ grep uses: demo/workflows-interactive/unreleased-commit.yml
- uses: actions/checkout@v4
- uses: nodeselector/actions-test-fixtures/nested-composite@dc9920815252718c9bfb9e3ac7d25d4fbf5ca9b1
$ echo "# The tool warns about unreleased code and offers releases"
# The tool warns about unreleased code and offers releases
$ gh actions-pin check demo/workflows-interactive/unreleased-commit.yml
✗ 1 of 1 workflow failed: 2 not_pinned
! 1 workflow not yet pinned — resolving below
! 1 action is pinned to a bare SHA without a tag ref
↳ resolving below
demo/workflows-interactive/unreleased-commit.yml
! nodeselector/actions-test-fixtures@dc9920815252: pinned to a bare SHA without a tag ref — weakens supply-chain security
✗ commit dc9920815252 does not belong to any release — you are running unreleased code
↳ pin to a tagged release instead: releases
┃ Pin nodeselector/actions-test-fixtures to which tag? (public)
┃ > v2.0.0
┃ v1.1.0
┃ Open releases → https://github.com/nodeselector/actions-test-fixtures/releases
┃ Skip this action
↑ up • ↓ down • / filter • enter submit
[user selects v2.0.0, confirms with y]
v4 → v4.3.1 (pinning to patch version)
✓ Pinned 5 dependencies in demo/workflows-interactive/unreleased-commit.yml
✓ 1 issue fixed
Expected effects:
- Red warning: "you are running unreleased code"
- Available releases listed for selection
- Link to releases page provided
- User selects a release, workflow ref and lockfile updated
try-it.sh unreleased-commitreproduces this scenario (interactive)
Note: These interactive edge cases (bare SHA, unreleased commit) are mostly encountered during initial onboarding. Once a repository is pinned with proper tag refs, subsequent checks are expected to be non-interactive or obviated by Dependabot.
CI / lint mode
In CI, the tool auto-detects non-interactive mode (CI=true or non-TTY stdout). For a pure lint check that never writes files, use --write=false:
$ gh actions-pin --write=false .github/workflows/This validates all lockfiles and exits non-zero on any finding — ideal for a CI gate step.
When run with writes enabled (the default), deterministic fixes are auto-applied. Actions that need human input are skipped and the run exits non-zero with guidance on what to resolve locally.
Input:
$ gh actions-pin check --no-interactive demo/workflows-interactive/sha-as-ref.ymlOutput:
Full terminal session
$ echo "# CI mode: auto-fix what you can, fail on what you cannot"
# CI mode: auto-fix what you can, fail on what you cannot
$ gh actions-pin check --no-interactive demo/workflows-interactive/sha-as-ref.yml
✗ 1 of 1 workflow failed: 2 not_pinned
! 1 workflow not yet pinned — resolving below
! 1 action is pinned to a bare SHA without a tag ref
↳ resolving below
demo/workflows-interactive/sha-as-ref.yml
! nodeselector/actions-test-fixtures@ea53476fdc17: pinned to a bare SHA without a tag ref — weakens supply-chain security
- nodeselector/actions-test-fixtures@ea53476fdc172d8552df5af9658a45a367e4f41d: requires interactive tag selection
! 2 actions found but not pinned
v4 → v4.3.1 (pinning to patch version)
✓ Pinned 5 dependencies in demo/workflows-interactive/sha-as-ref.yml
✓ 1 issue fixed
- 1 action skipped
✗ 1 action require interactive resolution — run `gh actions-pin` locally:
nodeselector/actions-test-fixtures@ea53476fdc172d8552df5af9658a45a367e4f41d
$ echo $?
1
Expected effects:
- Deterministic actions (tag-based) auto-pinned
- Ambiguous action (bare SHA) skipped with descriptive message
- Exit code
1— CI fails - Developer told exactly which action to resolve locally
try-it.sh ci-failurereproduces this scenario
gh actions-pin upgrade
Upgrade to latest
Target a specific action and upgrade to its latest stable tag across all workflows.
Input:
$ gh actions-pin upgrade --action actions/checkout demo/workflows-upgrade/ci.ymlOutput:
Full terminal session
$ echo "# Currently pinned to older versions"
# Currently pinned to older versions
$ grep -E 'uses:| -' demo/workflows-upgrade/ci.yml
- uses: actions/checkout@v4.2.0
- uses: actions/setup-go@v5.3.0
- uses: actions/checkout@v4.2.0
- uses: golangci/golangci-lint-action@v6.3.0
- github.com/actions/checkout@v4.2.0:sha1-d631b412a1b64f6193a3f0e66939e2ee43570fdc
- github.com/actions/setup-go@v5.3.0:sha1-f111f3307d8850f501ac008e886eec1fd1932a34
- github.com/golangci/golangci-lint-action@v6.3.0:sha1-e60da84bfae8c7920a47be973d75e15710aa8bd7
$ echo "# Upgrade actions/checkout to latest"
# Upgrade actions/checkout to latest
$ gh actions-pin upgrade --action actions/checkout demo/workflows-upgrade/ci.yml
actions/checkout v4.2.0 → v4.3.1
✓ Updated 2 references in demo/workflows-upgrade/ci.yml
$ echo "# Updated refs and lockfile"
# Updated refs and lockfile
$ grep -E 'uses:| -' demo/workflows-upgrade/ci.yml
- uses: actions/checkout@v4.3.1
- uses: actions/setup-go@v5.3.0
- uses: actions/checkout@v4.3.1
- uses: golangci/golangci-lint-action@v6.3.0
- github.com/actions/checkout@v4.3.1:sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
- github.com/actions/setup-go@v5.3.0:sha1-f111f3307d8850f501ac008e886eec1fd1932a34
- github.com/golangci/golangci-lint-action@v6.3.0:sha1-e60da84bfae8c7920a47be973d75e15710aa8bd7
Expected effects:
uses:refs updated fromv4.2.0→v4.3.1(latest stable)dependencies:block re-locked with new SHAs- Other actions in the workflow untouched
- Shows version diff (
v4.2.0 → v4.3.1) so upgrade is meaningful, not opaque SHA churn try-it.sh upgrade-latestreproduces this scenario
Upgrade to specific version
Same as above, but pins to exactly the version you specify. Combine with --from to scope upgrades to actions currently on a specific ref (e.g., major version migration).
Input:
$ gh actions-pin upgrade --action actions/checkout --version v5 demo/workflows-upgrade/ci.ymlFull terminal session
$ echo "# Currently pinned to actions/checkout@v4.2.0"
# Currently pinned to actions/checkout@v4.2.0
$ grep checkout demo/workflows-upgrade/ci.yml
- uses: actions/checkout@v4.2.0
- uses: actions/checkout@v4.2.0
- github.com/actions/checkout@v4.2.0:sha1-d631b412a1b64f6193a3f0e66939e2ee43570fdc
$ echo "# Upgrade to a specific version (v5), not latest"
# Upgrade to a specific version (v5), not latest
$ gh actions-pin upgrade --action actions/checkout --version v5 demo/workflows-upgrade/ci.yml
actions/checkout v4.2.0 → v5.0.1
✓ Updated 2 references in demo/workflows-upgrade/ci.yml
$ echo "# Verify the pinned version"
# Verify the pinned version
$ grep checkout demo/workflows-upgrade/ci.yml
- uses: actions/checkout@v5.0.1
- uses: actions/checkout@v5.0.1
- github.com/actions/checkout@v5.0.1:sha1-93cb6efe18208431cddfb8368fd83d5badbf9bfd
Expected effects:
- Mutable tag
v5narrowed to patch versionv5.0.1 - Both
uses:refs and lockfile updated atomically --from v4 --version v5pattern enables targeted major version migrationstry-it.sh upgrade-versionreproduces this scenario
JSON output
Machine-readable output for CI integration.
Input:
$ gh actions-pin check --json demo/workflows-pwned/*.yml 2>/dev/null | jq .Output:
Full JSON response
{
"dependencies": [
{
"nwo": "actions/checkout",
"ref": "v4",
"sha": "34e114876b0b11c390a56381ad16ebd13914f8d5",
"hash_algo": "sha1",
"direct": true
},
{
"nwo": "nodeselector/actions-test-fixtures/nested-composite",
"ref": "ref_moved",
"sha": "ea53476fdc172d8552df5af9658a45a367e4f41d",
"hash_algo": "sha1",
"direct": false
}
],
"findings": [
{
"workflow": "demo/workflows-pwned/1-pinned-before-hijack.yml",
"category": "ref_moved",
"severity": "error",
"dependency": "nodeselector/actions-test-fixtures/nested-composite@tampered",
"detail": "pinned ea53476fdc17 but ref now resolves to 614a37a63d1a",
"remediation": "update to 614a37a63d1a with `gh actions-pin upgrade`"
},
{
"workflow": "demo/workflows-pwned/1-pinned-before-hijack.yml",
"category": "imposter_commit",
"severity": "error",
"dependency": "nodeselector/actions-test-fixtures/nested-composite@tampered",
"detail": "SHA ea53476fdc17 is NOT reachable from ref — possible fork injection"
}
],
"valid": false,
"workflows": [
{
"path": "demo/workflows-pwned/1-pinned-before-hijack.yml",
"valid": false,
"findings": ["..."],
"dependencies": ["..."]
}
]
}Expected effects:
valid(bool) — top-level pass/fail for CI gatingfindings— array withcategory,severity,dependency,detail,remediationworkflows— per-file breakdown with findings and dependenciesdependencies— flat list of all resolved dependencies across workflows- Exit code mirrors
valid: 0 = all valid, 1 = findings present try-it.sh json-outputreproduces this scenario








Activity
github-actions commentedon May 4, 2026
Thank you for your issue! We have categorized it as an enhancement (feature or improvement) request, and it has been added to our backlog. In doing so, we are not committing to implementing this feature at this time, but, we will consider it for future releases based on community feedback and our own product roadmap.
Unless you see the help wantedContributions welcome
label, we are not currently looking for external contributions for this feature.
If you come across this issue and would like to see it implemented, please add a thumbs up! This will help us prioritize the feature. Please only comment if you have additional information or viewpoints to contribute.
fproulx-boostsecurity commentedon May 5, 2026
That's pretty sweet and exciting!
wrslatz commentedon May 12, 2026
I really like this plan overall!
actions-pinas the command name is interesting but offers limited extension later and feels less intuitive to me. Wouldactionsas the command allow for more flexibility later? I'm thinking something along the lines of these below optionsgh actions pinto pin dependenciesgh actions check(orpin-check,lock-check) to check the lock filegh actions upgradeto upgrade versionsAnother alternative
gh actions pin --checkto check the pinsgh actions pin --upgrade--checkas the default if no flag is passedBoth of these feel closer to the existing commands and allow future extension. For example, a
gh actions searchorgh actions list-versions, etc.The first bullet point in the description
often includes looking up the available version numbers. This is already available with
gh release list, but perhaps an experience focused around Actions specifically could be beneficial, as an example.benknoble commentedon May 28, 2026
I’m not sure I understand how auto-fixing is actually beneficial in CI environments:
Obviously the goal is to get folks to pin actions in the source of truth, but I think providing an easy path to « (dubiously) pinned in CI even if not pinned in source » provides a false sense of security and creates a hidden class of CI instability.
Auto-resolving sounds nice for the act of initially pinning, however. So perhaps it requires a
--forceflag or something to make it clearly inappropriate for CI?benknoble commentedon May 28, 2026
Based on current
gh help actions, perhaps theworkflowsubcommand could usefully house these commands? Since they operate on workflows…fix(ci): repin standards reusables to a commit that has a lockfile