@plainconceptsplatform/workflows 1.8.1 → 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
- for (const issue of issues) {
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
@@ -683,6 +683,17 @@ jobs:
683
683
  done
684
684
  fi
685
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
686
697
  [ -n "$waiting" ] || exit 0
687
698
  echo "::notice::#$ISSUE_NUMBER is blocked by$waiting, not yet landed; holding it."
688
699
  echo "stack-blocked=true" >> "$GITHUB_OUTPUT"
@@ -1275,13 +1286,21 @@ jobs:
1275
1286
  # so the landing does: re-adding `implement` with the App token is a bot label
1276
1287
  # event, which the router routes straight to implement. The child that just
1277
1288
  # landed counts as done even before the gate closes its issue a moment later.
1278
- for child in $children; do
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
1279
1296
  [ "$child" = "$closed_issue" ] && continue
1280
1297
  meta=$(gh issue view "$child" --repo "$REPO" --json state,labels,body 2>/dev/null || echo '{}')
1281
1298
  printf '%s' "$meta" | jq -e '(.state // "" | ascii_downcase) == "open"' >/dev/null || continue
1282
- 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
1283
1300
  blockers=$(printf '%s' "$meta" | jq -r '.body // ""' | grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
1284
- [ -n "$blockers" ] || continue
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
1285
1304
  waiting=""
1286
1305
  for b in $blockers; do
1287
1306
  [ "$b" = "$closed_issue" ] && continue
@@ -1295,7 +1314,8 @@ jobs:
1295
1314
  fi
1296
1315
  gh issue edit "$child" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
1297
1316
  gh issue edit "$child" --repo "$REPO" --add-label implement >/dev/null
1298
- 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
1299
1319
  done
1300
1320
  open_children=0
1301
1321
  for child in $children; do
@@ -1308,37 +1328,23 @@ jobs:
1308
1328
  if [ "$open_children" -gt 0 ]; then
1309
1329
  echo "$open_children child issue(s) still open on stack #$parent; the stack continues."
1310
1330
  else
1311
- # The root pull request must be green and mergeable. Green: a successful
1312
- # CI run on its head SHA. Mergeable: the same on-demand computation
1313
- # every gate dispatch already relies on; UNKNOWN settles in the
1314
- # background, so ask until it does, like mergeable_state() does.
1315
- root_state=UNKNOWN
1316
- for _ in 1 2 3 4 5 6; do
1317
- root_state=$(gh pr view "$root_pr" --repo "$REPO" --json mergeable --jq '.mergeable' 2>/dev/null || echo UNKNOWN)
1318
- [ "$root_state" = "UNKNOWN" ] || break
1319
- sleep 5
1320
- done
1321
- root_sha=$(gh api "repos/$REPO/pulls/$root_pr" --jq '.head.sha')
1322
- root_ci=$(gh api "repos/$REPO/actions/runs?head_sha=$root_sha&per_page=100" \
1323
- --jq '[.workflow_runs[]
1324
- | select(.name == env.CI_WORKFLOW_NAME and .status == "completed"
1325
- and .conclusion == "success")]
1326
- | sort_by(.created_at) | last // empty')
1327
- if [ -z "$root_ci" ] || [ "$root_state" != "MERGEABLE" ]; then
1328
- 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."
1329
1345
  else
1330
- completion_marker='<!-- stack-complete -->'
1331
- # The announcement is a pull request comment, and pull request
1332
- # comments live at the issue endpoint for the pull request number,
1333
- # so the marker is looked for where the comment was posted.
1334
- already=$(gh api "repos/$REPO/issues/$root_pr/comments?per_page=100" \
1335
- --jq "[.[] | select(.body | contains(\"$completion_marker\"))] | length" 2>/dev/null || echo 0)
1336
- if [ "${already:-0}" -gt 0 ]; then
1337
- echo "Stack #$parent already has a completion announcement; staying silent."
1338
- else
1339
- 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"
1340
- echo "Stack #$parent complete: announced root PR #$root_pr."
1341
- 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."
1342
1348
  fi
1343
1349
  fi
1344
1350
  fi
@@ -1462,7 +1468,9 @@ jobs:
1462
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
1463
1469
  continue
1464
1470
  fi
1465
- for child in ${next_children[$feature]}; do
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
1466
1474
  own=$(gh issue view "$child" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
1467
1475
  | grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
1468
1476
  waiting=""
@@ -1476,6 +1484,7 @@ jobs:
1476
1484
  gh issue edit "$child" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
1477
1485
  gh issue edit "$child" --repo "$REPO" --add-label implement >/dev/null
1478
1486
  echo "Released #$child of feature #$feature."
1487
+ break
1479
1488
  done
1480
1489
  done
1481
1490
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",