Skip to content

Add recipe to pin GitHub Actions to commit SHAs#169

Merged
timtebeek merged 24 commits intomainfrom
pin-actions-to-sha
Apr 29, 2026
Merged

Add recipe to pin GitHub Actions to commit SHAs#169
timtebeek merged 24 commits intomainfrom
pin-actions-to-sha

Conversation

@timtebeek
Copy link
Copy Markdown
Member

Summary

  • Adds PinGitHubActionsToSha recipe that replaces mutable tag/branch uses: references with immutable commit SHAs, complementing the existing UnpinnedActionsRecipe (which finds the problem) with an automated fix
  • Includes a bundled known-action-shas.json with 37 well-known action → SHA mappings (Codecov, Docker, AWS, Google, Gradle, GitHub official actions, etc.), with fallback to the GitHub API for any action not in the static map
  • pinOfficialActions option (default false) controls whether actions/* and github/* org actions are also pinned; original ref is preserved as an inline comment (e.g. uses: actions/checkout@b4ffde65... # v4)

Test plan

  • shouldPinThirdPartyActionFromStaticMap — verifies static map lookup and SHA substitution with comment
  • shouldNotPinOfficialActionsByDefault — no-op when pinOfficialActions = false
  • shouldPinOfficialActionsWhenOptedIn — pins actions/* when opted in
  • shouldPinGitHubOrgWhenOptedIn — pins github/* (e.g. codeql-action) when opted in
  • shouldNotModifyAlreadyPinnedAction — idempotent on already-SHA-pinned refs
  • shouldSkipLocalActions — ignores ./ refs
  • shouldSkipDockerReferences — ignores docker:// refs
  • shouldPinMultipleThirdPartyActions — pins multiple actions in one workflow
  • shouldIgnoreNonWorkflowFiles — only applies to .github/workflows/ files via IsGitHubActionsWorkflow
  • shouldPinActionWithSubpath — handles owner/repo/subpath@ref correctly
  • shouldMixPinnedAndUnpinnedActions — mixed workflow with already-pinned, local, and unpinned actions

New recipe that replaces mutable tag/branch references in GitHub Actions
uses: declarations with immutable commit SHAs. Includes static SHA
mapping for common actions with GitHub API fallback, configurable
official action pinning, and comprehensive test coverage.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Mar 26, 2026
@timtebeek timtebeek marked this pull request as draft March 26, 2026 22:13
All SHA mappings now live exclusively in known-action-shas.json.
If the resource cannot be loaded, return an empty map instead of
falling back to a duplicate in-code copy.
Runs weekly (Monday 06:00 UTC) and on manual dispatch. Scans the
repo's own workflow files for uses: references with tags/branches,
resolves each to a commit SHA via the GitHub API, and appends any
new entries to known-action-shas.json (existing entries are never
modified or removed). Opens a PR with the new mappings listed.
- Replace Set.of() with Collections.unmodifiableSet(new HashSet<>(Arrays.asList()))
- Replace String.isBlank() with trim().isEmpty()
- Replace InputStream.readAllBytes() with a ByteArrayOutputStream read loop
- Remove withSuffix() call (not present on Yaml.Scalar); drop inline ref comment
- Update test expectations to match (no trailing # ref comment)
Add the missing recipe catalog entry for PinGitHubActionsToSha to fix
CI validation. Also apply minor style cleanups (static imports,
consistent test indentation).
Load known SHAs in getInitialValue() instead of a static field,
following the ScanningRecipe pattern for proper lifecycle management.
…anch refs

- Replace known-action-shas.json with known-action-shas.properties to
  drop the Jackson dependency from PinGitHubActionsToSha
- Expand from 38 to 88 well-known action mappings covering the most
  popular GitHub Actions across CI/CD, Docker, cloud providers, build
  tools, security scanning, and language setup
- Remove branch references (@master) since only tags are stable
- Parse GitHub API responses with a simple regex instead of Jackson
- Update the update-known-shas workflow for the new properties format
Move the action-to-SHA resolution logic from the GHA workflow inline
run into .github/scripts/update-known-shas.sh so it can be run
locally outside of GitHub Actions. The workflow now calls the script
and uses git diff to detect changes.
Resolve all major version tags (v1, v2, v3, ...) for every action in
the properties file, bringing the total from 88 to 232 entries. This
ensures the recipe can pin any major version reference without falling
back to the GitHub API.
@timtebeek timtebeek marked this pull request as ready for review March 27, 2026 10:36
The script now checks all existing entries against the GitHub API and
updates any whose floating tags (e.g. v4) have moved to a new commit.
The workflow PR body distinguishes between updated and newly added entries.
Explains what the file contains, how to add new actions, and that
header comments are preserved across regeneration by the update script.
Expand the update script to resolve all version tags (v1, v1.2, v1.2.3)
for every action, not just major versions. Patch and minor versions are
more likely to be pinned to a stable SHA. Also fix bash 3.2 compat by
replacing mapfile with a while-read loop.

Grows the properties file from 232 to 4057 entries.
Minor and patch version tags (v1.2, v1.2.3) are immutable, so skip
them during drift detection. Only major version tags (v1, v2) can
float to new commits. Also update the properties file header comment
to accurately describe this behavior.
Expand the known-action-shas.properties from ~230 to ~9,400 entries
covering popular actions across CI/build, security, deployment, Docker,
GitHub/PR, linting, notifications, infrastructure, and utility
categories. Each action includes all major, minor, and patch version
tags.

Also add a SEED_ACTIONS list to the update script so these actions are
always expanded on future runs, even if not referenced in local
workflow files. 24 actions hit GitHub API rate limits and will be
resolved on the next run.
Produces `uses: action@sha # tag` instead of just `uses: action@sha`,
matching the convention used by Dependabot, Renovate, and StepSecurity.
Inlines the visitor and handles edge cases where `uses:` is the last
entry in its mapping (e.g. reusable workflow calls with no siblings).
Resolve recipes.csv conflict: adopt main's class renames (drop Recipe
suffix) while keeping the PinGitHubActionsToSha entry from this branch.
The shouldPinReusableWorkflowAsOnlyEntry test used
openrewrite/gh-automation@main which isn't in the static SHA map,
causing it to resolve via the live GitHub API. Since main is a
moving target, the expected SHA drifted and the test failed.

Replace with shouldPinActionAsLastEntryInMapping which uses
codecov/codecov-action@v4 from the static map, still exercising
the same code path (comment placement when uses: is the last
entry in its mapping).
steve-aom-elliott and others added 5 commits April 29, 2026 22:51
Adds an `includedActions` option that, when set, restricts pinning to
only those actions matching one of the supplied patterns. Patterns may
be `owner/repo` (exact), `owner/*` (any repo within an org), or
`owner/repo/subpath` (exact match including subpath). When the allow
list is non-empty, an explicit allow always wins — `pinOfficialActions`
is bypassed for matched entries — so `actions/checkout` can be pinned
without flipping the official-actions flag globally.

Six new tests cover allow-list filtering, the org wildcard, subpath
handling (both `owner/repo` matches with subpath and exact `owner/repo/subpath`
patterns), pinning an official action via the allow list, and the
empty-list-falls-back-to-default behavior.
Replace raw HttpURLConnection with org.openrewrite.ipc.http.HttpSender
obtained via HttpSenderExecutionContextView so the recipe respects any
HTTP sender configured by the host (e.g. the Moderne Platform, where
direct network access may be restricted or routed through a proxy).
…eaders

- Replace any same-line `# user comment` after `uses:` with `# <tag>`
  rather than prepending the tag, matching the convention Dependabot and
  Renovate read for future updates. New helper mergeTagCommentIntoPrefix
  encapsulates the merge logic and is used in both the in-mapping path and
  the doAfterVisit / Document.End path.
- Add a known-action-shas entry for slsa-framework reusable workflow at v2.1.0
  and a test that pins it end-to-end, exercising the
  `org/repo/.github/workflows/file.yml@<tag>` ref form.
- Switch the GitHub REST call from the legacy `application/vnd.github.v3+json`
  Accept media type to `application/vnd.github+json`, consistent with the
  `X-GitHub-Api-Version: 2022-11-28` header already on the request.
- Bump copyright header to 2026 on the three PR-added files.

Three additional tests cover the new comment-merging behavior:
shouldReplaceExistingInlineCommentWithVersionTag,
shouldReplaceExistingInlineCommentWhenUsesIsLastEntry, and
shouldPreserveCommentOnDifferentLine (which confirms standalone comments
on prior lines are untouched).
Run via `./gradlew recipeCsvGenerate` so the new option from the
allow-list change appears in both the description and the JSON options
list, keeping the recipe catalog in sync with the recipe class.
@steve-aom-elliott
Copy link
Copy Markdown
Contributor

A few items I noticed while looking at this that may be worth follow-ups (not blocking IMO):

  • Branch refs silently get pinned to a moment-in-time SHA. uses: foo/bar@main becomes foo/bar@<sha> # main, where the # main comment is now misleading and Dependabot/Renovate won't update it (they look up the named ref). Two reasonable options: skip non-tag refs entirely, or write # <sha-was-main-at-YYYY-MM-DD> so the staleness is obvious.
  • No signal when API resolution fails. Unauthenticated callers are rate-limited to 60/hour; once exceeded, the recipe silently leaves uses: unchanged. A workflow with many unmapped actions could end up half-pinned with no indication why. A note via ExecutionContext.getOnError() (or even just an INFO-level recipe-cycle marker) would help users diagnose.
  • catch (RuntimeException e) at PinGitHubActionsToSha.java#L314. Worth verifying whether HttpSender.send can throw checked IOException (or anything else outside RuntimeException) — if so, those would bubble out of the visitor instead of being treated as "skip this action."
  • Composite actions in action.yml are not covered. The IsGitHubActionsWorkflow precondition only matches .github/workflows/*.{yml,yaml}, so uses: references inside a composite action.yml file at any other path are silently skipped. The recipe name doesn't advertise this limitation — either widen the precondition or call it out in the description.
  • githubApiToken is a plain @Option string. It could be marked sensitive so it doesn't surface in run history or recipe-catalog UIs.

@steve-aom-elliott steve-aom-elliott self-requested a review April 29, 2026 21:41
Copy link
Copy Markdown
Contributor

@steve-aom-elliott steve-aom-elliott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few comments above that can be decided whether they're worth exploring further, but I think it's a fairly good initial version of the recipe otherwise.

@github-project-automation github-project-automation Bot moved this from In Progress to Ready to Review in OpenRewrite Apr 29, 2026
timtebeek and others added 2 commits April 30, 2026 00:03
- Anchor SHA_RESPONSE_PATTERN to the start of the JSON body so nested
  tree.sha / parents[].sha can no longer be returned by mistake.
- Pass the PR body via an env var and key the branch name on
  GITHUB_RUN_ID, avoiding bash command substitution on backticks in the
  body and same-day branch collisions.
When `uses:` references a branch (e.g. `main`, `master`), the resolved
SHA is only valid as of a point in time, since branches keep moving.
Emit the comment as `<branch> @ YYYY-MM-DD` so reviewers and update
tools can tell when the pin was taken. Tag refs continue to round-trip
as just `<tag>` so Dependabot/Renovate keep recognizing them.
@timtebeek timtebeek merged commit dc09631 into main Apr 29, 2026
1 check passed
@timtebeek timtebeek deleted the pin-actions-to-sha branch April 29, 2026 22:19
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants