Dependabot auto-merge #313
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: Dependabot auto-merge | |
| on: | |
| workflow_run: | |
| workflows: ["Quality", "Security", "Docker", "Dockerfile Lint", "Workflow Lint"] | |
| types: [completed] | |
| permissions: | |
| actions: read | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| automerge: | |
| if: github.event.workflow_run.event == 'pull_request' | |
| runs-on: ubuntu-latest | |
| env: | |
| SEEKARR_AUTOMERGE_TOKEN: ${{ secrets.SEEKARR_AUTOMERGE }} | |
| steps: | |
| - name: Validate Dependabot PR + CI success | |
| id: guard | |
| uses: actions/github-script@v9 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| let prNumber; | |
| let headSha; | |
| prNumber = context.payload.workflow_run?.pull_requests?.[0]?.number; | |
| headSha = context.payload.workflow_run?.head_sha; | |
| if (!prNumber || !headSha) { | |
| core.info('No PR context found; skipping.'); | |
| core.setOutput('ok', 'false'); | |
| return; | |
| } | |
| const { data: pr } = await github.rest.pulls.get({ | |
| ...context.repo, | |
| pull_number: prNumber, | |
| }); | |
| if (pr.state !== 'open') { | |
| core.info(`PR #${prNumber} is not open; skipping.`); | |
| core.setOutput('ok', 'false'); | |
| return; | |
| } | |
| if (pr.user?.login !== 'dependabot[bot]') { | |
| core.info(`PR #${prNumber} is not from dependabot[bot]; skipping.`); | |
| core.setOutput('ok', 'false'); | |
| return; | |
| } | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| ...context.repo, | |
| pull_number: prNumber, | |
| per_page: 100, | |
| }); | |
| const filenames = files.map((f) => f.filename); | |
| const hasNonGithubChanges = filenames.some((f) => !f.startsWith('.github/')); | |
| const hasWorkflowChanges = filenames.some((f) => f.startsWith('.github/workflows/')); | |
| core.setOutput('has_workflow_changes', String(hasWorkflowChanges)); | |
| const requiredWorkflows = []; | |
| if (hasNonGithubChanges) { | |
| requiredWorkflows.push('Quality', 'Security', 'Docker', 'Dockerfile Lint'); | |
| } | |
| if (hasWorkflowChanges) { | |
| requiredWorkflows.push('Workflow Lint'); | |
| } | |
| if (requiredWorkflows.length === 0) { | |
| core.info('No required CI workflows matched changed files; skipping.'); | |
| core.setOutput('ok', 'false'); | |
| return; | |
| } | |
| const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { | |
| ...context.repo, | |
| event: 'pull_request', | |
| head_sha: headSha, | |
| per_page: 100, | |
| }); | |
| const latestByName = new Map(); | |
| for (const run of runs) { | |
| const current = latestByName.get(run.name); | |
| if (!current || new Date(run.created_at) > new Date(current.created_at)) { | |
| latestByName.set(run.name, run); | |
| } | |
| } | |
| const missing = []; | |
| const notSuccessful = []; | |
| for (const workflowName of requiredWorkflows) { | |
| const run = latestByName.get(workflowName); | |
| if (!run) { | |
| missing.push(workflowName); | |
| continue; | |
| } | |
| if (run.status !== 'completed' || run.conclusion !== 'success') { | |
| notSuccessful.push(`${workflowName} (${run.status}/${run.conclusion})`); | |
| } | |
| } | |
| if (missing.length || notSuccessful.length) { | |
| core.info(`Missing required workflow runs: ${missing.join(', ') || 'none'}`); | |
| core.info(`Required workflow runs not successful yet: ${notSuccessful.join(', ') || 'none'}`); | |
| core.setOutput('ok', 'false'); | |
| return; | |
| } | |
| const { data: commit } = await github.rest.repos.getCommit({ | |
| ...context.repo, | |
| ref: headSha, | |
| }); | |
| const message = commit.commit.message || ''; | |
| const updateTypeMatch = message.match(/update-type:\s*([^\s]+)/); | |
| const updateType = updateTypeMatch ? updateTypeMatch[1] : ''; | |
| if (!updateType) { | |
| core.info('Could not determine Dependabot update-type from commit message; skipping.'); | |
| core.setOutput('ok', 'false'); | |
| return; | |
| } | |
| core.info(`PR #${prNumber} eligible. update-type=${updateType}`); | |
| core.setOutput('ok', 'true'); | |
| core.setOutput('pr_number', String(prNumber)); | |
| core.setOutput('update_type', updateType); | |
| - name: Auto-approve (patch/minor only) | |
| if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) | |
| uses: hmarr/auto-approve-action@v4 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pull-request-number: ${{ steps.guard.outputs.pr_number }} | |
| - name: Workflow update needs elevated token | |
| if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) && steps.guard.outputs.has_workflow_changes == 'true' && env.SEEKARR_AUTOMERGE_TOKEN == '' | |
| run: | | |
| echo "Dependabot PR touches .github/workflows. GitHub blocks mergePullRequest for app tokens without workflows permission." | |
| echo "Set SEEKARR_AUTOMERGE (classic PAT with repo+workflow scopes or fine-grained token with Workflows write + Pull requests write) to auto-merge these PRs." | |
| - name: Enable auto-merge (squash, workflow updates) | |
| if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) && steps.guard.outputs.has_workflow_changes == 'true' && env.SEEKARR_AUTOMERGE_TOKEN != '' | |
| uses: peter-evans/enable-pull-request-automerge@v3 | |
| with: | |
| token: ${{ env.SEEKARR_AUTOMERGE_TOKEN }} | |
| pull-request-number: ${{ steps.guard.outputs.pr_number }} | |
| merge-method: squash | |
| - name: Enable auto-merge (squash) | |
| if: steps.guard.outputs.ok == 'true' && contains('version-update:semver-patch version-update:semver-minor', steps.guard.outputs.update_type) && steps.guard.outputs.has_workflow_changes != 'true' | |
| uses: peter-evans/enable-pull-request-automerge@v3 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| pull-request-number: ${{ steps.guard.outputs.pr_number }} | |
| merge-method: squash |