Skip to content

Update Action Dependencies #1

Update Action Dependencies

Update Action Dependencies #1

name: Update Action Dependencies
on:
schedule:
# Run weekly on Monday
- cron: '0 9 * * 1'
# Allow manual triggering
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
check-updates:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: Setup Node.js
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
node-version: '18'
- name: Install action-dependency-updater
run: npm install -g @octokit/core js-yaml
- name: Create updater script
run: |
cat > update-actions.js << 'EOL'
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const { Octokit } = require('@octokit/core');
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
async function getLatestCommitSha(owner, repo) {
try {
const response = await octokit.request('GET /repos/{owner}/{repo}/commits', {
owner,
repo,
per_page: 1
});
if (response.data && response.data.length > 0) {
return {
sha: response.data[0].sha,
url: response.data[0].html_url
};
}
return null;
} catch (error) {
console.error(`Error fetching latest commit for ${owner}/${repo}:`, error.message);
return null;
}
}
async function createPullRequest(owner, repo, base, head, title, body, updates) {
try {
const response = await octokit.request('POST /repos/{owner}/{repo}/pulls', {
owner,
repo,
title,
body,
head,
base,
maintainer_can_modify: true
});
if (response.data && response.data.number) {
// Add labels to the pull request
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/labels', {
owner,
repo,
issue_number: response.data.number,
labels: ['dependencies', 'security', 'automated']
});
console.log(`Created PR #${response.data.number}: ${response.data.html_url}`);
}
} catch (error) {
console.error('Error creating PR:', error.message);
}
}
async function processWorkflows() {
const workflowsDir = path.join('.github', 'workflows');
const files = fs.readdirSync(workflowsDir);
let updates = [];
for (const file of files) {
if (!file.endsWith('.yml') && !file.endsWith('.yaml')) continue;
const filePath = path.join(workflowsDir, file);
const content = fs.readFileSync(filePath, 'utf8');
let workflow;
try {
workflow = yaml.load(content);
} catch (error) {
console.error(`Error parsing ${filePath}:`, error.message);
continue;
}
let modified = false;
let newContent = content;
// Find all action references
const actionRegex = /uses:\s+([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)@([a-f0-9]{40})/g;
let match;
while ((match = actionRegex.exec(content)) !== null) {
const [fullMatch, actionPath, currentSha] = match;
const [owner, repo] = actionPath.split('/');
console.log(`Checking ${owner}/${repo} (current: ${currentSha.substring(0, 7)}...)`);
const latest = await getLatestCommitSha(owner, repo);
if (latest && latest.sha !== currentSha) {
console.log(`Update available: ${currentSha.substring(0, 7)}... -> ${latest.sha.substring(0, 7)}...`);
newContent = newContent.replace(
fullMatch,
`uses: ${actionPath}@${latest.sha}`
);
updates.push({
action: actionPath,
file,
from: currentSha.substring(0, 7),
to: latest.sha.substring(0, 7),
commitUrl: latest.url
});
modified = true;
}
}
if (modified) {
fs.writeFileSync(filePath, newContent, 'utf8');
}
}
return updates;
}
async function main() {
// Get repository details from env
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
const branchDate = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const branchName = `deps/action-updates-${branchDate}`;
// Create new branch
try {
const refResponse = await octokit.request('GET /repos/{owner}/{repo}/git/ref/{ref}', {
owner,
repo,
ref: 'heads/main'
});
const mainSha = refResponse.data.object.sha;
// Create a new branch
await octokit.request('POST /repos/{owner}/{repo}/git/refs', {
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: mainSha
});
console.log(`Created branch: ${branchName}`);
} catch (error) {
console.error('Error creating branch:', error.message);
return;
}
// Process workflows and commit changes
const updates = await processWorkflows();
if (updates.length === 0) {
console.log('No updates found.');
return;
}
// Commit changes
try {
await octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', {
owner,
repo,
path: '.github/workflows/release.yml',
message: 'chore: update action dependencies',
content: Buffer.from(fs.readFileSync('.github/workflows/release.yml', 'utf8')).toString('base64'),
branch: branchName,
committer: {
name: 'github-actions[bot]',
email: 'github-actions[bot]@users.noreply.github.com'
}
});
console.log('Committed changes');
} catch (error) {
console.error('Error committing changes:', error.message);
return;
}
// Create PR
const prTitle = `chore: update ${updates.length} action dependencies`;
let prBody = '## Action Dependency Updates\n\n';
prBody += 'This PR updates the following GitHub Actions to their latest versions:\n\n';
for (const update of updates) {
prBody += `- **${update.action}** in \`${update.file}\`\n`;
prBody += ` - \`${update.from}...\` → \`${update.to}...\`\n`;
prBody += ` - [View commit](${update.commitUrl})\n\n`;
}
prBody += '\n\n> This PR was created automatically by the dependency update workflow.';
await createPullRequest(
owner,
repo,
'main',
branchName,
prTitle,
prBody,
updates
);
}
main().catch(error => {
console.error('Unhandled error:', error);
process.exit(1);
});
EOL
chmod +x update-actions.js
- name: Run updater script
run: node update-actions.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}