@plainconceptsplatform/workflows 1.6.42 → 1.6.48

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.
@@ -214,6 +214,47 @@ runs:
214
214
  retried += 1;
215
215
  }
216
216
 
217
+ // 1b. Release held children. The router holds a stack child while a `Blocked by #N` sibling is
218
+ // open and the landing of that sibling releases it. When the stack's root merged first, or the
219
+ // sibling landed on the default branch, no landing event ever comes: Pliny-Bot kept eight
220
+ // children held for a day behind issues that had been closed for hours. A sweep that finds
221
+ // every blocker closed sends the child back through the router, which no longer holds it.
222
+ // Once per issue: the router decides, and a child it holds again must not be dispatched on
223
+ // every sweep.
224
+ const RELEASE_MARKER = '<!-- housekeeping-release -->';
225
+ let released = 0;
226
+ for (const issue of issues) {
227
+ if (issue.pull_request) continue;
228
+ const labels = names(issue);
229
+ if (!labels.includes('implement')) continue;
230
+ if (['bot-working', 'stalled', 'review', 'pr-pending'].some((l) => labels.includes(l))) continue;
231
+ const thread = await github.paginate(github.rest.issues.listComments, {
232
+ ...context.repo, issue_number: issue.number, per_page: 100,
233
+ });
234
+ if (!thread.some((c) => (c.body ?? '').includes('<!-- stack-held -->'))) continue;
235
+ if (thread.some((c) => (c.body ?? '').includes(RELEASE_MARKER))) continue;
236
+ const blockers = [...new Set([...(issue.body ?? '').matchAll(/blocked by #(\d+)/gi)].map((m) => Number(m[1])))];
237
+ let stillOpen = 0;
238
+ for (const number of blockers) {
239
+ const blocker = await github.rest.issues.get({ ...context.repo, issue_number: number })
240
+ .then((r) => r.data).catch(() => null);
241
+ if (blocker && blocker.state === 'open') stillOpen += 1;
242
+ }
243
+ if (stillOpen > 0) continue;
244
+ await act(`#${issue.number}: releasing a held child, every blocker is closed`, async () => {
245
+ await github.rest.issues.createComment({
246
+ ...context.repo, issue_number: issue.number,
247
+ body: `${RELEASE_MARKER}\nEvery issue this one was blocked by is closed, so it is going back through the router.`,
248
+ });
249
+ await github.rest.actions.createWorkflowDispatch({
250
+ ...context.repo, workflow_id: 'work-router.yml',
251
+ ref: context.payload.repository.default_branch,
252
+ inputs: { operation: 'implement', 'issue-number': String(issue.number) },
253
+ });
254
+ });
255
+ released += 1;
256
+ }
257
+
217
258
  // 2. Strands. pr-pending says a pull request is open and waiting; when that pull request
218
259
  // closed without merging, nothing ever took the label off.
219
260
  for (const issue of issues) {
@@ -144,7 +144,11 @@ runs:
144
144
  if [ -n "$ci_run_id" ]; then
145
145
  run_sha="$(gh api "repos/$REPO/actions/runs/$ci_run_id" --jq '.head_sha' 2>/dev/null || true)"
146
146
  current_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
147
- if [ -n "$run_sha" ] && [ "$run_sha" != "$current_sha" ]; then
147
+ # A conflicting pull request has no merge ref, so CI never runs on its head: the gate keeps
148
+ # the carried verdict and resolves the conflict. Pliny-Bot #862 was handed to the gate and
149
+ # declined every five minutes for an hour waiting for a run that could not start.
150
+ conflicting="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json mergeable --jq '.mergeable' 2>/dev/null || true)"
151
+ if [ -n "$run_sha" ] && [ "$run_sha" != "$current_sha" ] && [ "$conflicting" != CONFLICTING ]; then
148
152
  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."
149
153
  ci_conclusion=""
150
154
  ci_run_id=""
@@ -154,6 +158,8 @@ runs:
154
158
  fi
155
159
  ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$fresh")"
156
160
  ci_run_id="$(jq -r '.id' <<<"$fresh")"
161
+ elif [ "$conflicting" = CONFLICTING ]; then
162
+ echo "::notice::PR #$PR_NUMBER conflicts with its base, so CI cannot run on its head; the gate resolves the conflict."
157
163
  fi
158
164
  fi
159
165
 
@@ -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
+ ci-depth-tiered:
54
+ 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
+ required: false
56
+ default: 'false'
53
57
  ci-run-id:
54
58
  description: A CI run that handed its verdict over from inside itself, and so still reads as in progress.
55
59
  required: false
@@ -78,6 +82,7 @@ runs:
78
82
  MAIN_RED_WAIT_HOURS: ${{ inputs.main-red-wait-hours }}
79
83
  MAX_REPAIRS: ${{ inputs.max-repairs }}
80
84
  DRY_RUN: ${{ inputs.dry-run }}
85
+ CI_DEPTH_TIERED: ${{ inputs.ci-depth-tiered }}
81
86
  HINT_RUN_ID: ${{ inputs.ci-run-id }}
82
87
  HINT_CONCLUSION: ${{ inputs.ci-conclusion }}
83
88
  run: bash "${GITHUB_ACTION_PATH}/merge-train.sh"
@@ -70,6 +70,8 @@ train_move() {
70
70
  success)
71
71
  if [ "${BEHIND_BY:-0}" -gt 0 ]; then
72
72
  move=update; reason="green but ${BEHIND_BY} commit(s) behind its base branch"
73
+ elif [ "${CI_SHALLOW:-false}" = true ]; then
74
+ move=deepen; reason="green on the light checks only; the full CI has not run on this head"
73
75
  else
74
76
  move=gate; reason="green and up to date"
75
77
  fi
@@ -161,6 +163,28 @@ SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}"
161
163
  now_epoch=$(date -u +%s)
162
164
  iso_epoch() { date -u -d "$1" +%s 2>/dev/null || echo 0; }
163
165
 
166
+ # The failing jobs of the CI run on a commit, read from the Actions API. The App behind the train's token
167
+ # can dispatch workflows but may not read check runs (403 "Resource not accessible by integration"): the
168
+ # check-runs calls then returned the same error text for the head and for the base, "what fails here" was
169
+ # always a subset of "what fails on the base", and every red pull request was left waiting. Nothing is
170
+ # printed, and the status is non-zero, when the run cannot be read, so the caller claims nothing.
171
+ ci_runs_for_sha() {
172
+ local sha="$1" runs
173
+ # Every completed run on the commit, not only the CI workflow's: on the default branch the CI runs inside the
174
+ # deploy workflow. Extra failures on the base are harmless, the rule only asks that the pull request's own
175
+ # failures are among them.
176
+ runs=$(gh api "repos/$REPO/actions/runs?head_sha=$sha&per_page=100" --jq '.workflow_runs[] | select(.status == "completed") | .id' 2>/dev/null) || return 1
177
+ [ -n "$runs" ] || return 1
178
+ printf '%s
179
+ ' "$runs"
180
+ }
181
+
182
+ ci_failed_jobs() {
183
+ local run="$1"
184
+ gh api "repos/$REPO/actions/runs/$run/jobs?per_page=100" \
185
+ --jq '.jobs[] | select(.conclusion == "failure") | [(.name | sub("^.* / "; "")), .completed_at] | @tsv' 2>/dev/null
186
+ }
187
+
164
188
  summary() { printf '%s\n' "$*" >>"$SUMMARY"; }
165
189
 
166
190
  # Every write goes through this, so a dry run is honest: there is no second path that forgets.
@@ -177,6 +201,32 @@ act() {
177
201
  "$@"
178
202
  }
179
203
 
204
+ # Tiered CI: the label that asks a pull request's CI for the full depth. The CI's depth job reads the
205
+ # pull request's labels at run time, so a re-run of the same pull_request run turns full: the event and
206
+ # its context stay the pull request's, which the scope, mutation and coverage jobs rely on (a separate
207
+ # workflow_dispatch run would lose it, and mutation runs on pull_request events only).
208
+ CI_FULL_LABEL="ci-full"
209
+
210
+ mark_full_ci() {
211
+ local pr="$1"
212
+ gh label create "$CI_FULL_LABEL" --repo "$REPO" --color 0E8A16 \
213
+ --description "The CI runs at full depth on this pull request (set by the merge train)" >/dev/null 2>&1 || true
214
+ gh api -X POST "repos/$REPO/issues/$pr/labels" -f "labels[]=$CI_FULL_LABEL" >/dev/null 2>&1
215
+ }
216
+
217
+ deepen_ci() {
218
+ local pr="$1" run="$2"
219
+ [ -n "$run" ] || return 1
220
+ mark_full_ci "$pr" || return 1
221
+ # A CI handover reaches the train from inside the run, and a run still going cannot be re-run.
222
+ local i
223
+ for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
224
+ [ "$(gh api "repos/$REPO/actions/runs/$run" --jq .status 2>/dev/null)" = completed ] && break
225
+ sleep 10
226
+ done
227
+ gh run rerun "$run" --repo "$REPO" >/dev/null 2>&1
228
+ }
229
+
180
230
  is_trusted_bot() {
181
231
  case " $TRUSTED_BOTS " in *" $1 "*) return 0 ;; esac
182
232
  return 1
@@ -413,6 +463,16 @@ for line in "${candidates[@]}"; do
413
463
  success | failure) ci_status=completed; ci_conclusion="$HINT_CONCLUSION" ;;
414
464
  esac
415
465
  fi
466
+ # Tiered CI (CI_DEPTH_TIERED): a bot's push runs the light checks and the full CI runs once, when the
467
+ # pull request is next in line. Pliny-Bot spent two thirds of its pull request CI on heads that were
468
+ # replaced before they merged. A green run without a successful "Full CI" job was light and says
469
+ # nothing about the tests. When the jobs cannot be read the run is taken as full, as before.
470
+ ci_shallow=false
471
+ if [ "${CI_DEPTH_TIERED:-false}" = true ] && [ "$ci_status" = completed ] && [ "$ci_conclusion" = success ] && [ -n "${ci_run_id:-}" ]; then
472
+ full_jobs=$(gh api "repos/$REPO/actions/runs/$ci_run_id/jobs?per_page=100" \
473
+ --jq '[.jobs[] | select((.name | test("(^|/ )Full CI$")) and .conclusion == "success")] | length' 2>/dev/null || echo "")
474
+ [ "$full_jobs" = 0 ] && ci_shallow=true
475
+ fi
416
476
  ci_stuck=false
417
477
  if [ "$ci_status" != none ] && [ "$ci_status" != completed ] && [ -n "${ci_created:-}" ]; then
418
478
  [ $((now_epoch - $(iso_epoch "$ci_created"))) -lt $((STUCK_HOURS * 3600)) ] || ci_stuck=true
@@ -433,15 +493,27 @@ for line in "${candidates[@]}"; do
433
493
  # the base is the pull request left alone.
434
494
  same_as_main=false
435
495
  if [ "$ci_status" = completed ] && [ "$ci_conclusion" = failure ] && [ "$park" = none ]; then
436
- failed_here=$(gh api "repos/$REPO/commits/$head_sha/check-runs?per_page=100" \
437
- --jq '[.check_runs[] | select(.conclusion == "failure") | (.name | sub("^.* / "; ""))] | unique | .[]' 2>/dev/null || true)
438
- failed_base=$(gh api "repos/$REPO/commits/$base_ref/check-runs?per_page=100" \
439
- --jq '[.check_runs[] | select(.conclusion == "failure") | (.name | sub("^.* / "; ""))] | unique | .[]' 2>/dev/null || true)
440
- # How long the base has been red: the oldest failing check on its tip. The wait is bounded,
441
- # because waiting for a base nobody fixes starves every pull request behind it. Pliny-Bot's
442
- # main was red from 19:30 and the train waited until morning, ten hours, saying nothing.
443
- red_since=$(gh api "repos/$REPO/commits/$base_ref/check-runs?per_page=100" \
444
- --jq '[.check_runs[] | select(.conclusion == "failure") | .completed_at] | min // empty' 2>/dev/null || true)
496
+ failed_here=""
497
+ failed_base=""
498
+ red_since=""
499
+ base_sha=$(gh api "repos/$REPO/commits/$base_ref" --jq .sha 2>/dev/null || true)
500
+ here_jobs=$(ci_failed_jobs "${ci_run_id:-0}" || true)
501
+ failed_here=$(cut -f1 <<<"$here_jobs" | sort -u)
502
+ # A base tip with no completed CI run (a stack branch's tip is a merge commit nothing ran on) says
503
+ # nothing about the base: the pull request is judged on its own failures.
504
+ if [ -n "$base_sha" ] && base_runs=$(ci_runs_for_sha "$base_sha"); then
505
+ base_jobs=""
506
+ for base_run in $base_runs; do
507
+ base_jobs="$base_jobs$(ci_failed_jobs "$base_run" || true)"$'
508
+ '
509
+ done
510
+ base_jobs=$(grep -v '^$' <<<"$base_jobs" || true)
511
+ failed_base=$(cut -f1 <<<"$base_jobs" | sort -u)
512
+ # How long the base has been red: the oldest failing job on its tip. The wait is bounded,
513
+ # because waiting for a base nobody fixes starves every pull request behind it. Pliny-Bot's
514
+ # main was red from 19:30 and the train waited until morning, ten hours, saying nothing.
515
+ red_since=$(cut -f2 <<<"$base_jobs" | sort | head -1)
516
+ fi
445
517
  red_for_hours=0
446
518
  if [ -n "$red_since" ]; then
447
519
  # A date that does not parse reads as epoch 0, which made main "red for 497899h" and ended the
@@ -469,7 +541,7 @@ for line in "${candidates[@]}"; do
469
541
  decision=$(HOLD="$hold" REVIEW_DECISION="$review_decision" PARK="$park" \
470
542
  ATTEMPTS="$attempts" MAX_ATTEMPTS="$MAX_ATTEMPTS" SILENT_GATE="$silent_gate" MERGEABLE="$mergeable" \
471
543
  CI_STATUS="$ci_status" CI_CONCLUSION="$ci_conclusion" CI_STUCK="$ci_stuck" SAME_AS_MAIN="$same_as_main" \
472
- BEHIND_BY="${behind_by:-0}" TRUSTED_AUTHOR="$trusted_author" train_move)
544
+ BEHIND_BY="${behind_by:-0}" TRUSTED_AUTHOR="$trusted_author" CI_SHALLOW="$ci_shallow" train_move)
473
545
  move=$(sed -n 's/^move=//p' <<<"$decision")
474
546
  reason=$(sed -n 's/^reason=//p' <<<"$decision")
475
547
 
@@ -510,11 +582,30 @@ for line in "${candidates[@]}"; do
510
582
  merge_blocked=true
511
583
  continue
512
584
  ;;
585
+ deepen)
586
+ if [ "$merge_blocked" = true ]; then
587
+ echo " #$pr waits for the pull request ahead of it before its full CI runs."
588
+ continue
589
+ fi
590
+ if act "Running the full CI on #$pr, which is next in line." deepen_ci "$pr" "${ci_run_id:-}"; then
591
+ merge_blocked=true
592
+ continue
593
+ fi
594
+ # Never a gate on the light run: it could merge code no test has run on. The pull request is
595
+ # skipped this tick and the line moves on, so one that cannot be deepened does not stop the rest.
596
+ echo "::warning::Could not start the full CI on #$pr; it is skipped this tick."
597
+ continue
598
+ ;;
513
599
  update)
