@plainconceptsplatform/workflows 1.6.48 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -148,6 +148,10 @@ runs:
148
148
  # the carried verdict and resolves the conflict. Pliny-Bot #862 was handed to the gate and
149
149
  # declined every five minutes for an hour waiting for a run that could not start.
150
150
  conflicting="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json mergeable --jq '.mergeable' 2>/dev/null || true)"
151
+ # A merge-queue run is built on the queue's own branch, so its commit is never the head;
152
+ # it is the verdict when the train hands over a pull request the queue ejected.
153
+ run_event="$(gh api "repos/$REPO/actions/runs/$ci_run_id" --jq '.event' 2>/dev/null || true)"
154
+ [ "$run_event" != merge_group ] || run_sha="$current_sha"
151
155
  if [ -n "$run_sha" ] && [ "$run_sha" != "$current_sha" ] && [ "$conflicting" != CONFLICTING ]; then
152
156
  echo "::notice::CI run $ci_run_id judged ${run_sha:0:7}, but the head is ${current_sha:0:7}; reading the verdict again from the head."
153
157
  ci_conclusion=""
@@ -0,0 +1,43 @@
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/land-pull-request/action.yml. Update with workflows update --force; consumer edits may be overwritten.
2
+ name: Land pull request
3
+ description: >-
4
+ Land one pull request the gate or a person approved: into the base branch's merge queue when it
5
+ has one, otherwise by merging it. The head must still be the commit that was judged.
6
+
7
+ inputs:
8
+ token:
9
+ description: App token with pull-requests and contents write, plus merge queues write where a queue is on.
10
+ required: true
11
+ pr:
12
+ description: The pull request number.
13
+ required: true
14
+ head-sha:
15
+ description: The head commit that was judged. Landing is refused once the branch has moved.
16
+ required: true
17
+ approve:
18
+ description: "'true' to approve the pull request first, so a ruleset's required approval is met by the gate."
19
+ required: false
20
+ default: 'false'
21
+ approval-body:
22
+ description: The review body that goes with the approval.
23
+ required: false
24
+ default: Approved by the merge gate.
25
+
26
+ outputs:
27
+ landed:
28
+ description: "'merged', 'enqueued', or empty when nothing happened."
29
+ value: ${{ steps.land.outputs.landed }}
30
+
31
+ runs:
32
+ using: composite
33
+ steps:
34
+ - id: land
35
+ shell: bash
36
+ env:
37
+ GH_TOKEN: ${{ inputs.token }}
38
+ REPO: ${{ github.repository }}
39
+ PR: ${{ inputs.pr }}
40
+ HEAD_SHA: ${{ inputs.head-sha }}
41
+ APPROVE: ${{ inputs.approve }}
42
+ APPROVAL_BODY: ${{ inputs.approval-body }}
43
+ run: bash "${GITHUB_ACTION_PATH}/land-pull-request.sh"
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/land-pull-request/land-pull-request.sh. Update with workflows update --force; consumer edits may be overwritten.
3
+ #
4
+ # Lands one approved pull request. A base branch with a merge queue gets the pull request in the
5
+ # queue, where GitHub builds it on top of what is ahead and merges it; any other base gets a squash
6
+ # merge, as before. The asynchronous merge endpoint is the one that enqueues: the older merge
7
+ # endpoint and auto-merge do not take stacked pull requests.
8
+ set -euo pipefail
9
+
10
+ : "${REPO:?}" "${PR:?}" "${HEAD_SHA:?}"
11
+ [[ "$PR" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid pull request number: $PR"; exit 1; }
12
+ [[ "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || { echo "::error::Invalid head commit for #$PR: $HEAD_SHA"; exit 1; }
13
+
14
+ out() { echo "landed=$1" >> "${GITHUB_OUTPUT:-/dev/null}"; }
15
+
16
+ pr_json=$(gh pr view "$PR" --repo "$REPO" --json headRefOid,baseRefName,headRefName,state)
17
+ state=$(jq -r .state <<<"$pr_json")
18
+ [ "$state" = OPEN ] || { echo "#$PR is $state; nothing to land."; out ""; exit 0; }
19
+ current=$(jq -r .headRefOid <<<"$pr_json")
20
+ if [ "$current" != "$HEAD_SHA" ]; then
21
+ echo "::warning::#$PR moved from ${HEAD_SHA:0:7} to ${current:0:7} after it was judged; not landing it."
22
+ out ""
23
+ exit 0
24
+ fi
25
+ base=$(jq -r .baseRefName <<<"$pr_json")
26
+ head_ref=$(jq -r .headRefName <<<"$pr_json")
27
+
28
+ owner=${REPO%%/*}
29
+ name=${REPO#*/}
30
+ queue=$(gh api graphql -f query='query($o:String!,$n:String!,$b:String!){repository(owner:$o,name:$n){mergeQueue(branch:$b){id}}}' \
31
+ -f o="$owner" -f n="$name" -f b="$base" --jq '.data.repository.mergeQueue.id // ""' 2>/dev/null || true)
32
+
33
+ if [ "${APPROVE:-false}" = true ]; then
34
+ # A ruleset that requires an approval counts the App's. Without one this is a record of the verdict.
35
+ gh api -X POST "repos/$REPO/pulls/$PR/reviews" -f commit_id="$HEAD_SHA" -f event=APPROVE \
36
+ -f body="${APPROVAL_BODY:-Approved by the merge gate.}" >/dev/null ||
37
+ echo "::warning::Could not approve #$PR; a required approval will hold it."
38
+ fi
39
+
40
+ if [ -z "$queue" ]; then
41
+ gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$HEAD_SHA"
42
+ # The branch goes as well: nothing else deletes it where `delete_branch_on_merge` is off.
43
+ gh api -X DELETE "repos/$REPO/git/refs/heads/$head_ref" >/dev/null 2>&1 || true
44
+ echo "Merged #$PR into $base."
45
+ out merged
46
+ exit 0
47
+ fi
48
+
49
+ pr_id=$(gh pr view "$PR" --repo "$REPO" --json id --jq .id)
50
+ # GraphQL first: the asynchronous merge endpoint refuses a GitHub App with "Enqueuer is not
51
+ # authorized to merge" (Agentic-Workflow-Dogfood, 2026-10-20), while enqueuePullRequest takes the
52
+ # same token. A stacked pull request refuses GraphQL and only the asynchronous endpoint takes it.
53
+ enqueued=$(gh api graphql -f query='mutation($id:ID!,$sha:GitObjectID!){enqueuePullRequest(input:{pullRequestId:$id,expectedHeadOid:$sha}){mergeQueueEntry{state}}}' \
54
+ -f id="$pr_id" -f sha="$HEAD_SHA" 2>&1 || true)
55
+ if grep -q '"mergeQueueEntry":{' <<<"$enqueued"; then
56
+ echo "#$PR is in the $base merge queue; GitHub merges it once its group passes."
57
+ out enqueued
58
+ exit 0
59
+ fi
60
+ if ! grep -q 'part of a stack' <<<"$enqueued"; then
61
+ echo "::error::#$PR could not be added to the $base merge queue: $(tr '\n' ' ' <<<"$enqueued" | cut -c1-400)"
62
+ out ""
63
+ exit 1
64
+ fi
65
+
66
+ # A stack enters the queue only through merge-async, which refuses a GitHub App there. The App lands
67
+ # it directly instead, through the ruleset's bypass list (it must be on it): every layer has passed its
68
+ # own checks and the gate, and the whole stack lands as one operation.
69
+ answer=$(gh api -X PUT "repos/$REPO/pulls/$PR/merge-async" -f merge_action=direct_merge -f merge_method=squash \
70
+ -F bypass_rules=true -f sha="$HEAD_SHA")
71
+ status=$(jq -r .status <<<"$answer")
72
+ uuid=$(jq -r '.details.uuid // ""' <<<"$answer")
73
+ for _ in $(seq 1 12); do
74
+ case "$status" in enqueued | merged) break ;; failed) break ;; esac
75
+ [ -n "$uuid" ] || break
76
+ sleep 5
77
+ answer=$(gh api "repos/$REPO/pulls/$PR/merge-async/$uuid" 2>/dev/null || echo '{}')
78
+ status=$(jq -r '.status // "pending"' <<<"$answer")
79
+ done
80
+
81
+ case "$status" in
82
+ enqueued)
83
+ echo "#$PR is in the $base merge queue; GitHub merges it once its group passes."
84
+ out enqueued
85
+ ;;
86
+ merged)
87
+ echo "#$PR merged into $base."
88
+ out merged
89
+ ;;
90
+ *)
91
+ message=$(jq -r '.details.message // ""' <<<"$answer")
92
+ echo "::error::Stack #$PR was not merged (status '${status}'${message:+: $message}). The App lands a stack by bypassing the base branch's ruleset, so it must be on that ruleset's bypass list."
93
+ out ""
94
+ exit 1
95
+ ;;
96
+ esac
@@ -50,6 +50,10 @@ inputs:
50
50
  description: Decide and report the move without making it.
51
51
  required: false
52
52
  default: 'false'
53
+ merge-queue:
54
+ description: "true when the base branch has GitHub's merge queue (repository variable MERGE_QUEUE): the train stops ordering and updating, and repairs what the queue ejects."
55
+ required: false
56
+ default: 'false'
53
57
  ci-depth-tiered:
54
58
  description: "true when the repository's CI runs only light checks on a bot's push and the full CI when the train asks (repository variable CI_DEPTH_TIERED)."
55
59
  required: false
@@ -82,6 +86,7 @@ runs:
82
86
  MAIN_RED_WAIT_HOURS: ${{ inputs.main-red-wait-hours }}
83
87
  MAX_REPAIRS: ${{ inputs.max-repairs }}
84
88
  DRY_RUN: ${{ inputs.dry-run }}
89
+ MERGE_QUEUE: ${{ inputs.merge-queue }}
85
90
  CI_DEPTH_TIERED: ${{ inputs.ci-depth-tiered }}
86
91
  HINT_RUN_ID: ${{ inputs.ci-run-id }}
87
92
  HINT_CONCLUSION: ${{ inputs.ci-conclusion }}
@@ -227,6 +227,28 @@ deepen_ci() {
227
227
  gh run rerun "$run" --repo "$REPO" >/dev/null 2>&1
228
228
  }
229
229
 
230
+ # Where a pull request stands in its base branch's merge queue: QUEUED, AWAITING_CHECKS, LOCKED,
231
+ # UNMERGEABLE or MERGEABLE, or empty when it is not in one.
232
+ queue_state() {
233
+ local pr="$1"
234
+ gh api graphql -f query='query($o:String!,$n:String!,$p:Int!){repository(owner:$o,name:$n){pullRequest(number:$p){mergeQueueEntry{state}}}}' \
235
+ -f o="${REPO%%/*}" -f n="${REPO#*/}" -F p="$pr" \
236
+ --jq '.data.repository.pullRequest.mergeQueueEntry.state // ""' 2>/dev/null || true
237
+ }
238
+
239
+ # The failed merge-group CI run behind the queue's last ejection of a pull request since its head
240
+ # was pushed, or nothing. The queue names its branches gh-readonly-queue/<base>/pr-<number>-<sha>.
241
+ queue_ejection_run() {
242
+ local pr="$1" since="$2" removed
243
+ removed=$(gh api --paginate "repos/$REPO/issues/$pr/timeline?per_page=100" \
244
+ --jq '.[] | select(.event == "removed_from_merge_queue") | .created_at' 2>/dev/null | sort | tail -1)
245
+ [ -n "$removed" ] || return 0
246
+ [ -z "$since" ] || [ "$removed" \> "$since" ] || return 0
247
+ gh api "repos/$REPO/actions/runs?event=merge_group&per_page=100" \
248
+ --jq "[.workflow_runs[] | select(.name == env.CI_WORKFLOW_NAME and .conclusion == \"failure\"
249
+ and (.head_branch | test(\"/pr-$pr-\")))] | sort_by(.created_at) | last | .id // empty" 2>/dev/null || true
250
+ }
251
+
230
252
  is_trusted_bot() {
231
253
  case " $TRUSTED_BOTS " in *" $1 "*) return 0 ;; esac
232
254
  return 1
@@ -379,6 +401,9 @@ fi
379
401
 
380
402
  for line in "${candidates[@]}"; do
381
403
  IFS=$'\t' read -r pr author head_sha head_ref base_ref hold <<<"$line"
404
+ # With GitHub's merge queue (MERGE_QUEUE) the queue orders, builds and merges: nothing here waits
405
+ # for the pull request ahead, brings a branch up to date or asks for a deeper CI.
406
+ [ "${MERGE_QUEUE:-false}" = true ] && merge_blocked=false
382
407
 
383
408
  if ! can_ride "$author"; then
384
409
  echo "#$pr: opened by $author, who has no write access; not on the train."
@@ -473,6 +498,23 @@ for line in "${candidates[@]}"; do
473
498
  --jq '[.jobs[] | select((.name | test("(^|/ )Full CI$")) and .conclusion == "success")] | length' 2>/dev/null || echo "")
474
499
  [ "$full_jobs" = 0 ] && ci_shallow=true
475
500
  fi
501
+ if [ "${MERGE_QUEUE:-false}" = true ]; then
502
+ # The pull request's own CI is the light one; the full CI runs on the queue's merge group.
503
+ ci_shallow=false
504
+ if [ "$(queue_state "$pr")" != "" ]; then
505
+ echo "#$pr ($author): skip, it is in the merge queue."
506
+ summary "| #$pr | $author | skip | in the merge queue |"
507
+ continue
508
+ fi
509
+ # The queue ejected it after this head was pushed: the merge group's CI failed on it, or it
510
+ # conflicted with what was ahead. That run is the verdict the gate repairs from; the pull
511
+ # request's own green light CI says nothing about it.
512
+ if ejected_run=$(queue_ejection_run "$pr" "${head_committed:-}"); [ -n "$ejected_run" ]; then
513
+ ci_run_id="$ejected_run"
514
+ ci_status=completed
515
+ ci_conclusion=failure
516
+ fi
517
+ fi
476
518
  ci_stuck=false
477
519
  if [ "$ci_status" != none ] && [ "$ci_status" != completed ] && [ -n "${ci_created:-}" ]; then
478
520
  [ $((now_epoch - $(iso_epoch "$ci_created"))) -lt $((STUCK_HOURS * 3600)) ] || ci_stuck=true
@@ -482,7 +524,7 @@ for line in "${candidates[@]}"; do
482
524
  behind_by=0
483
525
  if [ "$park" = none ] && [ "$hold" != true ] && [ "$review_decision" != CHANGES_REQUESTED ]; then
484
526
  mergeable=$(mergeable_state "$pr")
485
- if [ "$ci_status" = completed ] && [ "$ci_conclusion" = success ]; then
527
+ if [ "$ci_status" = completed ] && [ "$ci_conclusion" = success ] && [ "${MERGE_QUEUE:-false}" != true ]; then
486
528
  behind_by=$(gh api "repos/$REPO/compare/$base_ref...$head_sha" --jq '.behind_by' 2>/dev/null || echo 0)
487
529
  fi
488
530
  fi
@@ -2886,6 +2886,17 @@ else
2886
2886
  FAIL=$((FAIL + 1))
2887
2887
  echo "FAIL: the gate waits for CI on a conflicting pull request, which never gets a run" >&2
2888
2888
  fi
2889
+ # Merge queue: the train hands the queue what it can, repairs what the queue ejects, and the gate lands
2890
+ # through the queue when the base branch has one.
2891
+ if grep -qF 'queue_ejection_run' "${HERE}/../merge-train/merge-train.sh" &&
2892
+ grep -qF "merge-queue: \${{ vars.MERGE_QUEUE || 'false' }}" "${WORKFLOWS_DIR}/work-router.yml" &&
2893
+ grep -qF 'enqueuePullRequest' "${HERE}/../land-pull-request/land-pull-request.sh" &&
2894
+ grep -qF 'uses: ./.github/actions/land-pull-request' "${WORKFLOWS_DIR}/agent-merge-gate.md"; then
2895
+ PASS=$((PASS + 1))
2896
+ else
2897
+ FAIL=$((FAIL + 1))
2898
+ echo "FAIL: the merge queue path is incomplete" >&2
2899
+ fi
2889
2900
  # An empty AGENT_TIER_MODEL must remove an override a reused runner kept from an earlier job.
2890
2901
  if grep -qF 'rm -f .opencode/harness.user.json' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
2891
2902
  PASS=$((PASS + 1))
@@ -3,6 +3,8 @@ name: "App: CI"
3
3
 
4
4
  on:
5
5
  pull_request:
6
+ # GitHub's merge queue builds the base plus the pull requests ahead; the full CI runs there.
7
+ merge_group:
6
8
  schedule:
7
9
  - cron: "0 6 * * 1"
8
10
  workflow_dispatch:
@@ -61,7 +63,7 @@ concurrency:
61
63
  jobs:
62
64
  api:
63
65
  name: API (.NET)
64
- if: github.event_name != 'schedule'
66
+ if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
65
67
  runs-on: agents-arc
66
68
  timeout-minutes: 30
67
69
  services:
@@ -168,7 +170,7 @@ jobs:
168
170
 
169
171
  web:
170
172
  name: Web (Next.js)
171
- if: github.event_name != 'schedule'
173
+ if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
172
174
  runs-on: agents-arc
173
175
  timeout-minutes: 15
174
176
  steps:
@@ -280,6 +282,23 @@ jobs:
280
282
  # needs contents: write this workflow does not have, and fails the run that gates deploy.
281
283
  upload-release-assets: false
282
284
 
285
+ # The merge queue requires this check. On a pull request with MERGE_QUEUE on, the heavy jobs are
286
+ # skipped and the full CI runs on the merge group instead; skipped counts as passed.
287
+ full-ci:
288
+ name: Full CI
289
+ needs: [api, web]
290
+ if: always()
291
+ runs-on: agents-arc
292
+ timeout-minutes: 5
293
+ steps:
294
+ - name: Every heavy job passed
295
+ env:
296
+ RESULTS: ${{ join(needs.*.result, ' ') }}
297
+ run: |
298
+ for result in $RESULTS; do
299
+ case "$result" in failure | cancelled | timed_out) echo "A heavy job ended $result."; exit 1 ;; esac
300
+ done
301
+
283
302
  # Hands this run's verdict to the router's merge train, which decides whether this pull request
