Merge Conflict Notifier #18
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 Conflict Notifier | |
| on: | |
| schedule: | |
| - cron: '*/30 * * * *' # Hourly/half-hourly fallback | |
| push: | |
| branches: | |
| - main | |
| pull_request_target: | |
| types: [synchronize, reopened, edited] | |
| workflow_dispatch: | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| check-conflicts: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check open PRs for merge conflicts | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const label = 'needs-rebase'; | |
| // Ensure the label exists in the repo | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| }); | |
| } catch { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| color: 'e11d48', | |
| description: 'This PR has merge conflicts and needs a rebase.', | |
| }); | |
| } | |
| // Fetch all open PRs | |
| const { data: openPRs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100, | |
| }); | |
| for (const pr of openPRs) { | |
| // GitHub computes `mergeable` lazily — fetch each PR individually with retries to trigger and wait for computation | |
| let fullPR = null; | |
| for (let attempt = 1; attempt <= 3; attempt++) { | |
| const { data } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| }); | |
| fullPR = data; | |
| if (fullPR.mergeable !== null) { | |
| break; | |
| } | |
| core.info(`PR #${pr.number}: mergeable is null, waiting 5 seconds (attempt ${attempt}/3)...`); | |
| await new Promise(resolve => setTimeout(resolve, 5000)); | |
| } | |
| const isConflicting = fullPR.mergeable === false; | |
| const currentLabels = fullPR.labels.map(l => l.name); | |
| const alreadyLabeled = currentLabels.includes(label); | |
| if (isConflicting) { | |
| // Apply label if not already applied | |
| if (!alreadyLabeled) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: [label], | |
| }); | |
| // Post notification comment ONLY ONCE (when the label is first applied) | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: `⚠️ Hey @${fullPR.user.login}, this PR has merge conflicts with the \`main\` branch. | |
| Please pull the latest changes and resolve the conflicts so we can review it! | |
| \`\`\`bash | |
| git fetch origin | |
| git rebase origin/main | |
| # resolve any conflicts, then: | |
| git push --force-with-lease | |
| \`\`\` | |
| Once resolved, the \`needs-rebase\` label will be removed automatically on the next check. 🙌`, | |
| }); | |
| } | |
| } else if (!isConflicting && alreadyLabeled) { | |
| // PR is no longer conflicting — remove the stale label | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| name: label, | |
| }); | |
| } | |
| } |