Update Action Dependencies #1
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
| 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 }} |