514
600
  if [ "$merge_blocked" = true ]; then
515
601
  echo " #$pr waits for the pull request ahead of it before it is brought up to date."
516
602
  continue
517
603
  fi
604
+ # Next in line: with tiered CI the run this update starts is the one that decides, so it runs full.
605
+ if [ "${CI_DEPTH_TIERED:-false}" = true ]; then
606
+ act "Asking for the full CI on #$pr before it is brought up to date." mark_full_ci "$pr" ||
607
+ echo "::warning::Could not label #$pr for the full CI; its update runs the light checks."
608
+ fi
518
609
  # Merge, never rebase: the gate's pushes are fast-forward only, and so are a person's.
519
610
  if act "Merging $base_ref into #$pr so CI judges it against what it will land on." \
520
611
  update_branch "$pr" "$head_sha"; then
@@ -2828,6 +2828,64 @@ else
2828
2828
  FAIL=$((FAIL + 1))
2829
2829
  echo "FAIL: the merge train treats an unparsable red-since date as an endless red main" >&2
2830
2830
  fi
2831
+ # Every merge gate agent used to share one group, so a merging gate waited behind repairs.
2832
+ if grep -qF 'gh-aw-opencode-agent-merge-gate-${{ inputs.pr-number }}' "${WORKFLOWS_DIR}/agent-merge-gate.md"; then
2833
+ PASS=$((PASS + 1))
2834
+ else
2835
+ FAIL=$((FAIL + 1))
2836
+ echo "FAIL: merge gate agents share one repository-wide concurrency group" >&2
2837
+ fi
2838
+ # The gate resolves conflicts by taking hunks apart, not by keeping one side or both.
2839
+ if grep -qF 'A conflict is not always two additions.' "${WORKFLOWS_DIR}/agent-merge-gate.md"; then
2840
+ PASS=$((PASS + 1))
2841
+ else
2842
+ FAIL=$((FAIL + 1))
2843
+ echo "FAIL: the merge gate prompt must say a conflict is not always two additions" >&2
2844
+ fi
2845
+ # a held stack child whose blockers all closed is sent back through the router by the janitor.
2846
+ if grep -qF 'releasing a held child, every blocker is closed' "${HERE}/../housekeeping/action.yml"; then
2847
+ PASS=$((PASS + 1))
2848
+ else
2849
+ FAIL=$((FAIL + 1))
2850
+ echo "FAIL: the janitor does not release a held child whose blockers all closed" >&2
2851
+ fi
2852
+ # the train reads failing jobs from the Actions API, not from check runs its App cannot read.
2853
+ if grep -qF 'ci_failed_jobs()' "${HERE}/../merge-train/merge-train.sh" && ! grep -qF 'check-runs?per_page=100' "${HERE}/../merge-train/merge-train.sh"; then
2854
+ PASS=$((PASS + 1))
2855
+ else
2856
+ FAIL=$((FAIL + 1))
2857
+ echo "FAIL: the merge train compares failures through check runs its App cannot read" >&2
2858
+ fi
2859
+ # the gate measures check-diff against the branch the pull request merges into, not the default branch.
2860
+ if grep -qF "needs.subject.outputs.pr_base || github.event.repository.default_branch" "${WORKFLOWS_DIR}/agent-merge-gate.md"; then
2861
+ PASS=$((PASS + 1))
2862
+ else
2863
+ FAIL=$((FAIL + 1))
2864
+ echo "FAIL: the gate measures a stack child's comments and tests against the default branch" >&2
2865
+ fi
2866
+ # Tiered CI: the train deepens a light green run instead of gating it, and never gates a light run.
2867
+ if grep -qF 'move=deepen' "${HERE}/../merge-train/merge-train.sh" &&
2868
+ grep -qF 'deepen_ci "$pr" "${ci_run_id:-}"' "${HERE}/../merge-train/merge-train.sh" &&
2869
+ grep -qF "ci-depth-tiered: \${{ vars.CI_DEPTH_TIERED || 'false' }}" "${WORKFLOWS_DIR}/work-router.yml"; then
2870
+ PASS=$((PASS + 1))
2871
+ else
2872
+ FAIL=$((FAIL + 1))
2873
+ echo "FAIL: the merge train does not ask for the full CI before gating a light run" >&2
2874
+ fi
2875
+ # The implement agent tests only what it changed; the repository's CI runs the full suite.
2876
+ if grep -qF 'Test only what you changed; CI tests the rest.' "${WORKFLOWS_DIR}/agent-implement.md"; then
2877
+ PASS=$((PASS + 1))
2878
+ else
2879
+ FAIL=$((FAIL + 1))
2880
+ echo "FAIL: implement tests only what it changed is missing from the prompt" >&2
2881
+ fi
2882
+ # A conflicting pull request is gated on its carried verdict: CI cannot run on a head with no merge ref.
2883
+ if grep -qF '[ "$conflicting" != CONFLICTING ]' "${HERE}/../identify-gate-subject/action.yml"; then
2884
+ PASS=$((PASS + 1))
2885
+ else
2886
+ FAIL=$((FAIL + 1))
2887
+ echo "FAIL: the gate waits for CI on a conflicting pull request, which never gets a run" >&2
2888
+ fi
2831
2889
  # An empty AGENT_TIER_MODEL must remove an override a reused runner kept from an earlier job.
2832
2890
  if grep -qF 'rm -f .opencode/harness.user.json' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
2833
2891
  PASS=$((PASS + 1))
@@ -839,6 +839,16 @@ timeout-minutes: 180
839
839
 
840
840
  Never open a pull request that does not pass them.
841
841
 
842
+ **Test only what you changed; CI tests the rest.** In this run, run the tests that cover the
843
+ code you touched and nothing wider: the test files you added or edited, and the tests next to
844
+ the code you changed (web: `pnpm exec vitest related --run <changed files>`; .NET:
845
+ `dotnet test <the test project> --filter <the test classes>`). Do not run integration,
846
+ end-to-end, mutation or coverage suites here, and do not run the whole test suite to be safe:
847
+ the repository's CI runs all of it on your pull request before anything merges, on a runner
848
+ that has the database and the browsers this sandbox lacks. The verification commands above are
849
+ the repository's quick check for that reason. A full suite in the agent is the slowest part of
850
+ a run and repeats what CI does anyway.
851
+
842
852
  **Run them once, on the final tree.** They are slow: Pliny-Bot's `pnpm verify` is about
843
853
  twenty minutes of work on this runner. `pc-plan-apply` runs them in its verify step, and
844
854
  that run is this one: do not run them again when nothing changed since. Order the end of
