Update Ansible dependencies: Python 3.14.2→3.14.3, Ansible 13.3.0→13.… #19
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: Auto-Update Dependencies | ||
| # This workflow automatically creates Pull Requests with actual code changes | ||
| # to update dependencies when dependency update issues are created | ||
| on: | ||
| issues: | ||
| types: [opened, edited] | ||
| workflow_dispatch: | ||
| inputs: | ||
| issue_number: | ||
| description: 'Issue number to process' | ||
| required: true | ||
| type: number | ||
| permissions: | ||
| contents: write | ||
| issues: write | ||
| pull-requests: write | ||
| jobs: | ||
| auto-update: | ||
| name: Auto-Update Dependencies | ||
| runs-on: ubuntu-latest | ||
| # Only run for dependency update issues from trusted actors | ||
| if: | | ||
| (github.event_name == 'workflow_dispatch') || | ||
| ( | ||
| contains(github.event.issue.labels.*.name, 'dependencies') && | ||
| contains(github.event.issue.title, 'Update Available') && | ||
| ( | ||
| github.event.issue.user.login == 'github-actions[bot]' || | ||
| github.event.issue.author_association == 'OWNER' || | ||
| github.event.issue.author_association == 'MEMBER' || | ||
| github.event.issue.author_association == 'COLLABORATOR' | ||
| ) | ||
| ) | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Configure Git | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| - name: Parse issue and update dependencies | ||
| uses: actions/github-script@v8 | ||
| env: | ||
| ISSUE_NUMBER: ${{ github.event.inputs.issue_number }} | ||
| with: | ||
| script: | | ||
| const fs = require('fs'); | ||
| const { execSync } = require('child_process'); | ||
| const issueNumberInput = process.env.ISSUE_NUMBER; | ||
| const issueNumber = context.payload.issue?.number || (issueNumberInput ? parseInt(issueNumberInput, 10) : undefined); | ||
| // Get the issue details | ||
| const issue = await github.rest.issues.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber | ||
| }); | ||
| console.log(`Processing issue #${issueNumber}: ${issue.data.title}`); | ||
| const body = issue.data.body; | ||
| const labels = issue.data.labels.map(l => l.name); | ||
| // Search for any PR that references this issue | ||
| const allPRs = await github.rest.pulls.list({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open' | ||
| }); | ||
| const linkedPR = allPRs.data.find(pr => | ||
| pr.body && pr.body.includes(`#${issueNumber}`) | ||
| ); | ||
| if (linkedPR) { | ||
| console.log(`PR #${linkedPR.number} already exists for this issue`); | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber, | ||
| body: `A pull request already exists to address this update: #${linkedPR.number}` | ||
| }); | ||
| return; | ||
| } | ||
| // Extract version information from issue body | ||
| const versionRegex = /\*\*([^:]+)\*\*:\s+([^\s]+)\s+→\s+([^\s]+)/g; | ||
| const updates = {}; | ||
| let match; | ||
| while ((match = versionRegex.exec(body)) !== null) { | ||
| const [, component, currentVersion, latestVersion] = match; | ||
| updates[component.trim()] = { | ||
| current: currentVersion.trim(), | ||
| latest: latestVersion.trim() | ||
| }; | ||
| } | ||
| console.log('Extracted updates:', JSON.stringify(updates, null, 2)); | ||
| // Determine update type and files to modify | ||
| let updateType = ''; | ||
| let branchName = ''; | ||
| let files = []; | ||
| let updatesMade = false; | ||
| if (labels.includes('ansible')) { | ||
| updateType = 'Ansible'; | ||
| branchName = `automated-update/ansible-${Date.now()}`; | ||
| // Update Ansible installer | ||
| const ansibleFile = 'ansible/ansible_installer.sh'; | ||
| let content = fs.readFileSync(ansibleFile, 'utf8'); | ||
| let modified = false; | ||
| if (updates['Python']) { | ||
| const pythonRegex = /BUILD_PYTHON_VERSION:-([0-9.]+)/; | ||
| content = content.replace(pythonRegex, `BUILD_PYTHON_VERSION:-${updates['Python'].latest}`); | ||
| modified = true; | ||
| console.log(`Updated Python version to ${updates['Python'].latest}`); | ||
| } | ||
| if (updates['Ansible']) { | ||
| const ansibleRegex = /pip install ansible==([0-9.]+)/; | ||
| content = content.replace(ansibleRegex, `pip install ansible==${updates['Ansible'].latest}`); | ||
| modified = true; | ||
| console.log(`Updated Ansible version to ${updates['Ansible'].latest}`); | ||
| } | ||
| if (modified) { | ||
| fs.writeFileSync(ansibleFile, content); | ||
| files.push(ansibleFile); | ||
| updatesMade = true; | ||
| } | ||
| } else if (labels.includes('nginx')) { | ||
| updateType = 'NGINX'; | ||
| branchName = `automated-update/nginx-${Date.now()}`; | ||
| // Update NGINX installer (Bash) | ||
| const nginxShFile = 'nginx/nginx_installer.sh'; | ||
| let shContent = fs.readFileSync(nginxShFile, 'utf8'); | ||
| let shModified = false; | ||
| // Update NGINX installer (PowerShell) | ||
| const nginxPs1File = 'nginx/nginx_installer.ps1'; | ||
| let ps1Content = fs.readFileSync(nginxPs1File, 'utf8'); | ||
| let ps1Modified = false; | ||
| if (updates['NGINX']) { | ||
| shContent = shContent.replace(/NGINX_VERSION="([0-9.]+)"/, `NGINX_VERSION="${updates['NGINX'].latest}"`); | ||
| ps1Content = ps1Content.replace(/\$NGINX_VERSION = "([0-9.]+)"/, `$NGINX_VERSION = "${updates['NGINX'].latest}"`); | ||
| shModified = ps1Modified = true; | ||
| console.log(`Updated NGINX version to ${updates['NGINX'].latest}`); | ||
| } | ||
| if (updates['OpenSSL']) { | ||
| shContent = shContent.replace(/OPENSSL_VERSION="([0-9.]+)"/, `OPENSSL_VERSION="${updates['OpenSSL'].latest}"`); | ||
| ps1Content = ps1Content.replace(/\$OPENSSL_VERSION = "([0-9.]+)"/, `$OPENSSL_VERSION = "${updates['OpenSSL'].latest}"`); | ||
| shModified = ps1Modified = true; | ||
| console.log(`Updated OpenSSL version to ${updates['OpenSSL'].latest}`); | ||
| } | ||
| if (updates['PCRE2']) { | ||
| shContent = shContent.replace(/PCRE2_VERSION="([0-9.]+)"/, `PCRE2_VERSION="${updates['PCRE2'].latest}"`); | ||
| ps1Content = ps1Content.replace(/\$PCRE2_VERSION = "([0-9.]+)"/, `$PCRE2_VERSION = "${updates['PCRE2'].latest}"`); | ||
| shModified = ps1Modified = true; | ||
| console.log(`Updated PCRE2 version to ${updates['PCRE2'].latest}`); | ||
| } | ||
| if (updates['Zlib']) { | ||
| shContent = shContent.replace(/ZLIB_VERSION="([0-9.]+)"/, `ZLIB_VERSION="${updates['Zlib'].latest}"`); | ||
| ps1Content = ps1Content.replace(/\$ZLIB_VERSION = "([0-9.]+)"/, `$ZLIB_VERSION = "${updates['Zlib'].latest}"`); | ||
| shModified = ps1Modified = true; | ||
| console.log(`Updated Zlib version to ${updates['Zlib'].latest}`); | ||
| } | ||
| if (shModified) { | ||
| fs.writeFileSync(nginxShFile, shContent); | ||
| files.push(nginxShFile); | ||
| updatesMade = true; | ||
| } | ||
| if (ps1Modified) { | ||
| fs.writeFileSync(nginxPs1File, ps1Content); | ||
| files.push(nginxPs1File); | ||
| updatesMade = true; | ||
| } | ||
| } else if (labels.includes('kubernetes')) { | ||
| updateType = 'Kubernetes'; | ||
| branchName = `automated-update/kubernetes-${Date.now()}`; | ||
| const k8sFile = 'kubernetes/kubernetes_installer.sh'; | ||
| let content = fs.readFileSync(k8sFile, 'utf8'); | ||
| let modified = false; | ||
| if (updates['Kubernetes']) { | ||
| content = content.replace(/K8S_VERSION:-([v0-9.]+)/, `K8S_VERSION:-${updates['Kubernetes'].latest}`); | ||
| modified = true; | ||
| console.log(`Updated Kubernetes version to ${updates['Kubernetes'].latest}`); | ||
| } | ||
| if (modified) { | ||
| fs.writeFileSync(k8sFile, content); | ||
| files.push(k8sFile); | ||
| updatesMade = true; | ||
| } | ||
| } else { | ||
| console.log('Unknown dependency type, skipping PR creation'); | ||
| return; | ||
| } | ||
| if (!updatesMade) { | ||
| console.log('No updates were made to files'); | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber, | ||
| body: `⚠️ Unable to automatically update files. Manual intervention required.\n\nPlease review the issue details and update the files manually.` | ||
| }); | ||
| return; | ||
| } | ||
| // Create a new branch from the currently checked-out default branch | ||
| try { | ||
| execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' }); | ||
| console.log(`Created branch ${branchName}`); | ||
| } catch (error) { | ||
| console.error('Failed to create branch:', error.message); | ||
| throw error; | ||
| } | ||
| // Commit changes | ||
| try { | ||
| execSync(`git add ${files.join(' ')}`, { stdio: 'inherit' }); | ||
| const commitMessage = `chore: update ${updateType} dependencies | ||
| ${Object.entries(updates).map(([comp, vers]) => `- ${comp}: ${vers.current} → ${vers.latest}`).join('\n')} | ||
| Automated update from issue #${issueNumber}`; | ||
| execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); | ||
| console.log('Committed changes'); | ||
| } catch (error) { | ||
| console.error('Failed to commit:', error.message); | ||
| throw error; | ||
| } | ||
| // Push the branch | ||
| try { | ||
| execSync(`git push -u origin ${branchName}`, { stdio: 'inherit' }); | ||
| console.log('Pushed branch'); | ||
| } catch (error) { | ||
| console.error('Failed to push:', error.message); | ||
| throw error; | ||
| } | ||
| // Create PR body | ||
| const prBody = `## Automated Dependency Update | ||
| This PR automatically updates ${updateType} dependencies as identified in issue #${issueNumber}. | ||
| ### Changes Made | ||
| ${Object.entries(updates).map(([component, versions]) => | ||
| `- **${component}**: ${versions.current} → ${versions.latest}` | ||
| ).join('\n')} | ||
| ### Files Updated | ||
| ${files.map(f => `- \`${f}\``).join('\n')} | ||
| ### ⚠️ Important Notes | ||
| ${updateType === 'NGINX' ? ` | ||
| **NGINX requires SHA256 checksum updates:** | ||
| After reviewing this PR, you'll need to: | ||
| 1. Download the new NGINX tarball and calculate its SHA256: | ||
| \`\`\`bash | ||
| wget https://nginx.org/download/nginx-${updates['NGINX']?.latest}.tar.gz | ||
| sha256sum nginx-${updates['NGINX']?.latest}.tar.gz | ||
| \`\`\` | ||
| 2. Update the SHA256 checksums in both installer files | ||
| 3. Test the installation on a clean system | ||
| **The PR cannot be merged until SHA256 checksums are updated.** | ||
| ` : ''} | ||
| ### Testing Checklist | ||
| - [ ] Version numbers updated correctly | ||
| ${updateType === 'NGINX' ? '- [ ] SHA256 checksums updated and verified' : ''} | ||
| - [ ] Installation tested on clean system | ||
| - [ ] All functionality verified | ||
| ### Verification | ||
| Test the installation: | ||
| \`\`\`bash | ||
| ${files[0].includes('ansible') ? './ansible/ansible_installer.sh' : | ||
| files[0].includes('nginx') ? './nginx/nginx_installer.sh' : | ||
| files[0].includes('kubernetes') ? './kubernetes/kubernetes_installer.sh' : './installer.sh'} | ||
| \`\`\` | ||
| --- | ||
| *🤖 This PR was automatically created by the dependency management workflow.* | ||
| *Related issue: #${issueNumber}* | ||
| Closes #${issueNumber} | ||
| `; | ||
| // Create the pull request | ||
| try { | ||
| const pr = await github.rest.pulls.create({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| title: `🔄 Update ${updateType} Dependencies`, | ||
| head: branchName, | ||
| base: context.payload.repository.default_branch, | ||
| body: prBody, | ||
| draft: updateType === 'NGINX' // Mark as draft if NGINX (needs SHA256 updates) | ||
| }); | ||
| console.log(`Created PR #${pr.data.number}`); | ||
| // Add labels to PR | ||
| await github.rest.issues.addLabels({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: pr.data.number, | ||
| labels: ['dependencies', 'automated', ...labels.filter(l => !['enhancement', 'dependencies'].includes(l))] | ||
| }); | ||
| // Add comment to original issue with PR link | ||
| const commentBody = updateType === 'NGINX' | ||
| ? `🤖 **Automated PR Created** | ||
| A pull request has been created with automated dependency updates: #${pr.data.number} | ||
| ⚠️ **Action Required:** The PR is marked as draft because NGINX updates require SHA256 checksum verification. Please: | ||
| 1. Review the version updates | ||
| 2. Download and verify SHA256 checksums | ||
| 3. Update the checksums in the installer files | ||
| 4. Mark the PR as ready for review` | ||
| : `🤖 **Automated PR Created** | ||
| A pull request has been created with automated dependency updates: #${pr.data.number} | ||
| The changes have been automatically applied. Please review and test before merging.`; | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber, | ||
| body: commentBody | ||
| }); | ||
| console.log(`Successfully created PR and linked to issue #${issueNumber}`); | ||
| } catch (error) { | ||
| console.error('Error creating PR:', error); | ||
| // Comment on issue about the error | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: issueNumber, | ||
| body: `⚠️ **Automated PR Creation Failed** | ||
| There was an error creating the automated pull request. Error: ${error.message} | ||
| The branch \`${branchName}\` may have been created with updates. Please check and create a pull request manually if needed.` | ||
| }); | ||
| throw error; | ||
| } | ||