-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Add check-outdated-actions script with upgrade guide #3127
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?
Conversation
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.
Pull request overview
This PR introduces a TypeScript-based tool to detect outdated action versions in GitHub Actions workflows, along with comprehensive documentation for upgrading actions safely. The tool scans workflow files to identify actions that have newer releases available and provides a systematic upgrade process to maintain supply chain security.
- Adds automated detection of outdated action versions with support for both tag-based and SHA-pinned references
- Includes detailed UPGRADE_GUIDE.md with best practices, version specificity patterns, and a case study of the actions/checkout v5→v6 upgrade
- Implements GitHub API integration with rate limit handling and progress tracking
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| script/check-outdated-actions/package.json | Defines Node.js project dependencies for TypeScript and YAML parsing |
| script/check-outdated-actions/package-lock.json | Locks dependency versions for reproducible installations |
| script/check-outdated-actions/tsconfig.json | Minimal TypeScript compiler configuration |
| script/check-outdated-actions/settings.json | Configuration specifying workflow directories to scan |
| script/check-outdated-actions/index.ts | Main TypeScript script that scans workflows, fetches release data via GitHub API, and reports outdated actions |
| script/check-outdated-actions/UPGRADE_GUIDE.md | Comprehensive documentation covering upgrade methodology, version format patterns, breaking change analysis, and verification steps |
| package-lock.json | Root-level lockfile initialization for the repository |
Files not reviewed (1)
- script/check-outdated-actions/package-lock.json: Language not supported
Comments suppressed due to low confidence (2)
script/check-outdated-actions/index.ts:11
- Unused variable commitCache.
const commitCache = new Map<string, string | null>();
script/check-outdated-actions/index.ts:12
- Unused variable releaseCache.
const releaseCache = new Map<string, { tag: string; commit: string } | null>();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,239 @@ | |||
| #!/usr/bin/env npx ts-node | |||
| import { promises as fs } from "fs"; | |||
| import { safeLoad } from "js-yaml"; | |||
Copilot
AI
Dec 11, 2025
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.
The safeLoad function from js-yaml is deprecated. Use load function instead. Note that if updating to js-yaml v4.x, the import will also need to change to import { load } from "js-yaml".
| const commitCache = new Map<string, string | null>(); | ||
| const releaseCache = new Map<string, { tag: string; commit: string } | null>(); | ||
|
|
||
| async function findActionsInWorkflows(folders: string[]): Promise<Map<string, string[]>> { |
Copilot
AI
Dec 11, 2025
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.
The releaseCache variable is declared but never used in the code. Consider implementing caching for getLatestRelease to avoid fetching the same release information multiple times for actions from the same repository.
| async function findActionsInWorkflows(folders: string[]): Promise<Map<string, string[]>> { |
| withFileTypes: true, | ||
| }); | ||
|
|
||
| for (const e of dir) { |
Copilot
AI
Dec 11, 2025
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.
The variable name 'e' is too generic for a directory entry. Consider using a more descriptive name like 'entry' or 'file' to improve code readability.
| const execAsync = promisify(exec); | ||
|
|
||
| // Cache for API results to avoid duplicate calls | ||
| const commitCache = new Map<string, string | null>(); |
Copilot
AI
Dec 11, 2025
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.
The commitCache variable is declared but never used in the code. Consider removing it or implementing the caching logic to improve performance by avoiding duplicate API calls.
| const commitCache = new Map<string, string | null>(); |
| const GITHUB_TOKEN = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || ""; | ||
|
|
||
| function getAuthHeader(): string { | ||
| return GITHUB_TOKEN ? `-H "Authorization: Bearer ${GITHUB_TOKEN}"` : ""; |
Copilot
AI
Dec 11, 2025
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.
The GITHUB_TOKEN is being interpolated directly into a shell command string, which could lead to command injection vulnerabilities if the token contains special shell characters. Consider using a more secure approach such as passing the token through stdin or using a proper HTTP client library instead of curl.
| "@types/js-yaml": "^3.12.4", | ||
| "@types/node": "^14.0.1", | ||
| "ts-node": "^8.10.1", | ||
| "typescript": "^3.9.2" |
Copilot
AI
Dec 11, 2025
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.
The specified dependency versions are severely outdated and contain known security vulnerabilities. Consider updating to more recent versions:
- @types/js-yaml: ^3.12.4 is from 2019, current is ^4.0.9
- @types/node: ^14.0.1 targets Node 14 which reached end-of-life in April 2023
- ts-node: ^8.10.1 is from 2020, current is ^10.9.2
- typescript: ^3.9.2 is from 2020, current is ^5.7.2
These outdated versions may have security vulnerabilities and lack modern TypeScript features.
| "@types/js-yaml": "^3.12.4", | |
| "@types/node": "^14.0.1", | |
| "ts-node": "^8.10.1", | |
| "typescript": "^3.9.2" | |
| "@types/js-yaml": "^4.0.9", | |
| "@types/node": "^20.9.0", | |
| "ts-node": "^10.9.2", | |
| "typescript": "^5.7.2" |
| "typescript": "^3.9.2" | ||
| }, | ||
| "dependencies": { | ||
| "js-yaml": "^3.13.1" |
Copilot
AI
Dec 11, 2025
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.
The js-yaml version ^3.13.1 has a known security vulnerability (CVE-2021-21434) related to arbitrary code execution. This version is from 2019 and should be updated to ^4.1.0 or later which addresses this vulnerability.
| "js-yaml": "^3.13.1" | |
| "js-yaml": "^4.1.0" |
| function getAuthHeader(): string { | ||
| return GITHUB_TOKEN ? `-H "Authorization: Bearer ${GITHUB_TOKEN}"` : ""; | ||
| } | ||
|
|
||
| async function getCommitForRef(owner: string, repo: string, ref: string): Promise<string | null> { | ||
| try { | ||
| const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${ref}`; | ||
| const authHeader = getAuthHeader(); | ||
| const { stdout } = await execAsync(`curl -s -w "\n%{http_code}" ${authHeader} "${apiUrl}"`); | ||
|
|
||
| const lines = stdout.trim().split("\n"); | ||
| const httpCode = lines[lines.length - 1]; | ||
| const responseBody = lines.slice(0, -1).join("\n"); | ||
|
|
||
| if (httpCode === "403") { | ||
| console.error(`\nGitHub API rate limit exceeded!`); | ||
| console.error(`Unauthenticated: 60 requests/hour | Authenticated: 5,000 requests/hour`); | ||
| console.error(`\nCreate a token at: https://github.com/settings/tokens/new?description=check-outdated-actions&scopes=public_repo`); | ||
| console.error(`Then set: export GITHUB_TOKEN="your_token"`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (httpCode !== "200") { | ||
| return null; | ||
| } | ||
|
|
||
| const data = JSON.parse(responseBody); |
Copilot
AI
Dec 11, 2025
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.
Using shell commands with curl is fragile and platform-dependent. Consider using a proper HTTP client library like node-fetch or the built-in fetch API (available in Node.js 18+) for more reliable and secure API requests. This would also eliminate the need for shell escaping concerns.
| function getAuthHeader(): string { | |
| return GITHUB_TOKEN ? `-H "Authorization: Bearer ${GITHUB_TOKEN}"` : ""; | |
| } | |
| async function getCommitForRef(owner: string, repo: string, ref: string): Promise<string | null> { | |
| try { | |
| const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${ref}`; | |
| const authHeader = getAuthHeader(); | |
| const { stdout } = await execAsync(`curl -s -w "\n%{http_code}" ${authHeader} "${apiUrl}"`); | |
| const lines = stdout.trim().split("\n"); | |
| const httpCode = lines[lines.length - 1]; | |
| const responseBody = lines.slice(0, -1).join("\n"); | |
| if (httpCode === "403") { | |
| console.error(`\nGitHub API rate limit exceeded!`); | |
| console.error(`Unauthenticated: 60 requests/hour | Authenticated: 5,000 requests/hour`); | |
| console.error(`\nCreate a token at: https://github.com/settings/tokens/new?description=check-outdated-actions&scopes=public_repo`); | |
| console.error(`Then set: export GITHUB_TOKEN="your_token"`); | |
| process.exit(1); | |
| } | |
| if (httpCode !== "200") { | |
| return null; | |
| } | |
| const data = JSON.parse(responseBody); | |
| // Remove getAuthHeader, use fetch directly | |
| async function getCommitForRef(owner: string, repo: string, ref: string): Promise<string | null> { | |
| try { | |
| const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${ref}`; | |
| const headers: Record<string, string> = { | |
| "Accept": "application/vnd.github.v3+json" | |
| }; | |
| if (GITHUB_TOKEN) { | |
| headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`; | |
| } | |
| const response = await fetch(apiUrl, { headers }); | |
| if (response.status === 403) { | |
| console.error(`\nGitHub API rate limit exceeded!`); | |
| console.error(`Unauthenticated: 60 requests/hour | Authenticated: 5,000 requests/hour`); | |
| console.error(`\nCreate a token at: https://github.com/settings/tokens/new?description=check-outdated-actions&scopes=public_repo`); | |
| console.error(`Then set: export GITHUB_TOKEN="your_token"`); | |
| process.exit(1); | |
| } | |
| if (response.status !== 200) { | |
| return null; | |
| } | |
| const data = await response.json(); |
| } catch (e) { | ||
| console.error(`Error reading ${join(folder, e.name)}:`, e); |
Copilot
AI
Dec 11, 2025
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.
The error is caught using the same variable name 'e' as the loop variable from the directory iteration (line 23). This shadowing could cause confusion during debugging. Use a different variable name for the caught error, such as 'error' or 'err'.
| } catch (e) { | |
| console.error(`Error reading ${join(folder, e.name)}:`, e); | |
| } catch (error) { | |
| console.error(`Error reading ${join(folder, e.name)}:`, error); |
| async function getLatestRelease(actionRef: string): Promise<{ tag: string; commit: string } | null> { | ||
| try { | ||
| // Parse the action reference (e.g., "actions/checkout@v5" or "owner/repo/.github/workflows/workflow.yml@v1") | ||
| const [fullAction, currentVersion] = actionRef.split("@"); |
Copilot
AI
Dec 11, 2025
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.
Unused variable currentVersion.
| const [fullAction, currentVersion] = actionRef.split("@"); | |
| const [fullAction] = actionRef.split("@"); |
71c395c to
50b7464
Compare
This PR adds a tool to detect outdated
uses:dependency versions in this repo. (For references to actions and also references to whole workflows.)It also provides a methodical process of how to consider and implment and document each upgrade.
Supply chain security is an important job. And this repo is the start point for supply chain security for most projects on earth.
This script helps make your job easier to benefit us all!
For example:
Pre-requisites
Please note that at this time we are only accepting new starter workflows for Code Scanning. Updates to existing starter workflows are fine.
Tasks
Some general notes:
actionsorganization, or