@plainconceptsplatform/workflows 1.6.42 → 1.6.47

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) {
@@ -161,6 +161,28 @@ SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}"
161
161
  now_epoch=$(date -u +%s)
162
162
  iso_epoch() { date -u -d "$1" +%s 2>/dev/null || echo 0; }
163
163
 
164
+ # The failing jobs of the CI run on a commit, read from the Actions API. The App behind the train's token
165
+ # can dispatch workflows but may not read check runs (403 "Resource not accessible by integration"): the
166
+ # check-runs calls then returned the same error text for the head and for the base, "what fails here" was
167
+ # always a subset of "what fails on the base", and every red pull request was left waiting. Nothing is
168
+ # printed, and the status is non-zero, when the run cannot be read, so the caller claims nothing.
169
+ ci_runs_for_sha() {
170
+ local sha="$1" runs
171
+ # Every completed run on the commit, not only the CI workflow's: on the default branch the CI runs inside the
172
+ # deploy workflow. Extra failures on the base are harmless, the rule only asks that the pull request's own
173
+ # failures are among them.
174
+ 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
175
+ [ -n "$runs" ] || return 1
176
+ printf '%s
177
+ ' "$runs"
178
+ }
179
+
180
+ ci_failed_jobs() {
181
+ local run="$1"
182
+ gh api "repos/$REPO/actions/runs/$run/jobs?per_page=100" \
183
+ --jq '.jobs[] | select(.conclusion == "failure") | [(.name | sub("^.* / "; "")), .completed_at] | @tsv' 2>/dev/null
184
+ }
185
+
164
186
  summary() { printf '%s\n' "$*" >>"$SUMMARY"; }
165
187
 
166
188
  # Every write goes through this, so a dry run is honest: there is no second path that forgets.
@@ -433,15 +455,27 @@ for line in "${candidates[@]}"; do
433
455
  # the base is the pull request left alone.
434
456
  same_as_main=false
435
457
  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)
458
+ failed_here=""
459
+ failed_base=""
460
+ red_since=""
461
+ base_sha=$(gh api "repos/$REPO/commits/$base_ref" --jq .sha 2>/dev/null || true)
462
+ here_jobs=$(ci_failed_jobs "${ci_run_id:-0}" || true)
463
+ failed_here=$(cut -f1 <<<"$here_jobs" | sort -u)
464
+ # A base tip with no completed CI run (a stack branch's tip is a merge commit nothing ran on) says
465
+ # nothing about the base: the pull request is judged on its own failures.
466
+ if [ -n "$base_sha" ] && base_runs=$(ci_runs_for_sha "$base_sha"); then
467
+ base_jobs=""
468
+ for base_run in $base_runs; do
469
+ base_jobs="$base_jobs$(ci_failed_jobs "$base_run" || true)"$'
470
+ '
471
+ done
472
+ base_jobs=$(grep -v '^$' <<<"$base_jobs" || true)
473
+ failed_base=$(cut -f1 <<<"$base_jobs" | sort -u)
474
+ # How long the base has been red: the oldest failing job on its tip. The wait is bounded,
475
+ # because waiting for a base nobody fixes starves every pull request behind it. Pliny-Bot's
476
+ # main was red from 19:30 and the train waited until morning, ten hours, saying nothing.
477
+ red_since=$(cut -f2 <<<"$base_jobs" | sort | head -1)
478
+ fi
445
479
  red_for_hours=0
446
480
  if [ -n "$red_since" ]; then
447
481
  # A date that does not parse reads as epoch 0, which made main "red for 497899h" and ended the
@@ -2828,6 +2828,41 @@ 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
2831
2866
  # An empty AGENT_TIER_MODEL must remove an override a reused runner kept from an earlier job.
2832
2867
  if grep -qF 'rm -f .opencode/harness.user.json' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
2833
2868
  PASS=$((PASS + 1))
@@ -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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.6.42",
3
+ "version": "1.6.47",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",