@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.
- package/loops/actions/housekeeping/action.yml +41 -0
- package/loops/actions/identify-gate-subject/action.yml +7 -1
- package/loops/actions/merge-train/action.yml +5 -0
- package/loops/actions/merge-train/merge-train.sh +101 -10
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +58 -0
- package/loops/workflows/agent-implement.md +10 -0
- package/loops/workflows/agent-merge-gate.md +23 -1
- package/loops/workflows/work-router.yml +2 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|