diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..4e4b460 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,68 @@ +name: Auto-merge + +# Dual trigger: +# - `workflow_call` lets other repos call this as a reusable workflow. +# - `pull_request` auto-merges .github's own PRs using the same logic. +on: + workflow_call: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + enable-auto-merge: + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App installation token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.AUTOMERGE_APP_CLIENT_ID }} + private-key: ${{ secrets.AUTOMERGE_APP_PRIVATE_KEY }} + # Least-privilege: only the permissions needed to enable auto-merge. + permission-contents: write + permission-pull-requests: write + - name: Enable auto-merge (merge commit) + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + const nodeId = pr.data.node_id; + const mutation = ` + mutation EnableAutoMerge($id: ID!) { + enablePullRequestAutoMerge(input: { + pullRequestId: $id, + mergeMethod: MERGE + }) { + pullRequest { number autoMergeRequest { mergeMethod } } + } + } + `; + // Retry with exponential backoff. GitHub rejects with UNPROCESSABLE + // while a PR is still "unstable" (checks not started yet); since + // ~Mar 2026 it also rejects when all required checks already passed + // (GitHub auto-merges anyway in that case, so we treat it as success). + const maxAttempts = 6; + for (let i = 0; i < maxAttempts; i++) { + try { + await github.graphql(mutation, { id: nodeId }); + console.log('Auto-merge enabled.'); + break; + } catch (e) { + const msg = e.message ?? ''; + const isUnstable = msg.includes('unstable') || + (e.errors ?? []).some(err => (err.message ?? '').includes('unstable')); + const alreadyEnabled = msg.includes('already enabled') || + (e.errors ?? []).some(err => (err.message ?? '').includes('already enabled')); + if (alreadyEnabled) { console.log('Auto-merge already enabled, skipping.'); break; } + if (isUnstable && i < maxAttempts - 1) { + const delay = Math.min(5000 * 2 ** i, 30000); + console.log(`PR unstable (attempt ${i + 1}/${maxAttempts}), retrying in ${delay}ms…`); + await new Promise(r => setTimeout(r, delay)); + } else { throw e; } + } + }