CHORE: Bump actions/setup-python from 5 to 6 #7
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: Merge Bot | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| merge-command: | |
| name: Handle Merge Command | |
| runs-on: ubuntu-latest | |
| # Only run on PR comments | |
| if: github.event.issue.pull_request | |
| steps: | |
| - name: Parse command | |
| id: command | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const comment = context.payload.comment.body.toLowerCase().trim(); | |
| const user = context.payload.comment.user.login; | |
| core.info(`Comment from ${user}: ${comment}`); | |
| // Check for merge command (case insensitive) | |
| // Supported formats: | |
| // - @mergebot merge | |
| // - @merge-bot merge | |
| // - /merge | |
| // - merge (if alone) | |
| const mergePatterns = [ | |
| /@merge-?bot\s+merge/i, | |
| /^\/merge$/i, | |
| /^merge$/i, | |
| ]; | |
| const isMergeCommand = mergePatterns.some(pattern => pattern.test(comment)); | |
| if (!isMergeCommand) { | |
| core.info('Not a merge command, skipping'); | |
| return; | |
| } | |
| core.setOutput('should_merge', 'true'); | |
| core.info('✅ Merge command detected'); | |
| - name: React to comment | |
| if: steps.command.outputs.should_merge == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Add rocket emoji to show bot is processing | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket' | |
| }); | |
| - name: Check permissions | |
| if: steps.command.outputs.should_merge == 'true' | |
| id: check_perms | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const user = context.payload.comment.user.login; | |
| // Check if user has write access | |
| try { | |
| const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: user | |
| }); | |
| const hasPermission = ['admin', 'write'].includes(permission.permission); | |
| if (!hasPermission) { | |
| core.setFailed(`❌ @${user} does not have permission to merge (permission: ${permission.permission})`); | |
| // Comment on PR | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `❌ @${user} you don't have permission to merge PRs. Only collaborators with write access can use merge commands.` | |
| }); | |
| return; | |
| } | |
| core.info(`✅ User ${user} has ${permission.permission} access`); | |
| core.setOutput('has_permission', 'true'); | |
| } catch (error) { | |
| core.setFailed(`Error checking permissions: ${error.message}`); | |
| } | |
| - name: Get PR info | |
| if: steps.check_perms.outputs.has_permission == 'true' | |
| id: pr_info | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.issue.number | |
| }); | |
| core.setOutput('mergeable', pr.mergeable); | |
| core.setOutput('draft', pr.draft); | |
| core.setOutput('sha', pr.head.sha); | |
| core.setOutput('title', pr.title); | |
| core.info(`PR #${pr.number}: ${pr.title}`); | |
| core.info(`- Mergeable: ${pr.mergeable}`); | |
| core.info(`- Draft: ${pr.draft}`); | |
| core.info(`- SHA: ${pr.head.sha}`); | |
| - name: Check if PR is ready | |
| if: steps.check_perms.outputs.has_permission == 'true' | |
| id: ready | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const mergeable = '${{ steps.pr_info.outputs.mergeable }}'; | |
| const draft = '${{ steps.pr_info.outputs.draft }}'; | |
| if (draft === 'true') { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: '❌ Cannot merge: PR is still a draft. Please mark it as ready for review first.' | |
| }); | |
| core.setFailed('PR is draft'); | |
| return; | |
| } | |
| if (mergeable === 'false') { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: '❌ Cannot merge: PR has conflicts or is not mergeable. Please resolve conflicts first.' | |
| }); | |
| core.setFailed('PR not mergeable'); | |
| return; | |
| } | |
| core.setOutput('ready', 'true'); | |
| - name: Wait for checks | |
| if: steps.ready.outputs.ready == 'true' | |
| id: wait_checks | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const sha = '${{ steps.pr_info.outputs.sha }}'; | |
| const maxWaitMinutes = 10; | |
| const pollInterval = 10000; // 10 seconds | |
| const maxAttempts = (maxWaitMinutes * 60 * 1000) / pollInterval; | |
| let attempt = 0; | |
| while (attempt < maxAttempts) { | |
| const { data: checks } = await github.rest.checks.listForRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: sha | |
| }); | |
| const pendingChecks = checks.check_runs.filter( | |
| check => check.status !== 'completed' | |
| ); | |
| const failedChecks = checks.check_runs.filter( | |
| check => check.conclusion === 'failure' || check.conclusion === 'cancelled' | |
| ); | |
| if (failedChecks.length > 0) { | |
| const failedNames = failedChecks.map(c => c.name).join(', '); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `❌ Cannot merge: The following checks failed:\n\n${failedChecks.map(c => `- ❌ ${c.name}`).join('\n')}\n\nPlease fix the issues and try again.` | |
| }); | |
| core.setFailed(`Failed checks: ${failedNames}`); | |
| return; | |
| } | |
| if (pendingChecks.length === 0) { | |
| core.info('✅ All checks passed'); | |
| core.setOutput('checks_passed', 'true'); | |
| return; | |
| } | |
| const pendingNames = pendingChecks.map(c => c.name).join(', '); | |
| core.info(`Waiting for checks: ${pendingNames} (attempt ${attempt + 1}/${maxAttempts})`); | |
| await new Promise(resolve => setTimeout(resolve, pollInterval)); | |
| attempt++; | |
| } | |
| // Timeout | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `⏱️ Timeout waiting for checks to complete (waited ${maxWaitMinutes} minutes).\n\nYou can try the merge command again once checks complete.` | |
| }); | |
| core.setFailed('Timeout waiting for checks'); | |
| - name: Merge PR | |
| if: steps.wait_checks.outputs.checks_passed == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const user = context.payload.comment.user.login; | |
| const title = '${{ steps.pr_info.outputs.title }}'; | |
| try { | |
| await github.rest.pulls.merge({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.issue.number, | |
| merge_method: 'squash', | |
| commit_title: title, | |
| commit_message: '' | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `✅ PR merged successfully by @${user}!` | |
| }); | |
| // Add +1 reaction to original comment | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: '+1' | |
| }); | |
| core.info('✅ PR merged successfully'); | |
| } catch (error) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `❌ Failed to merge PR: ${error.message}` | |
| }); | |
| core.setFailed(`Merge failed: ${error.message}`); | |
| } | |
| - name: Delete branch | |
| if: steps.wait_checks.outputs.checks_passed == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.issue.number | |
| }); | |
| // Don't delete main or protected branches | |
| if (pr.head.ref === 'main' || pr.head.ref.startsWith('release/')) { | |
| core.info('Skipping branch deletion for protected branch'); | |
| return; | |
| } | |
| try { | |
| await github.rest.git.deleteRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${pr.head.ref}` | |
| }); | |
| core.info(`🗑️ Deleted branch: ${pr.head.ref}`); | |
| } catch (error) { | |
| core.warning(`Failed to delete branch: ${error.message}`); | |
| } |