284
303
  # is the one in front and what it needs. A pull request opened or updated with a bot token gets
285
304
  # no workflow_run event for its CI (the run's actor is the bot), and the train brings people's
@@ -3,6 +3,8 @@ name: "App: CI (Node monorepo)"
3
3
 
4
4
  on:
5
5
  pull_request:
6
+ # GitHub's merge queue builds the base plus the pull requests ahead; the full CI runs there.
7
+ merge_group:
6
8
  schedule:
7
9
  - cron: "0 6 * * 1"
8
10
  workflow_dispatch:
@@ -50,7 +52,7 @@ jobs:
50
52
 
51
53
  web:
52
54
  name: Web (Next.js static export)
53
- if: github.event_name != 'schedule'
55
+ if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
54
56
  runs-on: agents-arc
55
57
  steps:
56
58
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -101,7 +103,7 @@ jobs:
101
103
 
102
104
  e2e:
103
105
  name: E2E (Playwright)
104
- if: github.event_name != 'schedule'
106
+ if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
105
107
  needs: web
106
108
  runs-on: agents-arc
107
109
  steps:
@@ -126,7 +128,7 @@ jobs:
126
128
 
127
129
  desktop:
128
130
  name: Desktop (Electron)
129
- if: github.event_name != 'schedule'
131
+ if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
130
132
  needs: web
131
133
  runs-on: agents-arc
132
134
  steps:
@@ -151,7 +153,7 @@ jobs:
151
153
 
152
154
  mobile:
153
155
  name: Mobile (Capacitor)
154
- if: github.event_name != 'schedule'
156
+ if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || vars.MERGE_QUEUE != 'true')
155
157
  runs-on: agents-arc
156
158
  steps:
157
159
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -210,6 +212,23 @@ jobs:
210
212
  # needs contents: write this workflow does not have, and fails the run that gates deploy.
211
213
  upload-release-assets: false
212
214
 
215
+ # The merge queue requires this check. On a pull request with MERGE_QUEUE on, the heavy jobs are
216
+ # skipped and the full CI runs on the merge group instead; skipped counts as passed.
217
+ full-ci:
218
+ name: Full CI
219
+ needs: [web, e2e, desktop, mobile]
220
+ if: always()
221
+ runs-on: agents-arc
222
+ timeout-minutes: 5
223
+ steps:
224
+ - name: Every heavy job passed
225
+ env:
226
+ RESULTS: ${{ join(needs.*.result, ' ') }}
227
+ run: |
228
+ for result in $RESULTS; do
229
+ case "$result" in failure | cancelled | timed_out) echo "A heavy job ended $result."; exit 1 ;; esac
230
+ done
231
+
213
232
  # Hands this run's verdict to the router's merge train, which decides whether this pull request
214
233
  # is the one in front and what it needs. A pull request opened or updated with a bot token gets
215
234
  # no workflow_run event for its CI (the run's actor is the bot), and the train brings people's
@@ -1,30 +1,30 @@
1
- # Managed by @plainconceptsplatform/workflows. Source: loops/templates/release/github-release.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
- name: Publish GitHub release
3
-
4
- on:
5
- push:
6
- tags:
7
- - "v*"
8
-
9
- permissions:
10
- contents: write
11
-
12
- jobs:
13
- publish:
14
- name: Publish release
15
- runs-on: ubuntu-latest
16
- timeout-minutes: 10
17
- steps:
18
- - name: Create or update GitHub release
19
- env:
20
- GH_TOKEN: ${{ github.token }}
21
- TAG: ${{ github.ref_name }}
22
- shell: bash
23
- run: |
24
- set -euo pipefail
25
-
26
- if gh release view "$TAG" >/dev/null 2>&1; then
27
- gh release edit "$TAG" --title "$TAG" --generate-notes
28
- else
29
- gh release create "$TAG" --title "$TAG" --generate-notes
30
- fi
1
+ # Managed by @plainconceptsplatform/workflows. Source: loops/templates/release/github-release.yml. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ name: Publish GitHub release
3
+
4
+ on:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ permissions:
10
+ contents: write
11
+
12
+ jobs:
13
+ publish:
14
+ name: Publish release
15
+ runs-on: agents-arc
16
+ timeout-minutes: 10
17
+ steps:
18
+ - name: Create or update GitHub release
19
+ env:
20
+ GH_TOKEN: ${{ github.token }}
21
+ TAG: ${{ github.ref_name }}
22
+ shell: bash
23
+ run: |
24
+ set -euo pipefail
25
+
26
+ if gh release view "$TAG" >/dev/null 2>&1; then
27
+ gh release edit "$TAG" --title "$TAG" --generate-notes
28
+ else
29
+ gh release create "$TAG" --title "$TAG" --generate-notes
30
+ fi
@@ -610,16 +610,24 @@ jobs:
610
610
 
