Skip to content

Instantly share code, notes, and snippets.

@dobladov
Last active January 23, 2026 18:40
Show Gist options
  • Select an option

  • Save dobladov/ca10840ca830e687f4ff7c3c414dbad7 to your computer and use it in GitHub Desktop.

Select an option

Save dobladov/ca10840ca830e687f4ff7c3c414dbad7 to your computer and use it in GitHub Desktop.
Check GitHub actions updates
#!/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