Last active
January 23, 2026 18:40
-
-
Save dobladov/ca10840ca830e687f4ff7c3c414dbad7 to your computer and use it in GitHub Desktop.
Check GitHub actions updates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| import fs from 'node:fs/promises'; | |
| import {styleText} from 'node:util'; | |
| import { argv } from 'node:process'; | |
| const WORKFLOWS_DIR = argv[2] ?? '.github'; | |
| const getLatestVersionInfo = async (repos) => { | |
| if (repos.length === 0 || !process.env.GITHUB_TOKEN) { | |
| if (!process.env.GITHUB_TOKEN) { | |
| console.warn('Warning: GITHUB_TOKEN is not set. GraphQL API requires authentication.'); | |
| } | |
| return new Map(); | |
| } | |
| const query = `query {${repos.map((repo, i) => { | |
| const [owner, name] = repo.split('/'); | |
| return `repo_${i}: repository(owner: "${owner}", name: "${name}") { | |
| latestRelease { tagName } | |
| refs(refPrefix: "refs/tags/", first: 1, orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) { | |
| nodes { name } | |
| } | |
| }`; | |
| }).join('\n')}}`; | |
| try { | |
| const response = await fetch('https://api.github.com/graphql', { | |
| body: JSON.stringify({query}), | |
| headers: { | |
| Authorization: `bearer ${process.env.GITHUB_TOKEN}`, | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'GitHub-Action-Update-Checker', | |
| }, | |
| method: 'POST', | |
| }); | |
| if (!response.ok) { | |
| return new Map(); | |
| } | |
| const {data} = await response.json(); | |
| return new Map(repos.map((repo, i) => { | |
| const repoData = data?.[`repo_${i}`]; | |
| return [repo, repoData ? { | |
| latestRelease: repoData.latestRelease?.tagName || null, | |
| latestTag: repoData.refs?.nodes[0]?.name || null, | |
| } : null]; | |
| })); | |
| } catch (error) { | |
| console.error('Error fetching data from GitHub GraphQL API:', error); | |
| return new Map(); | |
| } | |
| }; | |
| /** | |
| * @param {string} current | |
| * @param {string} latest | |
| * @returns {'major' | 'minor' | 'patch'} | |
| */ | |
| const getUpdateType = (current, latest) => { | |
| /** @param {string} v */ | |
| const parseVersion = (v) => { | |
| const match = v.match(/v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/); | |
| if (!match) { | |
| return null; | |
| } | |
| return { | |
| major: parseInt(match[1], 10), | |
| minor: match[2] ? parseInt(match[2], 10) : 0, | |
| patch: match[3] ? parseInt(match[3], 10) : 0, | |
| }; | |
| }; | |
| const currentVer = parseVersion(current); | |
| const latestVer = parseVersion(latest); | |
| if (!currentVer || !latestVer) { | |
| return 'minor'; | |
| } | |
| if (latestVer.major > currentVer.major) { | |
| return 'major'; | |
| } | |
| if (latestVer.minor > currentVer.minor) { | |
| return 'minor'; | |
| } | |
| return 'patch'; | |
| }; | |
| const checkActions = async () => { | |
| try { | |
| const yamlFiles = await Array.fromAsync(fs.glob(`${WORKFLOWS_DIR}/**/*.{yml,yaml}`)); | |
| if (yamlFiles.length === 0) { | |
| console.log(`No workflow files found in ${WORKFLOWS_DIR}`); | |
| return; | |
| } | |
| const actionUsage = new Map(); | |
| const contents = await Promise.all(yamlFiles.map((file) => fs.readFile(file, 'utf8'))); | |
| for (const content of contents) { | |
| for (const [, fullPath, version] of content.matchAll(/uses:\s*([\w\-./]+)@([\w\-.]+)/g)) { | |
| if (fullPath.startsWith('./') || fullPath.startsWith('docker://')) { | |
| continue; | |
| } | |
| const parts = fullPath.split('/'); | |
| if (parts.length < 2) { | |
| continue; | |
| } | |
| const repo = `${parts[0]}/${parts[1]}`; | |
| if (!actionUsage.has(repo)) { | |
| actionUsage.set(repo, new Set()); | |
| } | |
| actionUsage.get(repo).add(version); | |
| } | |
| } | |
| if (actionUsage.size === 0) { | |
| console.log('No external GitHub Actions were found.'); | |
| return; | |
| } | |
| console.log(`Gathered ${actionUsage.size} unique actions. Checking for updates...\n`); | |
| const infoMap = await getLatestVersionInfo([...actionUsage.keys()]); | |
| const results = []; | |
| for (const [repo, versions] of actionUsage) { | |
| const info = infoMap.get(repo); | |
| const latest = info?.latestRelease || info?.latestTag || 'Unknown'; | |
| for (const current of versions) { | |
| let isUpToDate = current === info?.latestRelease || current === info?.latestTag; | |
| if (!isUpToDate && latest !== 'Unknown' && /^v?\d+$/.test(current)) { | |
| const norm = (v) => (v.startsWith('v') ? v : `v${v}`); | |
| isUpToDate = norm(latest).startsWith(`${norm(current)}.`); | |
| } | |
| if (!info || !isUpToDate) { | |
| results.push({ | |
| Action: repo, | |
| Current: current, | |
| Latest: latest, | |
| URL: `https://github.com/${repo}`, | |
| }); | |
| } | |
| } | |
| } | |
| results.sort((a, b) => a.Action.localeCompare(b.Action)); | |
| if (results.length > 0) { | |
| const maxActionLen = Math.max(...results.map((r) => r.Action.length)); | |
| const maxCurrentLen = Math.max(...results.map((r) => r.Current.length)); | |
| const maxLatestLen = Math.max(...results.map((r) => r.Latest.length)); | |
| for (const r of results) { | |
| const action = r.Action.padEnd(maxActionLen); | |
| const current = r.Current.padEnd(maxCurrentLen); | |
| const latest = r.Latest.padEnd(maxLatestLen); | |
| const updateType = getUpdateType(r.Current, r.Latest); | |
| const colorMap = { | |
| major: 'red', | |
| minor: 'yellow', | |
| patch: 'gray', | |
| }; | |
| const actionColor = colorMap[updateType]; | |
| console.log(`${styleText(actionColor, action)} ${current} ❯ ${styleText('green', latest)} ${styleText('blue', r.URL)}`); | |
| } | |
| console.log(`\n📢 Found ${results.length} action(s) with potential updates.`); | |
| console.log('Check the repository releases to see if you should update your pinned version.'); | |
| } else { | |
| console.log('\n✨ All actions appear to be up to date!'); | |
| } | |
| } catch (err) { | |
| const error = /** @type {Error & {code?: string}} */ (err); | |
| if (error.code === 'ENOENT') { | |
| console.error(`Error: Directory not found at ${WORKFLOWS_DIR}`); | |
| } else { | |
| console.error('An unexpected error occurred:', error.message); | |
| } | |
| } | |
| }; | |
| checkActions(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment