merge-queue #5273
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Custom merge queue — see .github/scripts/merge-queue.js for the full design. | |
| # | |
| # Event-driven with a scheduled safety net (the loop is stateless, so a missed event costs one | |
| # cycle at most): | |
| # - pull_request_target: instant reaction to joining/leaving the queue, pushes to queued PRs, and | |
| # `merge-queue:priority` label changes on queued PRs (the label reorders the queue, and the | |
| # person applying it is usually watching — without this it waited for the cron). | |
| # Deliberately NOT pull_request — that would execute the PR branch's copy of this workflow and | |
| # script with CIRCLE_TOKEN in the environment, letting any same-repo PR exfiltrate the secret. | |
| # pull_request_target runs master's workflow definition, and the checkout below pins ref: master, | |
| # so PR-controlled code never runs here. | |
| # - repository_dispatch (merge-queue-reconcile): sent by the CircleCI bit_merge job when it ends | |
| # (including the early-halt path of no-component merges). This is the turn-handoff trigger — | |
| # nothing else can be: bump commits carry "[skip ci]", which suppresses Actions push runs too, | |
| # and the cron is throttled to 5-15m. With the ping, the next PR gets its turn within seconds. | |
| # - push to master: PR merges landing (bump commits fire nothing, per the above). | |
| # - check_suite: a queued PR's Actions CI finishing fires no push/PR event, so without this the | |
| # green->turn transition waited for the throttled cron (10-20m in practice). Fires on every | |
| # suite completion in the repo; the reconcile is idempotent and cheap, and the concurrency | |
| # group collapses bursts. Master suites are filtered out in the job condition — push and | |
| # repository_dispatch already cover master's transitions, and every bit_merge would otherwise | |
| # fire a pointless reconcile ~40m after each merge. Not filtered on an associated PR: | |
| # check_suite.pull_requests is empty for fork PRs, which can legitimately be queued (their | |
| # head_branch is null, so the master filter keeps them). No self-recursion: this workflow's own | |
| # runs complete their suites via GITHUB_TOKEN, whose events never trigger workflows. Runs the | |
| # default branch's workflow copy, so PR code never runs here. | |
| # - status: check_suite only covers GitHub Actions suites — the CircleCI gating checks are | |
| # commit statuses, which fire this event instead. Without it, the last CircleCI job going | |
| # green fires nothing and a ready PR parks until the cron/heartbeat (2026-08-18: a scheduled | |
| # run evaluated 15s before bit_pr turned green, stranding an otherwise-mergeable PR). The job | |
| # condition drops pending states (no queue decision changes on pending), master-branch | |
| # statuses (push + repository_dispatch cover master; branches is empty for fork PR heads, so | |
| # they pass), and our own merge-queue/* context (heartbeat runs post it with a PAT whose | |
| # events do trigger workflows — one echo reconcile is harmless, but there's no point). | |
| # Like check_suite, always runs the default branch's workflow copy. | |
| # - workflow_run (merge-queue-review-ping): a review landing on a queued PR — approvals un-stick | |
| # winners demoted for a missing review. Reviews can't trigger this workflow directly: | |
| # pull_request_review runs the PR merge commit's workflow copy, which must never see | |
| # CIRCLE_TOKEN; the secret-less ping workflow absorbs that context, and its completion lands | |
| # here on master's trusted copy. | |
| # - schedule: safety net for missed events. GitHub throttles it far beyond its 5m spec (20-40m | |
| # silent gaps observed), which is tolerable now that the event triggers above cover the common | |
| # transitions. A CircleCI heartbeat (merge_queue_heartbeat) used to run the same script every | |
| # 10m as outage insurance — removed 2026-08 for flooding the CircleCI pipeline views (~144 | |
| # runs/day); if Actions goes down again (2026-08-06 froze every non-admin merge), run the | |
| # script from any machine as a break-glass: | |
| # GITHUB_TOKEN=<pat> node .github/scripts/merge-queue.js | |
| name: merge-queue | |
| on: | |
| schedule: | |
| - cron: '*/5 * * * *' | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: 'reconcile without mutating anything (no statuses, no branch updates, no dashboard writes)' | |
| type: boolean | |
| default: false | |
| push: | |
| branches: [master] | |
| # only events that can affect the queue: auto-merge toggles, pushes to PRs already in it, and | |
| # priority-label changes on queued PRs (filtered to the exact label in the job condition). | |
| # opened/reopened/ready_for_review PRs can't be queued yet (closing a PR cancels auto-merge), | |
| # and the scheduled safety net covers any residual case. | |
| pull_request_target: | |
| types: [synchronize, auto_merge_enabled, auto_merge_disabled, labeled, unlabeled] | |
| repository_dispatch: | |
| types: [merge-queue-reconcile] | |
| check_suite: | |
| types: [completed] | |
| status: {} | |
| workflow_run: | |
| workflows: [merge-queue-review-ping] | |
| types: [completed] | |
| # never let two reconcile loops overlap; a queued run supersedes waiting duplicates | |
| concurrency: | |
| group: merge-queue | |
| cancel-in-progress: false | |
| permissions: | |
| # contents: write AND pull-requests: write are both required by the update-branch API (master's | |
| # strict up-to-date protection means the bot must press "Update branch" on the queue head, or | |
| # the queue starves after every bump commit; read-only pulls yields 403 "Resource not | |
| # accessible by integration") | |
| contents: write | |
| statuses: write | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| reconcile: | |
| # skip full reconciles for pull_request_target events that can't change the queue: pushes to | |
| # PRs that aren't in it, and label events for anything but the priority label on a queued PR; | |
| # auto-merge toggle events always reconcile | |
| if: >- | |
| github.repository == 'teambit/bit' && | |
| (github.event_name != 'check_suite' || | |
| github.event.check_suite.head_branch != 'master') && | |
| (github.event_name != 'status' || | |
| (github.event.state != 'pending' && | |
| !contains(github.event.branches.*.name, 'master') && | |
| !startsWith(github.event.context, 'merge-queue/'))) && | |
| (github.event_name != 'workflow_run' || | |
| github.event.workflow_run.conclusion == 'success') && | |
| (github.event_name != 'pull_request_target' || | |
| github.event.action == 'auto_merge_enabled' || | |
| github.event.action == 'auto_merge_disabled' || | |
| (github.event.action == 'synchronize' && | |
| github.event.pull_request.auto_merge != null) || | |
| ((github.event.action == 'labeled' || github.event.action == 'unlabeled') && | |
| github.event.label.name == 'merge-queue:priority' && | |
| github.event.pull_request.auto_merge != null)) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # always run the trusted master copy of the script, never a branch's copy | |
| ref: master | |
| sparse-checkout: .github | |
| - name: reconcile merge queue | |
| run: node .github/scripts/merge-queue.js | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CIRCLE_TOKEN: ${{ secrets.CIRCLE_TOKEN }} | |
| MERGE_QUEUE_DRY_RUN: ${{ (github.event_name == 'workflow_dispatch' && inputs.dry_run) && 'true' || 'false' }} |