RFC: `gh actions pin` · Issue #13314 · cli/cli · GitHub
Skip to content

RFC: gh actions pin #13314

Open
Open

Description

@nodeselector

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:

Internal:

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.

  1. Manual pinning is tedious: look up the SHA for each action, replace the ref, remember which tag it corresponded to.
  2. 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.
  3. Upgrades are blind: Dependabot can bump SHAs but can't tell you what version changed or show meaningful diffs
  4. 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-789abcdef1234567890abcdef1234567890abcdef1

Try 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 scenarios

Fixtures 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.yml

Output:

gh actions-pin check

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 check passes cleanly (exit 0)
  • try-it.sh check-autofix reproduces 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-pin

Output:

Edit and re-pin workflow

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:

  • check re-resolves all actions from their new refs
  • New dependencies: block written with updated SHAs
  • try-it.sh edit-repin reproduces 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.yml

Output:

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 upgrade to accept the new version
  • try-it.sh ref-moved reproduces 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.yml

Output:

Imposter commit detected — fork injection

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-commit reproduces 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.yml

Output:

SHA-as-ref tag picker

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)
  • 📌 current badge on tags pointing directly at this commit
  • User selects a tag, workflow ref updated, lockfile written
  • try-it.sh sha-as-ref reproduces 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.yml

Output:

Unreleased commit warning

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-commit reproduces 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.yml

Output:

CI failure on ambiguous action

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-failure reproduces 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.yml

Output:

gh actions-pin upgrade

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 from v4.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-latest reproduces 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.yml
Full 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 v5 narrowed to patch version v5.0.1
  • Both uses: refs and lockfile updated atomically
  • --from v4 --version v5 pattern enables targeted major version migrations
  • try-it.sh upgrade-version reproduces 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 gating
  • findings — array with category, severity, dependency, detail, remediation
  • workflows — per-file breakdown with findings and dependencies
  • dependencies — flat list of all resolved dependencies across workflows
  • Exit code mirrors valid: 0 = all valid, 1 = findings present
  • try-it.sh json-output reproduces this scenario

Activity

  1. added
    needs-designAn engineering task needs design to proceed
    enhancementa request to improve CLI
    gh-workflowrelating to the gh workflow command
    on May 4, 2026
  2. github-actions commented on May 4, 2026

    @github-actions
    Contributor

    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 wanted Contributions 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.

  3. fproulx-boostsecurity commented on May 5, 2026

    @fproulx-boostsecurity

    That's pretty sweet and exciting!

  4. wrslatz commented on May 12, 2026

    @wrslatz
    Contributor

    I really like this plan overall!

    actions-pin as the command name is interesting but offers limited extension later and feels less intuitive to me. Would actions as the command allow for more flexibility later? I'm thinking something along the lines of these below options

    • gh actions pin to pin dependencies
    • gh actions check (or pin-check, lock-check) to check the lock file
    • gh actions upgrade to upgrade versions

    Another alternative

    • gh actions pin --check to check the pins
    • gh actions pin --upgrade
    • with --check as the default if no flag is passed

    Both of these feel closer to the existing commands and allow future extension. For example, a gh actions search or gh actions list-versions, etc.

    The first bullet point in the description

    • Manual pinning is tedious: look up the SHA for each action

    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.

  5. benknoble commented on May 28, 2026

    @benknoble

    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.
    

    I’m not sure I understand how auto-fixing is actually beneficial in CI environments:

    1. When auto-fixed, the source that runs in CI may be different from the source a developer sees in a local checkout. That is confusing and can be hard to debug (since CI may not even show the changes). Further, it might encourage (by being unnoticeable) developers to forget/ignore pinning in the source. « It happens invisibly in CI. »
    2. Assuming the auto-fix resolves the Git object ID and tag at fix time, the tag may have already been mutated since the point where it was added to the workflow. That still leaves folks vulnerable to the security issue of unpinned actions with tags mutable tags.

    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 --force flag or something to make it clearly inappropriate for CI?

  6. benknoble commented on May 28, 2026

    @benknoble

    I really like this plan overall!

    actions-pin as the command name is interesting but offers limited extension later and feels less intuitive to me. Would actions as the command allow for more flexibility later? I'm thinking something along the lines of these below options

    Based on current gh help actions, perhaps the workflow subcommand could usefully house these commands? Since they operate on workflows…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementa request to improve CLIgh-workflowrelating to the gh workflow commandneeds-designAn engineering task needs design to proceedneeds-investigationCLI team needs to investigate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions