name: Dependabot Auto Merge on: workflow_run: workflows: - CI types: - completed concurrency: group: dependabot-auto-merge-${{ github.event.workflow_run.head_branch }} cancel-in-progress: true permissions: contents: write pull-requests: write jobs: merge: name: Auto-merge Dependabot PRs if: ${{ github.event.workflow_run.event == 'pull_request' }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Find matching Dependabot PR id: pr uses: actions/github-script@v9 with: script: | const branch = context.payload.workflow_run.head_branch; const { owner, repo } = context.repo; const { data: pulls } = await github.rest.pulls.list({ owner, repo, state: "open", head: `${owner}:${branch}`, per_page: 10, }); const pr = pulls.find((item) => item.user?.login === "dependabot[bot]"); if (!pr) { core.info(`No open Dependabot PR found for branch ${branch}.`); core.setOutput("should_merge", "false"); return; } core.setOutput("should_merge", "true"); core.setOutput("number", String(pr.number)); - name: Merge PR when checks pass if: steps.pr.outputs.should_merge == 'true' uses: actions/github-script@v9 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const pull_number = Number("${{ steps.pr.outputs.number }}"); const isSuccessful = (status) => status === "SUCCESS" || status === "SKIPPED" || status === "NEUTRAL"; const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const getPullRequest = async () => { const { data } = await github.rest.pulls.get({ owner, repo, pull_number, }); return data; }; let pr = await getPullRequest(); if (pr.user?.login !== "dependabot[bot]") { core.info(`PR #${pull_number} is no longer a Dependabot PR.`); return; } if (pr.draft) { core.info(`PR #${pull_number} is still a draft.`); return; } for (let attempt = 1; attempt <= 6 && pr.mergeable === null; attempt += 1) { core.info(`PR #${pull_number} mergeability is still being computed; retry ${attempt}/6.`); await wait(5000); pr = await getPullRequest(); } if (pr.mergeable !== true) { core.info(`PR #${pull_number} is not currently mergeable; skipping auto-merge.`); return; } const { data: checks } = await github.rest.checks.listForRef({ owner, repo, ref: pr.head.sha, }); const requiredChecks = ["Lint", "Test and Coverage", "Type Check"]; const checkMap = new Map(checks.check_runs.map((run) => [run.name, run])); const missing = requiredChecks.filter((name) => !checkMap.has(name)); if (missing.length > 0) { core.info(`PR #${pull_number} is missing checks: ${missing.join(", ")}.`); return; } const failed = requiredChecks.filter((name) => { const run = checkMap.get(name); return run.status !== "completed" || !isSuccessful(String(run.conclusion).toUpperCase()); }); if (failed.length > 0) { core.info(`PR #${pull_number} still has pending or failing checks: ${failed.join(", ")}.`); return; } try { await github.rest.pulls.merge({ owner, repo, pull_number, merge_method: "merge", }); } catch (error) { if (error.status === 405 || error.status === 409) { core.info(`PR #${pull_number} could not be merged right now: ${error.message}`); return; } throw error; } await github.rest.git.deleteRef({ owner, repo, ref: `heads/${pr.head.ref}`, }); core.info(`Merged Dependabot PR #${pull_number} and deleted ${pr.head.ref}.`);