@@ -337,7 +337,11 @@ jobs:
337
337
  - name: Check the comments and tests it adds
338
338
  id: check
339
339
  env:
340
- BASE: origin/${{ github.event.repository.default_branch || 'main' }}
340
+ # The branch the pull request merges into, not the default branch: for a stack child that is the
341
+ # feature branch. Against the default branch the base is the merge-base with it, which holds every
342
+ # sibling already landed in the stack, so Pliny-Bot #853 (2 tests of its own) was held for 28 against
343
+ # a budget of 26 that the parent's work had already spent.
344
+ BASE: origin/${{ needs.subject.outputs.pr_base || github.event.repository.default_branch || 'main' }}
341
345
  run: |
342
346
  set -uo pipefail
343
347
  if [ ! -f .opencode/scripts/check-diff.mjs ]; then
@@ -898,6 +902,13 @@ secrets:
898
902
  engine:
899
903
  id: opencode
900
904
  version: "1.2.14"
905
+ # One agent per pull request and kind, not one per repository. gh-aw's default group made every
906
+ # gate's agent job wait behind every other: Pliny-Bot's merging gate for #835 sat pending behind
907
+ # three repair agents for #846 to #848. The router already serialises what may merge (merge-belt)
908
+ # and keeps a pull request's repairs in a line of their own.
909
+ concurrency:
910
+ group: "gh-aw-opencode-agent-merge-gate-${{ inputs.pr-number }}-${{ inputs.ci-conclusion }}"
911
+ queue: max
901
912
  env:
