-
Notifications
You must be signed in to change notification settings - Fork 364
Update actions to use versions that use Node v24 #3451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,227 @@ | ||||||
| /** | ||||||
| * Scan all workflow and action YAML files for GitHub Action references and | ||||||
| * ensure they are pinned to commit SHAs. Handles two cases: | ||||||
| * 1. Already pinned (`uses: owner/repo@<sha> # <tag>`) — updates stale SHAs | ||||||
| * 2. Unpinned (`uses: owner/repo@<tag>`) — replaces with `@<sha> # <tag>` | ||||||
|
Comment on lines
+2
to
+5
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you say "stale", does this mean "not latest" or something else? ie. Is this an upgrade script or just one that transforms our workflow yaml file to all have SHA-pinned actions references? |
||||||
| * | ||||||
| * Usage: node utils/update-pinned-actions.js | ||||||
| */ | ||||||
| import {execSync} from "node:child_process"; | ||||||
| import fs from "node:fs"; | ||||||
|
|
||||||
| import fg from "fast-glob"; | ||||||
|
|
||||||
| // Matches already-pinned: `uses: owner/repo@<sha> # <tag>` | ||||||
| // - Lookbehind ensures no `#` before `uses:` on the same line (skips YAML comments) | ||||||
| // - Supports optional quotes: `uses: "owner/repo@<sha>" # <tag>` | ||||||
| // - Uses [^\S\n]+ instead of \s+ to prevent matching across lines | ||||||
| // Groups: action(1), sha(2), quote(3), ref(4) | ||||||
| const PINNED_RE = | ||||||
| /(?<=^[^#\n]*uses:\s+"?)([^@\s"]+)@([a-f0-9]{40})("?)[^\S\n]+#[^\S\n]*(\S+)/gm; | ||||||
|
|
||||||
| // Matches pinned-without-tag: `uses: owner/repo@<sha>` (no `# <tag>` comment) | ||||||
| // Used only for collecting action names, not for updates. | ||||||
| // Groups: action(1) | ||||||
| const PINNED_NO_TAG_RE = | ||||||
| /(?<=^[^#\n]*uses:\s+"?)([^\s@"]+\/[^\s@"]+)@[a-f0-9]{40}"?\s*$/gm; | ||||||
|
|
||||||
| // Matches unpinned: `uses: owner/repo@<tag>` (where tag is NOT a 40-char hex SHA) | ||||||
| // - Excludes local actions (starting with ./) | ||||||
| // - Lookbehind ensures no `#` before `uses:` on the same line (skips YAML comments) | ||||||
| // - Supports optional quotes: `uses: "owner/repo@<tag>"` | ||||||
| // - Action name uses [^\s@"] to avoid capturing quotes | ||||||
| // Groups: action(1), unused(2), ref(3), quote(4) | ||||||
| const UNPINNED_RE = | ||||||
| /(?<=^[^#\n]*uses:\s+"?)([^\s@"]+\/[^\s@"]+)@(?!([a-f0-9]{40})(?:\s|"))([^\s"]+)("?)/gm; | ||||||
|
|
||||||
| /** | ||||||
| * Resolve a tag or branch to its commit SHA via git ls-remote. | ||||||
| * For annotated tags the dereferenced (^{}) commit SHA is returned. | ||||||
| */ | ||||||
| const resolveRef = (action, ref) => { | ||||||
| // Extract just owner/repo (ignore sub-paths like /restore, /save) | ||||||
| const repo = action.split("/").slice(0, 2).join("/"); | ||||||
| const url = `https://github.com/${repo}.git`; | ||||||
|
|
||||||
| // Try tags first (covers both lightweight and annotated) | ||||||
| const tagOutput = execSync(`git ls-remote --tags ${url} ${ref} ${ref}^{}`, { | ||||||
| encoding: "utf-8", | ||||||
| }).trim(); | ||||||
|
|
||||||
| if (tagOutput) { | ||||||
| const lines = tagOutput.split("\n"); | ||||||
| // If there's a ^{} line it's an annotated tag — use the deref SHA | ||||||
| const deref = lines.find((l) => l.includes("^{}")); | ||||||
| if (deref) { | ||||||
| return deref.split(/\s+/)[0]; | ||||||
| } | ||||||
| return lines[0].split(/\s+/)[0]; | ||||||
| } | ||||||
|
|
||||||
| // Fall back to branches | ||||||
| const branchOutput = execSync(`git ls-remote --heads ${url} ${ref}`, { | ||||||
| encoding: "utf-8", | ||||||
| }).trim(); | ||||||
|
|
||||||
| if (branchOutput) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we fall back to a branch ref, the might want to warn. I guess we will still have the changes in the repo to review through normal git workflows... but it feels worth warning about. |
||||||
| return branchOutput.split(/\s+/)[0]; | ||||||
| } | ||||||
|
|
||||||
| return null; | ||||||
| }; | ||||||
|
|
||||||
| // -- main ------------------------------------------------------------------- | ||||||
|
|
||||||
| const files = fg.sync([ | ||||||
| ".github/workflows/*.yml", | ||||||
| ".github/workflows/*.yaml", | ||||||
| ".github/actions/**/*.yml", | ||||||
| ".github/actions/**/*.yaml", | ||||||
| "actions/**/action.yml", | ||||||
| "actions/**/action.yaml", | ||||||
| ]); | ||||||
|
|
||||||
| // Collect unique action+ref pairs across all files | ||||||
|
|
||||||
| const seen = new Map(); // key: "action@ref" → resolved SHA (filled later) | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although not entirely necessary, I find moving the contents of the script body into a |
||||||
|
|
||||||
| const allRepos = new Set(); // all unique owner/repo names (for listing) | ||||||
|
|
||||||
| for (const file of files) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These loops all feel like they'd be handy to have in functions so the steps of this script are easier to digest. |
||||||
| const content = fs.readFileSync(file, "utf-8"); | ||||||
| let m; | ||||||
|
|
||||||
| // Collect already-pinned refs | ||||||
| // Groups: action(1), sha(2), quote(3), ref(4) | ||||||
| PINNED_RE.lastIndex = 0; | ||||||
| while ((m = PINNED_RE.exec(content)) !== null) { | ||||||
| const [, action, , , ref] = m; | ||||||
| seen.set(`${action}@${ref}`, null); | ||||||
| allRepos.add(action.split("/").slice(0, 2).join("/")); | ||||||
| } | ||||||
|
|
||||||
| // Collect pinned-without-tag refs (for listing only) | ||||||
| PINNED_NO_TAG_RE.lastIndex = 0; | ||||||
| while ((m = PINNED_NO_TAG_RE.exec(content)) !== null) { | ||||||
| const [, action] = m; | ||||||
| allRepos.add(action.split("/").slice(0, 2).join("/")); | ||||||
| } | ||||||
|
|
||||||
| // Collect unpinned refs (tag/branch directly after @) | ||||||
| UNPINNED_RE.lastIndex = 0; | ||||||
| while ((m = UNPINNED_RE.exec(content)) !== null) { | ||||||
| const [, action, , ref] = m; | ||||||
| seen.set(`${action}@${ref}`, null); | ||||||
| allRepos.add(action.split("/").slice(0, 2).join("/")); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if (seen.size === 0) { | ||||||
| console.log("No action references found."); | ||||||
| process.exit(0); | ||||||
| } | ||||||
|
|
||||||
| // Print unique non-actions/* repos in alphabetical order | ||||||
| const uniqueRepos = [...allRepos] | ||||||
| .filter((repo) => !repo.startsWith("actions/")) | ||||||
| .sort(); | ||||||
| console.log("Allowed actions:\n"); | ||||||
| for (const repo of uniqueRepos) { | ||||||
| console.log(`${repo}@*,`); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Can we indent this repo listing and make it a "bulleted" list?
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alternately, we could use
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, since the workflow yml files reference actions as |
||||||
| } | ||||||
| console.log(""); | ||||||
|
|
||||||
| console.log(`Found ${seen.size} unique action reference(s). Resolving…\n`); | ||||||
|
|
||||||
| // Resolve each unique action+ref | ||||||
| let failures = 0; | ||||||
| for (const key of seen.keys()) { | ||||||
| const [action, ref] = key.split("@"); | ||||||
| console.log(` Resolving ${action} @ ${ref}`); | ||||||
| try { | ||||||
| const sha = resolveRef(action, ref); | ||||||
| if (!sha) { | ||||||
| console.log(` ⚠ Could not resolve ref "${ref}" for ${action}`); | ||||||
| failures++; | ||||||
| } else { | ||||||
| seen.set(key, sha); | ||||||
| console.log(` → ${sha}`); | ||||||
| } | ||||||
| } catch (err) { | ||||||
| console.log(` ⚠ Error resolving ${action}@${ref}: ${err.message}`); | ||||||
| failures++; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| console.log(""); | ||||||
|
|
||||||
| // Update files in-place | ||||||
| let updatedFiles = 0; | ||||||
| let updatedRefs = 0; | ||||||
| let alreadyCurrent = 0; | ||||||
|
|
||||||
| for (const file of files) { | ||||||
| let content = fs.readFileSync(file, "utf-8"); | ||||||
| let fileChanged = false; | ||||||
|
|
||||||
| // Update already-pinned refs with stale SHAs | ||||||
| // Groups: action(1), sha(2), quote(3), ref(4) | ||||||
| PINNED_RE.lastIndex = 0; | ||||||
| content = content.replace( | ||||||
| PINNED_RE, | ||||||
| (match, action, oldSha, quote, ref) => { | ||||||
| const newSha = seen.get(`${action}@${ref}`); | ||||||
| if (!newSha || newSha === oldSha) { | ||||||
| if (newSha === oldSha) { | ||||||
| alreadyCurrent++; | ||||||
| } | ||||||
| return match; | ||||||
| } | ||||||
| console.log(` ${file}: ${action}@${ref}`); | ||||||
| console.log(` ${oldSha} → ${newSha}`); | ||||||
| fileChanged = true; | ||||||
| updatedRefs++; | ||||||
| return `${action}@${newSha}${quote} # ${ref}`; | ||||||
| }, | ||||||
| ); | ||||||
|
|
||||||
| // Pin unpinned refs (tag/branch → sha # tag) | ||||||
| // Groups: action(1), unused(2), ref(3), quote(4) | ||||||
| UNPINNED_RE.lastIndex = 0; | ||||||
| content = content.replace( | ||||||
| UNPINNED_RE, | ||||||
| (match, action, _unused, ref, quote) => { | ||||||
| const newSha = seen.get(`${action}@${ref}`); | ||||||
| if (!newSha) { | ||||||
| return match; | ||||||
| } | ||||||
| console.log(` ${file}: ${action}@${ref} (unpinned)`); | ||||||
| console.log(` → ${newSha} # ${ref}`); | ||||||
| fileChanged = true; | ||||||
| updatedRefs++; | ||||||
| return `${action}@${newSha}${quote} # ${ref}`; | ||||||
| }, | ||||||
| ); | ||||||
|
|
||||||
| if (fileChanged) { | ||||||
| fs.writeFileSync(file, content); | ||||||
| updatedFiles++; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Summary | ||||||
| console.log(""); | ||||||
| if (updatedRefs > 0) { | ||||||
| console.log( | ||||||
| `🏁 Updated ${updatedRefs} reference(s) across ${updatedFiles} file(s).`, | ||||||
| ); | ||||||
| } else { | ||||||
| console.log("🏁 All pinned actions are already up-to-date."); | ||||||
| } | ||||||
| if (alreadyCurrent > 0) { | ||||||
| console.log(` ${alreadyCurrent} reference(s) already current.`); | ||||||
| } | ||||||
| if (failures > 0) { | ||||||
| console.log(` ⚠ ${failures} reference(s) could not be resolved.`); | ||||||
| process.exit(1); | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we upgrade this to be a .ts script using
swc? We do that in other scripts in this dir and it at least gives us some type safety.