611
611
  ${{ needs.subject.outputs.issue != '' && format('Findings and verification on the linked issue: #{0}.', needs.subject.outputs.issue) || 'Findings and verification are in the review comment above.' }} [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
612
612
  ${{ env.EVIDENCE_BEFORE_REVIEW == 'true' && contains(fromJson('["human-review","owner-review"]'), needs.validate_output.outputs.outcome) && 'Screenshots of this head follow in their own comment if it carries a verification plan. Approving the pull request merges it.' || '' }}
613
- - name: Merge approved pull request
613
+ - name: Read the head to land
614
+ id: landing_head
614
615
  if: needs.validate_output.outputs.outcome == 'auto-merge' && (needs.subject.outputs.stack_root != 'true' || needs.subject.outputs.root_complete == 'true')
615
616
  env:
616
617
  GH_TOKEN: ${{ steps.app-token.outputs.token }}
617
618
  REPO: ${{ github.repository }}
618
619
  PR: ${{ needs.subject.outputs.pr }}
619
- run: |
620
- set -euo pipefail
621
- head_sha=$(gh pr view "$PR" --repo "$REPO" --json headRefOid --jq '.headRefOid')
622
- gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$head_sha"
620
+ run: echo "sha=$(gh pr view "$PR" --repo "$REPO" --json headRefOid --jq '.headRefOid')" >> "$GITHUB_OUTPUT"
621
+ # Into the base branch's merge queue when it has one (approved first, so a ruleset's required
622
+ # approval is the gate's), otherwise merged as before.
623
+ - name: Merge approved pull request
624
+ if: needs.validate_output.outputs.outcome == 'auto-merge' && (needs.subject.outputs.stack_root != 'true' || needs.subject.outputs.root_complete == 'true')
625
+ uses: ./.github/actions/land-pull-request
626
+ with:
627
+ token: ${{ steps.app-token.outputs.token }}
628
+ pr: ${{ needs.subject.outputs.pr }}
629
+ head-sha: ${{ steps.landing_head.outputs.sha }}
630
+ approve: 'true'
623
631
  # The root's own gate run stops here: children may still be landing on the feature
624
632
  # branch, and the merge decision for main belongs to a person. The comment says only
625
633
  # what a reader needs: the gate held the root, children merge into the feature branch,
@@ -29,7 +29,7 @@ jobs:
29
29
  name: Tag and publish the release
30
30
  if: >-
31
31
  startsWith(github.event.head_commit.message, 'chore(release): v')
32
- runs-on: ubuntu-latest
32
+ runs-on: agents-arc
33
33
  timeout-minutes: 10
34
34
  steps:
35
35
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -862,7 +862,7 @@ jobs:
862
862
  # gate. Those run in a group of their own per pull request, in parallel, while everything that
863
863
  # may merge still waits its turn on the one belt.
864
864
  concurrency:
865
- group: ${{ needs.classify.outputs.ci-conclusion == 'failure' && format('merge-repair-{0}', needs.classify.outputs.pr-number) || 'merge-belt' }}
865
+ group: ${{ needs.classify.outputs.ci-conclusion == 'failure' && format('merge-repair-{0}', needs.classify.outputs.pr-number) || (vars.MERGE_QUEUE == 'true' && format('merge-gate-{0}', needs.classify.outputs.pr-number) || 'merge-belt') }}
866
866
  cancel-in-progress: false
867
867
  queue: max
868
868
  permissions: write-all
@@ -940,6 +940,7 @@ jobs:
940
940
  ci-conclusion: ${{ needs.classify.outputs.ci-conclusion }}
941
941
  # Repository variable, read by the repository's CI as well: one switch for both sides.
942
942
  ci-depth-tiered: ${{ vars.CI_DEPTH_TIERED || 'false' }}
943
+ merge-queue: ${{ vars.MERGE_QUEUE || 'false' }}
943
944
 
944
945
  # The decision the gate parked for: a person with write access approves a pull request the
945
946
  # gate handed to them (human-review or owner-review), and the bot merges the commit they
@@ -1014,8 +1015,9 @@ jobs:
1014
1015
  --jq '[.workflow_runs[] | select(.name == env.CI_WORKFLOW_NAME)] | sort_by(.created_at) | last | .conclusion // ""')
1015
1016
  [ "$ci" = success ] || skip "$CI_WORKFLOW_NAME on ${head_sha:0:7} is '${ci:-not run}'"
1016
1017
 
1017
- gh pr merge "$PR" --repo "$REPO" --squash --match-head-commit "$head_sha"
1018
- echo "merged=true" >> "$GITHUB_OUTPUT"
1018
+ # Into the merge queue where the base branch has one, merged otherwise.
1019
+ landed=$(HEAD_SHA="$head_sha" GITHUB_OUTPUT=/dev/stdout bash ./.github/actions/land-pull-request/land-pull-request.sh | sed -n 's/^landed=//p' | tail -1)
1020
+ if [ -n "$landed" ]; then echo "merged=true" >> "$GITHUB_OUTPUT"; fi
1019
1021
  - name: Clear the issue's labels
1020
1022
  if: steps.merge.outputs.merged == 'true' && needs.classify.outputs.linked-issue != ''
1021
1023
  env:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.6.48",
3
+ "version": "1.7.1",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",