902
913
  OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
903
914
 
@@ -1094,6 +1105,17 @@ timeout-minutes: 120
1094
1105
  use `--ours`, `--theirs`, or a blanket conflict-marker deletion without reviewing the
1095
1106
  intended behavior from both sides, and never rebase: the push is fast-forward only.
1096
1107
 
1108
+ A conflict is not always two additions. When one side deleted what the other still edits (a
1109
+ removed module, a retired block of translations), keeping the deleting side wholesale drops
1110
+ everything the pull request put inside that block, and keeping both brings the deleted code
1111
+ back. Take such a hunk apart: keep what the pull request added that still has a home, drop only
1112
+ what the base retired. After resolving a locale file or any other keyed file, list the keys the
1113
+ pull request added (its tip against its merge base) and confirm each still exists, then build
1114
+ and run the tests that use them. Pliny-Bot #837 lost its approval card strings this way and
1115
+ failed on a missing label. Two pull requests that each passed CI can also break the base
1116
+ together, one deleting a namespace the other still uses; only a green run on the current base
1117
+ says a merge is safe.
1118
+
1097
1119
  Scope verification to the files the merge touched: pass changed file paths to
1098
1120
  lint/format tools instead of running them repository-wide (see step 6's scoped
1099
1121
  verification guidance).
@@ -938,6 +938,8 @@ jobs:
938
938
  dry-run: ${{ env.MERGE_TRAIN_DRY_RUN }}
939
939
  ci-run-id: ${{ needs.classify.outputs.ci-run-id }}
940
940
  ci-conclusion: ${{ needs.classify.outputs.ci-conclusion }}
941
+ # Repository variable, read by the repository's CI as well: one switch for both sides.
942
+ ci-depth-tiered: ${{ vars.CI_DEPTH_TIERED || 'false' }}
941
943
 
942
944
  # The decision the gate parked for: a person with write access approves a pull request the
943
945
  # gate handed to them (human-review or owner-review), and the bot merges the commit they
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.6.42",
3
+ "version": "1.6.48",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",