Skip to content

Add PR check to enforce SHA-pinned GitHub Actions - #2771

Merged
P0NDER0SA merged 5 commits into
mainfrom
add-pr-action-pinning-check
Aug 26, 2026
Merged

Add PR check to enforce SHA-pinned GitHub Actions#2771
P0NDER0SA merged 5 commits into
mainfrom
add-pr-action-pinning-check

Conversation

@P0NDER0SA

Copy link
Copy Markdown
Contributor

Description

Follow-up to the GH Actions SHA-pinning remediation: adds a mandatory PR check that automatically fails if a workflow or composite action references a third-party action by a mutable tag/branch instead of a full 40-character commit SHA. Same implementation as cds-snc/notification-manifests#5133.

Why not Renovate or the existing OSSF Scorecard job?

  • Renovate already auto-proposes PRs to keep pins updated, but it's a remediation bot — it can't block a human PR that introduces a new unpinned action.
  • Scorecard's Pinned-Dependencies check only runs weekly/on push to main as a scoring job — it never runs on pull_request and doesn't fail the build.

Neither currently blocks a PR, which is the actual gap this closes.

What this does

  • New workflow: .github/workflows/check_pinned_actions.yaml, triggered on pull_request/push:main when .github/workflows/** or .github/actions/** change.
  • Runs zizmor and filters its results down to just the unpinned-uses finding, so it stays scoped to SHA-pinning only (zizmor's other audits, e.g. template-injection, are a separate concern, out of scope here).
  • Fails the check with actionable output (file + finding) and remediation guidance in the logs if any unpinned action is found.
  • Exception mechanism: an inline # zizmor: ignore[unpinned-uses] <reason> comment skips the check for that line. Demonstrated on cds-snc/notification-pr-bot@main in docker.yaml, which intentionally tracks main (already excluded from Renovate via this repo's renovate.json ignoreDeps) and can't be hash-pinned without losing that behavior.

To make this a real merge gate

Branch protection on main needs "Scan for unpinned actions" added as a required status check (not done in this PR — repo settings, not code).

Testing

Ran zizmor locally against this repo's .github/workflows: found and fixed the one pre-existing unpinned-uses finding, confirmed 0 findings remain, and confirmed the CI script correctly fails against a deliberately unpinned test workflow.

Runs zizmor against .github/workflows (and .github/actions, if present) on
every PR that touches them, and fails the check if any action is referenced
by a mutable tag/branch instead of a full commit SHA. Follows up on the
one-time SHA-pinning remediation to prevent regressions.

Exceptions can be added inline with a '# zizmor: ignore[unpinned-uses]'
comment, demonstrated on the notification-pr-bot@main reference which
intentionally tracks main.
@github-actions

Copy link
Copy Markdown

Keeps the exception self-documenting per zizmor's own convention
(# zizmor: ignore[rule] <reason>), rather than relying on a separate
comment line above that could get edited or moved away independently.
…ants

Falls back to the concrete feature (or a placeholder) instead of
printing null if the location isn't a Local symbolic key.
Passing a nonexistent local path caused zizmor to misinterpret it as
a remote owner/repo spec and fail with a GitHub API 404, since most
of these repos don't have composite actions under .github/actions.
Copilot's suggested fix used a nonexistent 'given_path' field and lost
the finding-level '.desc' by restructuring through .locations[] without
capturing it first - verified it actually printed 'unknown location:N:
null' against real zizmor output. This version keeps .desc, selects the
Primary location, and adds a line number.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants