@plainconceptsplatform/workflows 1.8.1 → 1.9.0
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 +9 -1
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +36 -0
- package/loops/templates/agentics/agentics-app-errors.yml +2 -1
- package/loops/templates/agentics/agentics-error-report.yml +2 -1
- package/loops/workflows/authorize-bot-work.yml +107 -105
- package/loops/workflows/release-tag.yml +2 -0
- package/loops/workflows/work-router.yml +50 -35
- package/package.json +1 -1
|
@@ -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,42 @@ 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
|
|
2953
|
+
# A repository variable AGENTIC_WORKFLOWS_DISABLED turns everything off: the router's two root jobs
|
|
2954
|
+
# (every other job needs one of them) and every workflow with a trigger of its own.
|
|
2955
|
+
OFF_OK=1
|
|
2956
|
+
for job in authorize classify; do
|
|
2957
|
+
sed -n "/^ ${job}:\$/,/^ runs-on:/p" "${WORKFLOWS_DIR}/work-router.yml" | grep -qF "if: vars.AGENTIC_WORKFLOWS_DISABLED == ''" ||
|
|
2958
|
+
{ OFF_OK=0; echo "FAIL: the router's ${job} job ignores AGENTIC_WORKFLOWS_DISABLED" >&2; }
|
|
2959
|
+
done
|
|
2960
|
+
for f in "${WORKFLOWS_DIR}/authorize-bot-work.yml" "${WORKFLOWS_DIR}/release-tag.yml"; do
|
|
2961
|
+
[ -f "$f" ] || continue
|
|
2962
|
+
grep -qF "vars.AGENTIC_WORKFLOWS_DISABLED == ''" "$f" ||
|
|
2963
|
+
{ OFF_OK=0; echo "FAIL: $(basename "$f") ignores AGENTIC_WORKFLOWS_DISABLED" >&2; }
|
|
2964
|
+
done
|
|
2965
|
+
if [ "$OFF_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2930
2966
|
# A stack child runs the feature branch's copy of the workflows, so the router brings the default
|
|
2931
2967
|
# branch in before it dispatches one.
|
|
2932
2968
|
if grep -qF 'bring $DEFAULT_BRANCH into $STACK_BRANCH before a child runs' "${WORKFLOWS_DIR}/work-router.yml"; then
|
|
@@ -87,7 +87,8 @@ env:
|
|
|
87
87
|
jobs:
|
|
88
88
|
collect:
|
|
89
89
|
# Fork of a consumer: not their Azure, not their issues.
|
|
90
|
-
|
|
90
|
+
# AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
|
|
91
|
+
if: "!github.event.repository.fork && vars.AGENTIC_WORKFLOWS_DISABLED == ''"
|
|
91
92
|
runs-on: agents-arc
|
|
92
93
|
timeout-minutes: 20
|
|
93
94
|
# Load bearing, not decoration. The federated credential on the deploy identity trusts the
|
|
@@ -58,7 +58,8 @@ env:
|
|
|
58
58
|
jobs:
|
|
59
59
|
report:
|
|
60
60
|
# Fork of a consumer: the fleet is not theirs and neither is the report.
|
|
61
|
-
|
|
61
|
+
# AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
|
|
62
|
+
if: "!github.event.repository.fork && vars.AGENTIC_WORKFLOWS_DISABLED == ''"
|
|
62
63
|
runs-on: agents-arc
|
|
63
64
|
timeout-minutes: 20
|
|
64
65
|
concurrency:
|
|
@@ -1,105 +1,107 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/authorize-bot-work.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
# Human adds implement/refine/direct/feature label → validates permission → bot adds bot-working
|
|
3
|
-
# This ensures the bot is the actor for all agentic workflows.
|
|
4
|
-
#
|
|
5
|
-
# IMPORTANT: Only triggers for HUMAN actors. When the bot transitions refine→implement,
|
|
6
|
-
# it adds bot-working itself, so authorize-bot-work must not fire again.
|
|
7
|
-
name: "Authorize Bot Work"
|
|
8
|
-
|
|
9
|
-
run-name: "Authorizing: ${{ github.event.issue.title }} (#${{ github.event.issue.number }})"
|
|
10
|
-
|
|
11
|
-
on:
|
|
12
|
-
issues:
|
|
13
|
-
types: [labeled]
|
|
14
|
-
|
|
15
|
-
permissions:
|
|
16
|
-
contents: read
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
authorize:
|
|
20
|
-
# Only trigger for work labels from HUMANS (not bots), and only if another bot run does not
|
|
21
|
-
# already own the issue.
|
|
22
|
-
#
|
|
23
|
-
# `review` is deliberately NOT excluded. It used to be, and that made the label a one-way
|
|
24
|
-
# door: the classifier refuses to route while `review` is set, so a person adding `refine`
|
|
25
|
-
# to a parked issue got no run, no comment and no error anywhere. Triage's own
|
|
26
|
-
# needs-maintainer verdict tells the maintainer to add `refine`, which could not work.
|
|
27
|
-
#
|
|
28
|
-
# A person adding a work label IS the human review the label was waiting for, so this
|
|
29
|
-
# workflow clears it below before handing the issue to the bot. The classifier's own guard
|
|
30
|
-
# stays as it is: it exists to stop the *bot* re-triggering itself, and by the time
|
|
31
|
-
# bot-working is added `review` is already gone.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/authorize-bot-work.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
# Human adds implement/refine/direct/feature label → validates permission → bot adds bot-working
|
|
3
|
+
# This ensures the bot is the actor for all agentic workflows.
|
|
4
|
+
#
|
|
5
|
+
# IMPORTANT: Only triggers for HUMAN actors. When the bot transitions refine→implement,
|
|
6
|
+
# it adds bot-working itself, so authorize-bot-work must not fire again.
|
|
7
|
+
name: "Authorize Bot Work"
|
|
8
|
+
|
|
9
|
+
run-name: "Authorizing: ${{ github.event.issue.title }} (#${{ github.event.issue.number }})"
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
issues:
|
|
13
|
+
types: [labeled]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
authorize:
|
|
20
|
+
# Only trigger for work labels from HUMANS (not bots), and only if another bot run does not
|
|
21
|
+
# already own the issue.
|
|
22
|
+
#
|
|
23
|
+
# `review` is deliberately NOT excluded. It used to be, and that made the label a one-way
|
|
24
|
+
# door: the classifier refuses to route while `review` is set, so a person adding `refine`
|
|
25
|
+
# to a parked issue got no run, no comment and no error anywhere. Triage's own
|
|
26
|
+
# needs-maintainer verdict tells the maintainer to add `refine`, which could not work.
|
|
27
|
+
#
|
|
28
|
+
# A person adding a work label IS the human review the label was waiting for, so this
|
|
29
|
+
# workflow clears it below before handing the issue to the bot. The classifier's own guard
|
|
30
|
+
# stays as it is: it exists to stop the *bot* re-triggering itself, and by the time
|
|
31
|
+
# bot-working is added `review` is already gone.
|
|
32
|
+
# AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
|
|
33
|
+
if: >
|
|
34
|
+
vars.AGENTIC_WORKFLOWS_DISABLED == '' &&
|
|
35
|
+
(github.event.label.name == 'implement' ||
|
|
36
|
+
github.event.label.name == 'refine') &&
|
|
37
|
+
!contains(github.event.issue.labels.*.name, 'bot-working') &&
|
|
38
|
+
!endsWith(github.actor, '[bot]')
|
|
39
|
+
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
40
|
+
timeout-minutes: 5
|
|
41
|
+
concurrency:
|
|
42
|
+
group: authorize-${{ github.event.issue.number }}
|
|
43
|
+
cancel-in-progress: false
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
issues: write
|
|
47
|
+
steps:
|
|
48
|
+
- name: Check actor permission
|
|
49
|
+
id: check
|
|
50
|
+
env:
|
|
51
|
+
GH_TOKEN: ${{ github.token }}
|
|
52
|
+
ACTOR: ${{ github.actor }}
|
|
53
|
+
run: |
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$ACTOR/permission" \
|
|
57
|
+
--jq '.permission' 2>/dev/null || echo "none")
|
|
58
|
+
|
|
59
|
+
case "$permission" in
|
|
60
|
+
admin | maintain | write)
|
|
61
|
+
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
|
62
|
+
echo "::notice::$ACTOR has $permission permission - authorized"
|
|
63
|
+
;;
|
|
64
|
+
*)
|
|
65
|
+
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
|
66
|
+
echo "::warning::$ACTOR has '$permission' permission - not authorized"
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
|
|
70
|
+
- name: Checkout for App token action
|
|
71
|
+
if: steps.check.outputs.authorized == 'true'
|
|
72
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
73
|
+
with:
|
|
74
|
+
persist-credentials: false
|
|
75
|
+
|
|
76
|
+
- name: Generate App token
|
|
77
|
+
if: steps.check.outputs.authorized == 'true'
|
|
78
|
+
id: app-token
|
|
79
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
80
|
+
with:
|
|
81
|
+
app-id: ${{ secrets.BOT_APP_ID }}
|
|
82
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
83
|
+
|
|
84
|
+
- name: Bot takes the issue over
|
|
85
|
+
if: steps.check.outputs.authorized == 'true'
|
|
86
|
+
env:
|
|
87
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
88
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
89
|
+
LABEL: ${{ github.event.label.name }}
|
|
90
|
+
run: |
|
|
91
|
+
set -euo pipefail
|
|
92
|
+
|
|
93
|
+
# Clear review FIRST. A person adding a work label is the human review the label was
|
|
94
|
+
# waiting for, and the classifier refuses to route an issue that still carries it, so
|
|
95
|
+
# removing it here is what makes the hand-off work at all. It also has to happen
|
|
96
|
+
# before bot-working, because that is the event the classifier reads: adding
|
|
97
|
+
# bot-working first would raise an event whose payload still shows review.
|
|
98
|
+
# --remove-label on an absent label is a no-op, so this is safe on a fresh issue.
|
|
99
|
+
# `stalled` goes with it: a person taking the issue on restarts the work, so the
|
|
100
|
+
# marker the janitor retries on must not survive into the new run.
|
|
101
|
+
gh issue edit "$ISSUE_NUMBER" --remove-label "review" --remove-label "stalled" 2>/dev/null ||
|
|
102
|
+
echo "::notice::no review or stalled label to clear on #$ISSUE_NUMBER"
|
|
103
|
+
|
|
104
|
+
echo "Adding bot-working label to issue #$ISSUE_NUMBER (triggered by $LABEL)"
|
|
105
|
+
gh issue edit "$ISSUE_NUMBER" --add-label "bot-working"
|
|
106
|
+
|
|
107
|
+
echo "::notice::bot-working label added - bot will now own the $LABEL workflow"
|
|
@@ -27,7 +27,9 @@ concurrency:
|
|
|
27
27
|
jobs:
|
|
28
28
|
tag:
|
|
29
29
|
name: Tag and publish the release
|
|
30
|
+
# AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
|
|
30
31
|
if: >-
|
|
32
|
+
vars.AGENTIC_WORKFLOWS_DISABLED == '' &&
|
|
31
33
|
startsWith(github.event.head_commit.message, 'chore(release): v')
|
|
32
34
|
runs-on: agents-arc
|
|
33
35
|
timeout-minutes: 10
|
|
@@ -307,6 +307,10 @@ permissions:
|
|
|
307
307
|
jobs:
|
|
308
308
|
# Public repositories must check repository permission before an event can start a model.
|
|
309
309
|
authorize:
|
|
310
|
+
# Off switch: a repository variable AGENTIC_WORKFLOWS_DISABLED, with any value, stops every
|
|
311
|
+
# route. Every other job needs this one or classify, so nothing runs. For a repository that has
|
|
312
|
+
# the workflows installed but is not ready for them.
|
|
313
|
+
if: vars.AGENTIC_WORKFLOWS_DISABLED == ''
|
|
310
314
|
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
311
315
|
timeout-minutes: 5
|
|
312
316
|
permissions:
|
|
@@ -371,6 +375,8 @@ jobs:
|
|
|
371
375
|
esac
|
|
372
376
|
|
|
373
377
|
classify:
|
|
378
|
+
# The same off switch as authorize; jobs that need only classify stop here.
|
|
379
|
+
if: vars.AGENTIC_WORKFLOWS_DISABLED == ''
|
|
374
380
|
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
375
381
|
timeout-minutes: 5
|
|
376
382
|
permissions:
|
|
@@ -683,6 +689,17 @@ jobs:
|
|
|
683
689
|
done
|
|
684
690
|
fi
|
|
685
691
|
fi
|
|
692
|
+
# One child of a stack at a time. A sibling's pull request still open on the feature branch
|
|
693
|
+
# means this child would cut from a branch that is about to change under it, and whichever
|
|
694
|
+
# lands second conflicts (Odyssey feat/323: three children built side by side, two left
|
|
695
|
+
# DIRTY). That sibling's landing releases the next child. The child's own pull request,
|
|
696
|
+
# if it has one, is not a sibling.
|
|
697
|
+
if [ -z "$waiting" ]; then
|
|
698
|
+
for pr in $(gh pr list --repo "$REPO" --state open --base "$STACK_BRANCH" --limit 100 --json number,body \
|
|
699
|
+
--jq ".[] | select(.body | test(\"(?i)(close[sd]?|fix(e[sd])?|resolve[sd]?) #${ISSUE_NUMBER}([^0-9]|$)\") | not) | .number" 2>/dev/null || true); do
|
|
700
|
+
waiting="$waiting #$pr"
|
|
701
|
+
done
|
|
702
|
+
fi
|
|
686
703
|
[ -n "$waiting" ] || exit 0
|
|
687
704
|
echo "::notice::#$ISSUE_NUMBER is blocked by$waiting, not yet landed; holding it."
|
|
688
705
|
echo "stack-blocked=true" >> "$GITHUB_OUTPUT"
|
|
@@ -1275,13 +1292,21 @@ jobs:
|
|
|
1275
1292
|
# so the landing does: re-adding `implement` with the App token is a bot label
|
|
1276
1293
|
# event, which the router routes straight to implement. The child that just
|
|
1277
1294
|
# landed counts as done even before the gate closes its issue a moment later.
|
|
1278
|
-
|
|
1295
|
+
#
|
|
1296
|
+
# One child per landing, the lowest-numbered one that is free. Releasing every free
|
|
1297
|
+
# sibling at once started three implements on Odyssey's feat/323 within a second;
|
|
1298
|
+
# each cut from the same branch, and every landing left the others conflicting. The
|
|
1299
|
+
# next landing releases the next child. A held child with no `Blocked by` of its
|
|
1300
|
+
# own (held because a sibling was in flight) is free once it carries the hold marker.
|
|
1301
|
+
for child in $(printf '%s\n' $children | sort -n); do
|
|
1279
1302
|
[ "$child" = "$closed_issue" ] && continue
|
|
1280
1303
|
meta=$(gh issue view "$child" --repo "$REPO" --json state,labels,body 2>/dev/null || echo '{}')
|
|
1281
1304
|
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
|
|
1305
|
+
printf '%s' "$meta" | jq -e '[.labels[]?.name] | (index("implement") != null) and (index("bot-working") == null) and (index("pr-pending") == null)' >/dev/null || continue
|
|
1283
1306
|
blockers=$(printf '%s' "$meta" | jq -r '.body // ""' | grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|
|
1284
|
-
[ -
|
|
1307
|
+
if [ -z "$blockers" ]; then
|
|
1308
|
+
gh api "repos/$REPO/issues/$child/comments?per_page=100" --jq '.[].body' 2>/dev/null | grep -qF '<!-- stack-held -->' || continue
|
|
1309
|
+
fi
|
|
1285
1310
|
waiting=""
|
|
1286
1311
|
for b in $blockers; do
|
|
1287
1312
|
[ "$b" = "$closed_issue" ] && continue
|
|
@@ -1295,7 +1320,8 @@ jobs:
|
|
|
1295
1320
|
fi
|
|
1296
1321
|
gh issue edit "$child" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
|
|
1297
1322
|
gh issue edit "$child" --repo "$REPO" --add-label implement >/dev/null
|
|
1298
|
-
echo "Released #$child: everything it was blocked by has landed."
|
|
1323
|
+
echo "Released #$child: everything it was blocked by has landed. The next landing releases the next one."
|
|
1324
|
+
break
|
|
1299
1325
|
done
|
|
1300
1326
|
open_children=0
|
|
1301
1327
|
for child in $children; do
|
|
@@ -1308,37 +1334,23 @@ jobs:
|
|
|
1308
1334
|
if [ "$open_children" -gt 0 ]; then
|
|
1309
1335
|
echo "$open_children child issue(s) still open on stack #$parent; the stack continues."
|
|
1310
1336
|
else
|
|
1311
|
-
#
|
|
1312
|
-
#
|
|
1313
|
-
#
|
|
1314
|
-
#
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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."
|
|
1337
|
+
# Announce as soon as the last child lands, whatever the root's CI is doing at
|
|
1338
|
+
# this instant. The landing itself pushes to the feature branch, so the root's CI
|
|
1339
|
+
# is usually still running here; waiting for green left the announcement unposted
|
|
1340
|
+
# with nothing to post it later but the six-hourly housekeeping (Odyssey #332 sat
|
|
1341
|
+
# with an auto-merge verdict, held, from 00:09 until a person announced it). The
|
|
1342
|
+
# gate still reads CI and mergeability itself before it merges the root.
|
|
1343
|
+
completion_marker='<!-- stack-complete -->'
|
|
1344
|
+
# The announcement is a pull request comment, and pull request
|
|
1345
|
+
# comments live at the issue endpoint for the pull request number,
|
|
1346
|
+
# so the marker is looked for where the comment was posted.
|
|
1347
|
+
already=$(gh api "repos/$REPO/issues/$root_pr/comments?per_page=100" \
|
|
1348
|
+
--jq "[.[] | select(.body | contains(\"$completion_marker\"))] | length" 2>/dev/null || echo 0)
|
|
1349
|
+
if [ "${already:-0}" -gt 0 ]; then
|
|
1350
|
+
echo "Stack #$parent already has a completion announcement; staying silent."
|
|
1329
1351
|
else
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
|
1352
|
+
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"
|
|
1353
|
+
echo "Stack #$parent complete: announced root PR #$root_pr."
|
|
1342
1354
|
fi
|
|
1343
1355
|
fi
|
|
1344
1356
|
fi
|
|
@@ -1462,7 +1474,9 @@ jobs:
|
|
|
1462
1474
|
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
1475
|
continue
|
|
1464
1476
|
fi
|
|
1465
|
-
|
|
1477
|
+
# One child per feature, the lowest-numbered free one; each landing on the feature branch
|
|
1478
|
+
# releases the next (land-stack-child), so siblings never build side by side.
|
|
1479
|
+
for child in $(printf '%s\n' ${next_children[$feature]} | sort -n); do
|
|
1466
1480
|
own=$(gh issue view "$child" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
1467
1481
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|
|
1468
1482
|
waiting=""
|
|
@@ -1476,6 +1490,7 @@ jobs:
|
|
|
1476
1490
|
gh issue edit "$child" --repo "$REPO" --remove-label implement >/dev/null 2>&1 || true
|
|
1477
1491
|
gh issue edit "$child" --repo "$REPO" --add-label implement >/dev/null
|
|
1478
1492
|
echo "Released #$child of feature #$feature."
|
|
1493
|
+
break
|
|
1479
1494
|
done
|
|
1480
1495
|
done
|
|
1481
1496
|
|