@plainconceptsplatform/workflows 1.8.0 → 1.8.3
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.
|
@@ -223,10 +223,17 @@ runs:
|
|
|
223
223
|
// every sweep.
|
|
224
224
|
const RELEASE_MARKER = '<!-- housekeeping-release -->';
|
|
225
225
|
let released = 0;
|
|
226
|
-
|
|
226
|
+
// One child per feature per sweep, the lowest-numbered: children of one stack build on the
|
|
227
|
+
// same branch, and releasing several at once lets them build side by side and conflict.
|
|
228
|
+
// The router holds a child while a sibling's pull request is open, and the sibling's
|
|
229
|
+
// landing releases the next one.
|
|
230
|
+
const releasedFeatures = new Set();
|
|
231
|
+
for (const issue of [...issues].sort((a, b) => a.number - b.number)) {
|
|
227
232
|
if (issue.pull_request) continue;
|
|
228
233
|
const labels = names(issue);
|
|
229
234
|
if (!labels.includes('implement')) continue;
|
|
235
|
+
const feature = /<!-- split-parent: (\d+) -->/.exec(issue.body ?? '')?.[1];
|
|
236
|
+
if (feature && releasedFeatures.has(feature)) continue;
|
|
230
237
|
if (['bot-working', 'stalled', 'review', 'pr-pending'].some((l) => labels.includes(l))) continue;
|
|
231
238
|
const thread = await github.paginate(github.rest.issues.listComments, {
|
|
232
239
|
...context.repo, issue_number: issue.number, per_page: 100,
|
|
@@ -252,6 +259,7 @@ runs:
|
|
|
252
259
|
inputs: { operation: 'implement', 'issue-number': String(issue.number) },
|
|
253
260
|
});
|
|
254
261
|
});
|
|
262
|
+
if (feature) releasedFeatures.add(feature);
|
|
255
263
|
released += 1;
|
|
256
264
|
}
|
|
257
265
|
|
|
@@ -2927,6 +2927,29 @@ else
|
|
|
2927
2927
|
FAIL=$((FAIL + 1))
|
|
2928
2928
|
echo "FAIL: housekeeping does not announce a stack whose children all closed outside the router" >&2
|
|
2929
2929
|
fi
|
|
2930
|
+
# The router announces a finished stack when the last child lands, not only when the root's CI
|
|
2931
|
+
# happens to be green at that instant: the landing pushes to the root, so its CI is usually still
|
|
2932
|
+
# running, and an unannounced root is held by the gate whatever its verdict (Odyssey #332).
|
|
2933
|
+
if grep -qF 'not green/mergeable yet' "${WORKFLOWS_DIR}/work-router.yml"; then
|
|
2934
|
+
FAIL=$((FAIL + 1))
|
|
2935
|
+
echo "FAIL: the stack-complete announcement waits for the root's CI, and nothing re-announces it" >&2
|
|
2936
|
+
else
|
|
2937
|
+
PASS=$((PASS + 1))
|
|
2938
|
+
fi
|
|
2939
|
+
# Children of one stack build one at a time: every releaser lets one child go per feature, and the
|
|
2940
|
+
# router holds a child while a sibling's pull request is open on the feature branch (Odyssey
|
|
2941
|
+
# feat/323 built three children side by side and two landed conflicting).
|
|
2942
|
+
ONE_AT_A_TIME_OK=1
|
|
2943
|
+
grep -qF 'The next landing releases the next one."' "${WORKFLOWS_DIR}/work-router.yml" ||
|
|
2944
|
+
{ ONE_AT_A_TIME_OK=0; echo "FAIL: land-stack-child releases every free sibling at once" >&2; }
|
|
2945
|
+
grep -qF 'echo "Released #$child of feature #$feature."' "${WORKFLOWS_DIR}/work-router.yml" &&
|
|
2946
|
+
grep -A1 -F 'echo "Released #$child of feature #$feature."' "${WORKFLOWS_DIR}/work-router.yml" | grep -qE '^\s+break$' ||
|
|
2947
|
+
{ ONE_AT_A_TIME_OK=0; echo "FAIL: stack-landed releases every free child of the next feature at once" >&2; }
|
|
2948
|
+
grep -qF -- '--base "$STACK_BRANCH" --limit 100 --json number,body' "${WORKFLOWS_DIR}/work-router.yml" ||
|
|
2949
|
+
{ ONE_AT_A_TIME_OK=0; echo "FAIL: check-stack-branch dispatches a child while a sibling's pull request is open" >&2; }
|
|
2950
|
+
grep -qF 'releasedFeatures' "${HERE}/../housekeeping/action.yml" ||
|
|
2951
|
+
{ ONE_AT_A_TIME_OK=0; echo "FAIL: housekeeping releases several children of one feature in a sweep" >&2; }
|
|
2952
|
+
if [ "$ONE_AT_A_TIME_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2930
2953
|
# A stack child runs the feature branch's copy of the workflows, so the router brings the default
|
|
2931
2954
|
# branch in before it dispatches one.
|
|
2932
2955
|
if grep -qF 'bring $DEFAULT_BRANCH into $STACK_BRANCH before a child runs' "${WORKFLOWS_DIR}/work-router.yml"; then
|
|
@@ -542,6 +542,7 @@ jobs:
|
|
|
542
542
|
stack-branch: ${{ steps.stack.outputs.stack-branch }}
|
|
543
543
|
stack-in-flight: ${{ steps.inflight.outputs.stack-in-flight }}
|
|
544
544
|
stack-blocked: ${{ steps.blocked.outputs.stack-blocked }}
|
|
545
|
+
flat-blocked: ${{ steps.flatblocked.outputs.flat-blocked }}
|
|
545
546
|
implement-queue: ${{ steps.queue.outputs.queue }}
|
|
546
547
|
steps:
|
|
547
548
|
# Which implement queue this issue waits in. The children of one refine share a queue,
|
|
@@ -682,6 +683,17 @@ jobs:
|
|
|
682
683
|
done
|
|
683
684
|
fi
|
|
684
685
|
fi
|
|
686
|
+
# One child of a stack at a time. A sibling's pull request still open on the feature branch
|
|
687
|
+
# means this child would cut from a branch that is about to change under it, and whichever
|
|
688
|
+
# lands second conflicts (Odyssey feat/323: three children built side by side, two left
|
|
689
|
+
# DIRTY). That sibling's landing releases the next child. The child's own pull request,
|
|
690
|
+
# if it has one, is not a sibling.
|
|
691
|
+
if [ -z "$waiting" ]; then
|
|
692
|
+
for pr in $(gh pr list --repo "$REPO" --state open --base "$STACK_BRANCH" --limit 100 --json number,body \
|
|
693
|
+
--jq ".[] | select(.body | test(\"(?i)(close[sd]?|fix(e[sd])?|resolve[sd]?) #${ISSUE_NUMBER}([^0-9]|$)\") | not) | .number" 2>/dev/null || true); do
|
|
694
|
+
waiting="$waiting #$pr"
|
|
695
|
+
done
|
|
696
|
+
fi
|
|
685
697
|
[ -n "$waiting" ] || exit 0
|
|
686
698
|
echo "::notice::#$ISSUE_NUMBER is blocked by$waiting, not yet landed; holding it."
|
|
687
699
|
echo "stack-blocked=true" >> "$GITHUB_OUTPUT"
|
|
@@ -695,6 +707,38 @@ jobs:
|
|
|
695
707
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body-file /tmp/held.md >/dev/null || true
|
|
696
708
|
fi
|
|
697
709
|
|
|
710
|
+
# Unattended, a feature refine did not split takes the flat path, which has no stack to wait
|
|
711
|
+
# in. Blocked by a feature that has not landed, it would build on a default branch without
|
|
712
|
+
# that feature. It is held the same way a child is, and stack-landed releases it.
|
|
713
|
+
- name: Hold an unsplit feature whose blockers have not landed
|
|
714
|
+
id: flatblocked
|
|
715
|
+
if: steps.stack.outputs.is-stack != 'true'
|
|
716
|
+
env:
|
|
717
|
+
GH_TOKEN: ${{ github.token }}
|
|
718
|
+
REPO: ${{ github.repository }}
|
|
719
|
+
ISSUE_NUMBER: ${{ needs.classify.outputs.issue-number }}
|
|
720
|
+
run: |
|
|
721
|
+
set -euo pipefail
|
|
722
|
+
echo "flat-blocked=false" >> "$GITHUB_OUTPUT"
|
|
723
|
+
[ "${UNATTENDED:-false}" = true ] || exit 0
|
|
724
|
+
waiting=""
|
|
725
|
+
for b in $( { gh api "repos/$REPO/issues/$ISSUE_NUMBER/dependencies/blocked_by" --jq '.[].number' 2>/dev/null || true
|
|
726
|
+
gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
727
|
+
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' || true
|
|
728
|
+
} | sort -u ); do
|
|
729
|
+
gh issue view "$b" --repo "$REPO" --json state --jq '.state' 2>/dev/null | grep -qix open || continue
|
|
730
|
+
waiting="$waiting #$b"
|
|
731
|
+
done
|
|
732
|
+
[ -n "$waiting" ] || exit 0
|
|
733
|
+
echo "::notice::#$ISSUE_NUMBER is blocked by$waiting, not yet landed; holding it."
|
|
734
|
+
echo "flat-blocked=true" >> "$GITHUB_OUTPUT"
|
|
735
|
+
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --remove-label bot-working >/dev/null 2>&1 || true
|
|
736
|
+
marker='<!-- stack-held -->'
|
|
737
|
+
if ! gh api "repos/$REPO/issues/$ISSUE_NUMBER/comments?per_page=100" --jq '.[].body' 2>/dev/null | grep -qF "$marker"; then
|
|
738
|
+
printf '%s\nHeld until%s have landed on the default branch: this feature builds on them. It keeps `implement` and is released when they merge.\n' "$marker" "$waiting" > /tmp/held.md
|
|
739
|
+
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body-file /tmp/held.md >/dev/null || true
|
|
740
|
+
fi
|
|
741
|
+
|
|
698
742
|
- name: Detect an in-flight implement on the feature branch
|
|
699
743
|
id: inflight
|
|
700
744
|
if: steps.stack.outputs.is-stack == 'true'
|
|
@@ -720,7 +764,7 @@ jobs:
|
|
|
720
764
|
|
|
721
765
|
call-implement:
|
|
722
766
|
needs: [classify, authorize, check-implement-pr, check-stack-branch]
|
|
723
|
-
if: needs.classify.outputs.route == 'implement' && needs.authorize.outputs.trusted == 'true' && needs.check-implement-pr.outputs.has-open-pr != 'true' && needs.check-stack-branch.outputs.is-stack != 'true'
|
|
767
|
+
if: needs.classify.outputs.route == 'implement' && needs.authorize.outputs.trusted == 'true' && needs.check-implement-pr.outputs.has-open-pr != 'true' && needs.check-stack-branch.outputs.is-stack != 'true' && needs.check-stack-branch.outputs.flat-blocked != 'true'
|
|
724
768
|
uses: ./.github/workflows/agent-implement.lock.yml
|
|
725
769
|
# One FIFO queue per IMPLEMENT_PARALLELISM slot, chosen by check-stack-branch: the children of
|
|
726
770
|
# one refine always share a queue, independent issues spread across them.
|
|
@@ -1242,13 +1286,21 @@ jobs:
|
|
|
1242
1286
|
# so the landing does: re-adding `implement` with the App token is a bot label
|
|
1243
1287
|
# event, which the router routes straight to implement. The child that just
|
|
1244
1288
|
# landed counts as done even before the gate closes its issue a moment later.
|
|
1245
|
-
|
|
1289
|
+
#
|
|
1290
|
+
# One child per landing, the lowest-numbered one that is free. Releasing every free
|
|
1291
|
+
# sibling at once started three implements on Odyssey's feat/323 within a second;
|
|
1292
|
+
# each cut from the same branch, and every landing left the others conflicting. The
|
|
1293
|
+
# next landing releases the next child. A held child with no `Blocked by` of its
|
|
1294
|
+
# own (held because a sibling was in flight) is free once it carries the hold marker.
|
|
1295
|
+
for child in $(printf '%s\n' $children | sort -n); do
|
|
1246
1296
|
[ "$child" = "$closed_issue" ] && continue
|
|
1247
1297
|
meta=$(gh issue view "$child" --repo "$REPO" --json state,labels,body 2>/dev/null || echo '{}')
|
|
1248
1298
|
printf '%s' "$meta" | jq -e '(.state // "" | ascii_downcase) == "open"' >/dev/null || continue
|
|
1249
|
-
printf '%s' "$meta" | jq -e '[.labels[]?.name] | (index("implement") != null) and (index("bot-working") == null)' >/dev/null || continue
|
|
1299
|
+
printf '%s' "$meta" | jq -e '[.labels[]?.name] | (index("implement") != null) and (index("bot-working") == null) and (index("pr-pending") == null)' >/dev/null || continue
|
|
1250
1300
|
blockers=$(printf '%s' "$meta" | jq -r '.body // ""' | grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|
|
1251
|
-
[ -
|
|
1301
|
+
if [ -z "$blockers" ]; then
|
|
1302
|
+
gh api "repos/$REPO/issues/$child/comments?per_page=100" --jq '.[].body' 2>/dev/null | grep -qF '<!-- stack-held -->' || continue
|
|
1303
|
+
fi
|
|
1252
1304
|
waiting=""
|
|
1253
1305
|
for b in $blockers; do
|
|
1254
1306
|
[ "$b" = "$closed_issue" ] && continue
|
|
@@ -1262,7 +1314,8 @@ jobs:
|
|
|
1262
1314
|
fi
|
|
1263
1315
|
gh issue edit "$child" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
|
|
1264
1316
|
gh issue edit "$child" --repo "$REPO" --add-label implement >/dev/null
|
|
1265
|
-
echo "Released #$child: everything it was blocked by has landed."
|
|
1317
|
+
echo "Released #$child: everything it was blocked by has landed. The next landing releases the next one."
|
|
1318
|
+
break
|
|
1266
1319
|
done
|
|
1267
1320
|
open_children=0
|
|
1268
1321
|
for child in $children; do
|
|
@@ -1275,37 +1328,23 @@ jobs:
|
|
|
1275
1328
|
if [ "$open_children" -gt 0 ]; then
|
|
1276
1329
|
echo "$open_children child issue(s) still open on stack #$parent; the stack continues."
|
|
1277
1330
|
else
|
|
1278
|
-
#
|
|
1279
|
-
#
|
|
1280
|
-
#
|
|
1281
|
-
#
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
and .conclusion == "success")]
|
|
1293
|
-
| sort_by(.created_at) | last // empty')
|
|
1294
|
-
if [ -z "$root_ci" ] || [ "$root_state" != "MERGEABLE" ]; then
|
|
1295
|
-
echo "Root PR #$root_pr is not green/mergeable yet; the human will merge when it is."
|
|
1331
|
+
# Announce as soon as the last child lands, whatever the root's CI is doing at
|
|
1332
|
+
# this instant. The landing itself pushes to the feature branch, so the root's CI
|
|
1333
|
+
# is usually still running here; waiting for green left the announcement unposted
|
|
1334
|
+
# with nothing to post it later but the six-hourly housekeeping (Odyssey #332 sat
|
|
1335
|
+
# with an auto-merge verdict, held, from 00:09 until a person announced it). The
|
|
1336
|
+
# gate still reads CI and mergeability itself before it merges the root.
|
|
1337
|
+
completion_marker='<!-- stack-complete -->'
|
|
1338
|
+
# The announcement is a pull request comment, and pull request
|
|
1339
|
+
# comments live at the issue endpoint for the pull request number,
|
|
1340
|
+
# so the marker is looked for where the comment was posted.
|
|
1341
|
+
already=$(gh api "repos/$REPO/issues/$root_pr/comments?per_page=100" \
|
|
1342
|
+
--jq "[.[] | select(.body | contains(\"$completion_marker\"))] | length" 2>/dev/null || echo 0)
|
|
1343
|
+
if [ "${already:-0}" -gt 0 ]; then
|
|
1344
|
+
echo "Stack #$parent already has a completion announcement; staying silent."
|
|
1296
1345
|
else
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
# comments live at the issue endpoint for the pull request number,
|
|
1300
|
-
# so the marker is looked for where the comment was posted.
|
|
1301
|
-
already=$(gh api "repos/$REPO/issues/$root_pr/comments?per_page=100" \
|
|
1302
|
-
--jq "[.[] | select(.body | contains(\"$completion_marker\"))] | length" 2>/dev/null || echo 0)
|
|
1303
|
-
if [ "${already:-0}" -gt 0 ]; then
|
|
1304
|
-
echo "Stack #$parent already has a completion announcement; staying silent."
|
|
1305
|
-
else
|
|
1306
|
-
gh pr comment "$root_pr" --repo "$REPO" --body "Stack complete: all children have merged into the feature branch and CI is green. This root pull request is ready to merge into $DEFAULT_BRANCH: the merge gate decides it next, and a person is asked only when its blast radius or a finding says so. $completion_marker"
|
|
1307
|
-
echo "Stack #$parent complete: announced root PR #$root_pr."
|
|
1308
|
-
fi
|
|
1346
|
+
gh pr comment "$root_pr" --repo "$REPO" --body "Stack complete: all children have merged into the feature branch. This root pull request is ready to merge into $DEFAULT_BRANCH once its CI is green: the merge gate decides it next, and a person is asked only when its blast radius or a finding says so. $completion_marker"
|
|
1347
|
+
echo "Stack #$parent complete: announced root PR #$root_pr."
|
|
1309
1348
|
fi
|
|
1310
1349
|
fi
|
|
1311
1350
|
fi
|
|
@@ -1429,7 +1468,9 @@ jobs:
|
|
|
1429
1468
|
gh issue comment "$feature" --repo "$REPO" --body "Feature #$parent landed on $DEFAULT_BRANCH, but \`$DEFAULT_BRANCH\` could not be merged into \`$branch\` ($answer). The children of this feature stay held until a person resolves the branch and re-adds \`implement\` to the first one." >/dev/null || true
|
|
1430
1469
|
continue
|
|
1431
1470
|
fi
|
|
1432
|
-
|
|
1471
|
+
# One child per feature, the lowest-numbered free one; each landing on the feature branch
|
|
1472
|
+
# releases the next (land-stack-child), so siblings never build side by side.
|
|
1473
|
+
for child in $(printf '%s\n' ${next_children[$feature]} | sort -n); do
|
|
1433
1474
|
own=$(gh issue view "$child" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
1434
1475
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|
|
1435
1476
|
waiting=""
|
|
@@ -1443,9 +1484,24 @@ jobs:
|
|
|
1443
1484
|
gh issue edit "$child" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
|
|
1444
1485
|
gh issue edit "$child" --repo "$REPO" --add-label implement >/dev/null
|
|
1445
1486
|
echo "Released #$child of feature #$feature."
|
|
1487
|
+
break
|
|
1446
1488
|
done
|
|
1447
1489
|
done
|
|
1448
1490
|
|
|
1491
|
+
# A feature refine did not split waits on the issue itself, held by check-stack-branch with
|
|
1492
|
+
# `implement` kept. It cuts from the default branch, which now has what it waited on.
|
|
1493
|
+
flat=$(gh issue list --repo "$REPO" --state open --label implement --limit 200 \
|
|
1494
|
+
--json number,labels,body \
|
|
1495
|
+
--jq '.[] | select([.labels[].name] | index("bot-working") | not)
|
|
1496
|
+
| select(.body | test("<!-- split-parent: [0-9]+ -->") | not) | .number')
|
|
1497
|
+
for issue in $flat; do
|
|
1498
|
+
depends_on_parent "$issue" || continue
|
|
1499
|
+
[ -z "$(blockers_of "$issue")" ] || { echo "#$issue still waits on other work."; continue; }
|
|
1500
|
+
gh issue edit "$issue" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
|
|
1501
|
+
gh issue edit "$issue" --repo "$REPO" --add-label implement >/dev/null
|
|
1502
|
+
echo "Released unsplit feature #$issue."
|
|
1503
|
+
done
|
|
1504
|
+
|
|
1449
1505
|
# The report: what merged without a person, and what the gate noted on the way. One issue
|
|
1450
1506
|
# per feature, written once; its marker makes a rerun a no-op.
|
|
1451
1507
|
marker="<!-- unattended-report: $parent -->"
|