diff --git a/.github/workflows/dependabot-auto-merge.yaml b/.github/workflows/dependabot-auto-merge.yaml new file mode 100644 index 0000000..adcb412 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yaml @@ -0,0 +1,119 @@ +name: Dependabot auto-merge +"on": + # Use pull_request_target instead of pull_request to get elevated permissions + # This is safe for Dependabot PRs because: + # 1. We verify the PR author is dependabot[bot] + # 2. We don't check out or run code from the PR + # 3. We only enable auto-merge, which requires branch protection to pass + pull_request_target: + types: [opened, reopened, synchronize] + pull_request_review: + types: [submitted] + check_suite: + types: [completed] + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: >- + (github.event_name == 'pull_request_target' && github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.base.ref == 'main') || (github.event_name == 'pull_request_review' && + github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.base.ref == 'main') || + (github.event_name == 'check_suite' && github.event.check_suite.pull_requests[0] != null && + startsWith(github.event.check_suite.head_branch, 'dependabot/')) + steps: + - name: Get PR details + id: pr + # jq's // empty alternative operator returns an empty string instead of + # the literal "null" when a key is absent or null in the event payload. + # This ensures the downstream empty/null/numeric checks work correctly. + run: | + if [ "${GITHUB_EVENT_NAME}" = "check_suite" ]; then + PR_NUMBER="$(jq -r '.check_suite.pull_requests[0].number // empty' "${GITHUB_EVENT_PATH}")" + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ] || ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "No valid PR number found in check_suite event (got: $PR_NUMBER)" + exit 1 + fi + PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json author,baseRefName) + AUTHOR=$(echo "$PR_JSON" | jq -r '.author.login') + BASE_REF=$(echo "$PR_JSON" | jq -r '.baseRefName') + + { + echo "author=$AUTHOR" + echo "base_ref=$BASE_REF" + echo "number=$PR_NUMBER" + } >> "$GITHUB_OUTPUT" + else + # For pull_request_target and pull_request_review, use event data + { + echo "author=$(jq -r '.pull_request.user.login // empty' "${GITHUB_EVENT_PATH}")" + echo "base_ref=$(jq -r '.pull_request.base.ref // empty' "${GITHUB_EVENT_PATH}")" + echo "number=$(jq -r '.pull_request.number // empty' "${GITHUB_EVENT_PATH}")" + } >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable auto-merge for Dependabot PRs + if: steps.pr.outputs.author == 'dependabot[bot]' && steps.pr.outputs.base_ref == 'main' + shell: bash + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} + # yamllint disable rule:line-length + run: | + set -o pipefail + + attempt_merge() { + gh pr merge --auto --rebase "${PR_NUMBER}" --repo "$GITHUB_REPOSITORY" 2>&1 | tee /tmp/gh-output.txt + } + + if attempt_merge; then + exit 0 + fi + + # Not a real failure: requirements not yet met, or auto-merge already enabled. + if grep -qE "auto-merge is already enabled|[Rr]equired.*status.*check|[Rr]equired approving review|[Rr]equired.*review" /tmp/gh-output.txt; then + echo "Auto-merge not enabled yet - this is expected when requirements are not met or already enabled" + exit 0 + fi + + # The repository-level "Allow auto-merge" setting is off. Re-enable it and retry once. + # This can happen if the setting is accidentally toggled in the repository admin UI. + if grep -qF "Auto merge is not allowed for this repository" /tmp/gh-output.txt; then + echo "Repository Allow auto-merge is disabled; attempting to re-enable..." + if ! gh api "repos/${GITHUB_REPOSITORY}" --method PATCH --field allow_auto_merge=true > /dev/null; then + echo "Could not re-enable Allow auto-merge on the repository. Manual intervention required." + exit 1 + fi + echo "Repository setting re-enabled; retrying..." + if ! attempt_merge; then + echo "Auto-merge still failed after re-enabling repository setting:" + cat /tmp/gh-output.txt + exit 1 + fi + exit 0 + fi + + # Concurrent merge race: another PR merged while this one was being processed, + # moving the base branch. Request a Dependabot rebase to update this branch; + # this workflow re-triggers naturally via check_suite:completed when the rebased + # branch passes checks. With N concurrent Dependabot PRs, this converges in N-1 + # rounds: each round merges one PR, and any PRs that fall behind again each get + # a fresh rebase request on the next trigger. + if grep -qF "Base branch was modified" /tmp/gh-output.txt; then + echo "Base branch was modified by a concurrent merge. Requesting Dependabot rebase..." + gh pr comment "${PR_NUMBER}" --repo "$GITHUB_REPOSITORY" --body "@dependabot rebase" || true + echo "Rebase requested; workflow will re-trigger when the rebased branch passes checks." + exit 0 + fi + + # Unexpected error. + echo "Unexpected error enabling auto-merge:" + cat /tmp/gh-output.txt + exit 1 + # yamllint enable