Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 13 additions & 72 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
name: automerge

# Repo-level auto-merge that works on BOTH plain repos and merge-queue repos.
#
# Plain repos: enable GitHub auto-merge (squash) so the PR merges when branch
# protection is satisfied.
# Merge-queue repos: GitHub auto-merge does NOT reliably add a PR to the queue,
# so once the PR is mergeable we call the enqueuePullRequest GraphQL mutation
# explicitly. It fires on CI completion via workflow_run (the check-suite
# trigger never re-fires for GITHUB_TOKEN CI) so the PR is actually ready.
#
# Branch protection (required reviews/checks) still fully applies either way.
# Thin caller. All merge logic lives in reusable-automerge.yml (standard: apollo
# docs/standards/github-repo-standard.md Section 8). Only the triggers are repo-specific:
# `workflow_run.workflows` lists THIS repo's CI workflow names. This repo calls the
# reusable workflow by local path so a PR that changes it is exercised on itself before
# it reaches main, where every other repo consumes it via @main.

on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
types: [opened, reopened, synchronize, ready_for_review, closed]
pull_request_review:
types: [submitted]
workflow_run:
Expand All @@ -26,71 +21,17 @@ on:
permissions:
contents: write
pull-requests: write
issues: write
checks: read
statuses: read
actions: read

concurrency:
group: automerge-${{ github.event.pull_request.number || github.event.workflow_run.id }}
cancel-in-progress: false

jobs:
automerge:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const {owner, repo} = context.repo;

// Collect candidate PR numbers from whatever event fired.
let numbers = [];
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_review') {
numbers = [context.payload.pull_request.number];
} else if (context.eventName === 'workflow_run') {
numbers = (context.payload.workflow_run.pull_requests || []).map(p => p.number);
}
if ((context.eventName === 'schedule' || context.eventName === 'workflow_dispatch') && numbers.length === 0) {
// Safety-net sweep: every open, non-draft PR. Recovers a green PR that
// stranded with no pending event to re-fire the paths above (e.g. it fell
// out of the merge queue after enqueue).
const {data} = await github.rest.pulls.list({owner, repo, state: 'open', per_page: 100});
numbers = data.filter(p => !p.draft).map(p => p.number);
}
if (numbers.length === 0) { core.info('no candidate PRs'); return; }

const Q = `query($o:String!,$r:String!,$n:Int!){
repository(owner:$o,name:$r){
mergeQueue{ id }
pullRequest(number:$n){
id number isDraft state merged isInMergeQueue mergeStateStatus
}
}
}`;
const ENQUEUE = `mutation($id:ID!){ enqueuePullRequest(input:{pullRequestId:$id}){ mergeQueueEntry{ position } } }`;
const AUTOMERGE = `mutation($id:ID!){ enablePullRequestAutoMerge(input:{pullRequestId:$id, mergeMethod:SQUASH}){ pullRequest{ number } } }`;
const sleep = ms => new Promise(r => setTimeout(r, ms));

for (const n of numbers) {
// mergeStateStatus is computed async; retry briefly on UNKNOWN.
let data;
for (let i = 0; i < 6; i++) {
data = await github.graphql(Q, {o: owner, r: repo, n});
if (data.repository.pullRequest.mergeStateStatus !== 'UNKNOWN') break;
await sleep(5000);
}
const pr = data.repository.pullRequest;
const hasQueue = !!data.repository.mergeQueue;
if (!pr || pr.state !== 'OPEN' || pr.merged) { core.info(`#${n}: not an open PR`); continue; }
if (pr.isDraft) { core.info(`#${n}: draft, skipping`); continue; }
if (pr.isInMergeQueue) { core.info(`#${n}: already in merge queue`); continue; }

if (hasQueue) {
if (pr.mergeStateStatus === 'CLEAN') {
try { await github.graphql(ENQUEUE, {id: pr.id}); core.info(`#${n}: enqueued`); }
catch (e) { core.info(`#${n}: enqueue not accepted yet (${e.message})`); }
} else {
core.info(`#${n}: mergeStateStatus=${pr.mergeStateStatus}, waiting for a later workflow_run event`);
}
} else {
try { await github.graphql(AUTOMERGE, {id: pr.id}); core.info(`#${n}: auto-merge enabled`); }
catch (e) { core.info(`#${n}: auto-merge enable skipped (${e.message})`); }
}
}
uses: ./.github/workflows/reusable-automerge.yml
with:
runs-on: '"ubuntu-latest"'
Loading
Loading