@plainconceptsplatform/workflows 1.6.41 → 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,18 +455,37 @@ 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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
482
|
+
# wait at once (Pliny-Bot repaired five pull requests for failures main had too). No date, no claim.
|
|
483
|
+
red_epoch=$(iso_epoch "$red_since")
|
|
484
|
+
if [ "$red_epoch" -gt 0 ]; then
|
|
485
|
+
red_for_hours=$(( (now_epoch - red_epoch) / 3600 ))
|
|
486
|
+
else
|
|
487
|
+
echo "::warning::Could not read when $base_ref went red ('$red_since'); waiting for it as if it just did."
|
|
488
|
+
fi
|
|
448
489
|
fi
|
|
449
490
|
if [ -n "$failed_here" ] && [ -n "$failed_base" ] &&
|
|
450
491
|
[ -z "$(comm -23 <(sort <<<"$failed_here") <(sort <<<"$failed_base"))" ]; then
|
|
@@ -2821,6 +2821,48 @@ else
|
|
|
2821
2821
|
FAIL=$((FAIL + 1))
|
|
2822
2822
|
echo "FAIL: implement flags an issue stalled next to the pull request it just opened" >&2
|
|
2823
2823
|
fi
|
|
2824
|
+
# A date the train cannot parse must not read as "main has been red since 1970".
|
|
2825
|
+
if grep -qF 'Could not read when $base_ref went red' "${HERE}/../merge-train/merge-train.sh"; then
|
|
2826
|
+
PASS=$((PASS + 1))
|
|
2827
|
+
else
|
|
2828
|
+
FAIL=$((FAIL + 1))
|
|
2829
|
+
echo "FAIL: the merge train treats an unparsable red-since date as an endless red main" >&2
|
|
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
|
|
2824
2866
|
# An empty AGENT_TIER_MODEL must remove an override a reused runner kept from an earlier job.
|
|
2825
2867
|
if grep -qF 'rm -f .opencode/harness.user.json' "${WORKFLOWS_DIR}/shared/opencode-ci.md"; then
|
|
2826
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
|
-
|
|
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).
|