Skip to content
Merged
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
64 changes: 32 additions & 32 deletions .github/workflows/auto-merge-ready-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ name: Auto Merge Ready PRs
# decides what tools/queen/publish.py opens as a pull request; it never arms
# or performs a merge (#5525).
#
# RED CHECKS THE BEE ANSWERED FOR. Measured 2026-10-03: this workflow merged
# nothing since 2026-09-20 -- all 300 merges in that window were by hand --
# because it wanted EVERY check green, while `spec-guards` is red on master
# itself and every open bee pull request carries three to six red advisory
# checks. So a red check passes here only when ALL of these hold:
# RED CHECKS THE BEE ANSWERED FOR. A red check passes here only when ALL hold:
# - it is not a required check of the base branch's ruleset (those must be
# green: no review can discount them, and an unreadable ruleset makes
# every check required);
Expand All @@ -40,29 +36,15 @@ name: Auto Merge Ready PRs
# discount_holds).

on:
# ON A SCHEDULE, because the thing that used to call it was a laptop.
#
# The batch merge ran as a local scheduled task every 30 minutes. Measured
# 2026-09-19: every run since 07:39 the previous day had failed in three
# seconds on the session model the app asked for, and nobody saw it - the
# script logs nothing when it never starts. Meanwhile 52 bee pull requests
# sat green and unmerged. A merge job that lives on one machine is a merge
# job that stops when that machine does.
#
# Twenty minutes rather than five: every merge puts the remaining branches
# behind master, and their checks re-run.
# Backstop only: GitHub started this cron about every six hours, not every
# twenty minutes (measured 2026-10-04..06); a laptop-run merger failed unseen.
schedule:
- cron: '*/20 * * * *'
# AND ON THE BEE'S WORD. The bot applies `bee-reviewed` with an app token,
# and app-token events do start workflows, so a reviewed pull request is
# looked at within a minute. The schedule alone is not enough: measured
# 2026-10-04..06, GitHub started this cron about every six hours (02:19Z,
# 09:13Z, 18:28Z, 00:16Z, 06:25Z), not every twenty minutes. Only the base
# branch's copy of this file runs on this event, and the only thing read
# from the payload is the pull request's number: the gate below re-reads
# everything else from the API, exactly as on the schedule.
# One pull request per event: the bee's label, or a finished required check
# (#10396). Only its number is read; the gate re-reads the rest from the API.
pull_request_target:
types: [labeled]
workflow_run: {workflows: ["Schema Validation", "Issue Gate", "Spec Parse Ratchet", "Own Language"], types: [completed]}
workflow_dispatch:
inputs:
reason:
Expand All @@ -75,10 +57,10 @@ on:
type: boolean
default: false

# One run at a time. Two batches merging the same pull request is how a merge
# fails for a reason that has nothing to do with the pull request.
# One run at a time per pull request (a global group drops pending runs: GitHub
# keeps one pending per group). Sweeps share `sweep`; the merge step tolerates "already merged".
concurrency:
group: auto-merge-ready-prs
group: auto-merge-${{ github.event_name == 'workflow_run' && (github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.head_sha) || github.event_name == 'pull_request_target' && github.event.pull_request.number || 'sweep' }}
cancel-in-progress: false

permissions:
Expand All @@ -88,12 +70,20 @@ permissions:
jobs:
auto-merge:
# Any other label is not the bee speaking; skip without a run's worth of work.
if: github.event_name != 'pull_request_target' || github.event.label.name == 'bee-reviewed'
if: (github.event_name != 'pull_request_target' || github.event.label.name == 'bee-reviewed') && (github.event_name != 'workflow_run' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled'))
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6

# workflow_run: the pull request of the finished check (head sha when GitHub gives a fork run none); "none" judges nothing.
- id: resolve
if: github.event_name == 'workflow_run'
env: {GH_TOKEN: "${{ github.token }}", R: "${{ github.repository }}", N: "${{ github.event.workflow_run.pull_requests[0].number }}", S: "${{ github.event.workflow_run.head_sha }}"}
run: |
[ -n "$N" ] || N=$(gh api "repos/$R/commits/$S/pulls" | jq -r --arg s "$S" '[.[]|select(.state=="open" and .base.ref=="master" and .head.sha==$s)]|if length==1 then .[0].number else empty end') || N=
echo "pr=${N:-none}" >> "$GITHUB_OUTPUT"

- name: Find ready PRs
id: find-ready
env:
Expand All @@ -109,7 +99,7 @@ jobs:
SELF_WORKFLOW: ${{ github.workflow }}
# The labelled pull request when the bee's label started this run;
# empty on the schedule and on a manual run.
EVENT_PR: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || '' }}
EVENT_PR: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || steps.resolve.outputs.pr || '' }}
run: |
READY_PRS=()
# "<pr>:<head sha>" for every ready PR: the merge step pins the SHA
Expand Down Expand Up @@ -409,12 +399,22 @@ jobs:
echo "PRs: ${{ steps.find-ready.outputs.ready_prs }}"
exit 0

# Merge as TRI CLAW: github.token's events start no workflow, an App's do. The token is minted for the
# "merge-pr" row of specs/claw/github_surface.t27 (contents write, pull_requests write). No secret -> github.token.
- id: have
if: ${{ steps.find-ready.outputs.count != '0' && !inputs.dry_run }}
env: {K: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", I: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}"}
run: if [ -n "$K" ] && [ -n "$I" ]; then echo ok=1 >> "$GITHUB_OUTPUT"; fi
- {id: tok, if: steps.have.outputs.ok, continue-on-error: true, uses: actions/create-github-app-token@v2, with: {app-id: "${{ vars.CLAW_APP_ID || secrets.CLAW_APP_ID }}", private-key: "${{ secrets.CLAW_APP_PRIVATE_KEY }}", owner: gHashTag, repositories: t27, permission-contents: write, permission-pull-requests: write}}

- name: Merge Ready PRs
if: ${{ steps.find-ready.outputs.count != '0' && !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ steps.tok.outputs.token || github.token }}
AS: ${{ steps.tok.outputs.token && 'TRI CLAW' || 'github.token (the merge starts no downstream workflow; CLAW_APP_PRIVATE_KEY unset or not mintable)' }}
READY_HEADS: ${{ steps.find-ready.outputs.ready_heads }}
run: |
echo "claw/merge: merging as $AS" >> "$GITHUB_STEP_SUMMARY"
echo "Merging ${{ steps.find-ready.outputs.count }} PRs..."
MERGED=0
FAILED=0
Expand All @@ -431,10 +431,10 @@ jobs:
# --match-head-commit: the head that was judged above. A push that
# lands between the gate and this line is a head no bee reviewed;
# GitHub refuses the merge instead of taking it (#5547 review).
# `|| echo` made this step exit 0 whether every merge succeeded or
# every one failed: a batch where nothing merged reported green.
if gh pr merge "$pr" --merge --delete-branch --match-head-commit "$sha"; then
MERGED=$((MERGED + 1))
elif [ "$(gh pr view "$pr" --json state --jq .state)" = MERGED ]; then
echo " PR #$pr was merged by another run"
else
echo " Failed to merge PR #$pr"
FAILED=$((FAILED + 1))
Expand Down
Loading