From 53b13153bc2acfba35e1b78da11b72e6a792d71f Mon Sep 17 00:00:00 2001 From: kazuki nakai Date: Fri, 22 May 2026 19:29:59 +0900 Subject: [PATCH 1/2] ci: auto-enable auto-merge on PRs --- .github/workflows/auto-merge.yml | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/auto-merge.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..459b740 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,66 @@ +name: Auto-merge +on: + pull_request: + types: [opened, reopened, synchronize] + +permissions: + pull-requests: write + contents: write + +jobs: + enable-auto-merge: + runs-on: ubuntu-latest + steps: + - name: Enable auto-merge (merge commit) + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_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 in two cases: + // 1. PR was just opened and checks haven't started yet ("unstable") + // 2. All required checks already passed before this mutation ran + // (breaking change introduced ~March 2026) + // In case 2 GitHub will auto-merge without this mutation, so we + // treat both as non-fatal and retry until the window opens or give up. + 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; + } + } + } From 2f362dac6075eca2e773c047a6cd2697ce96910a Mon Sep 17 00:00:00 2001 From: kazuki nakai Date: Sat, 23 May 2026 09:44:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20fix=20auto-merge=20=E2=80=94=20use=20?= =?UTF-8?q?App=20token=20+=20reusable=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/auto-merge.yml | 40 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 459b740..4e4b460 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -1,20 +1,30 @@ 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] -permissions: - pull-requests: write - contents: write - 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: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.app-token.outputs.token }} script: | const pr = await github.rest.pulls.get({ owner: context.repo.owner, @@ -32,13 +42,10 @@ jobs: } } `; - // Retry with exponential backoff. - // GitHub rejects with UNPROCESSABLE in two cases: - // 1. PR was just opened and checks haven't started yet ("unstable") - // 2. All required checks already passed before this mutation ran - // (breaking change introduced ~March 2026) - // In case 2 GitHub will auto-merge without this mutation, so we - // treat both as non-fatal and retry until the window opens or give up. + // 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 { @@ -51,16 +58,11 @@ jobs: (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 (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; - } + } else { throw e; } } }