diff --git a/.github/workflows/auto-merge-ready-prs.yml b/.github/workflows/auto-merge-ready-prs.yml index e35014923f..9d58d0415c 100644 --- a/.github/workflows/auto-merge-ready-prs.yml +++ b/.github/workflows/auto-merge-ready-prs.yml @@ -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); @@ -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: @@ -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: @@ -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: @@ -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=() # ":" for every ready PR: the merge step pins the SHA @@ -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 @@ -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))