@plainconceptsplatform/workflows 0.20.0 → 0.20.2
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/dist/index.js +0 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +0 -0
- package/loops/actions/housekeeping/action.yml +7 -1
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +0 -0
- package/loops/actions/validate-review-output/validate-review-output.sh +16 -2
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +15 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +46 -0
- package/loops/workflows/work-router.yml +7 -0
- package/package.json +12 -11
package/dist/index.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -96,7 +96,13 @@ runs:
|
|
|
96
96
|
for (const issue of issues) {
|
|
97
97
|
if (issue.pull_request) continue;
|
|
98
98
|
const labels = names(issue);
|
|
99
|
-
|
|
99
|
+
// `stalled` alone is enough to be looked at. This used to require `review`, on the
|
|
100
|
+
// assumption that a park always sets both, and an issue that ended up carrying
|
|
101
|
+
// `stalled` without it fell through every branch below: never retried, never
|
|
102
|
+
// reported, invisible. One sat that way for thirty hours in a consumer with
|
|
103
|
+
// `implement` still on it and a janitor running every six hours past it. `stalled`
|
|
104
|
+
// is the marker the retry keys on, so it is what decides whether to look.
|
|
105
|
+
if (!labels.includes('review') && !labels.includes('stalled')) continue;
|
|
100
106
|
|
|
101
107
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
102
108
|
...context.repo, issue_number: issue.number, per_page: 100,
|
|
File without changes
|
|
@@ -15,16 +15,30 @@ if [ ! -f "$output_file" ] || [ ! -f "$threads_file" ] \
|
|
|
15
15
|
exit 0
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
|
+
# Every jq error becomes "invalid" rather than a non-zero exit, the way the merge gate's
|
|
19
|
+
# validator already does it. A crash here fails the step, which skips the apply job and parks
|
|
20
|
+
# the issue as stalled -- while the agent's fix is already pushed and sitting on the branch.
|
|
21
|
+
# Output the workflow cannot read is an outcome to report, not a reason to fall over.
|
|
18
22
|
jq -r --arg pr "$pr_number" --slurpfile threads "$threads_file" '
|
|
23
|
+
def decide:
|
|
19
24
|
.items as $items
|
|
20
25
|
| ($threads[0] | map(select(.isResolved == false and .isOutdated == false) | .id) | sort) as $expected
|
|
21
26
|
| ($items | [.[] | select(.type == "add_comment" and (.item_number | tostring) == $pr and (.body | type == "string"))]) as $comments
|
|
22
27
|
| ($comments | map(.body | if test("\\*\\*Review outcome:\\*\\*\\s*(implemented|already-satisfied|needs-human)"; "i") then capture("\\*\\*Review outcome:\\*\\*\\s*(?<v>implemented|already-satisfied|needs-human)"; "i").v | ascii_downcase else empty end) | .[0] // "invalid") as $outcome
|
|
23
|
-
|
|
28
|
+
# `add` on an empty array is null, not [], and `unique` then dies with "Cannot iterate over
|
|
29
|
+
# null". That is not an edge case: it is every pull-request-level review. Requesting changes
|
|
30
|
+
# from the Files tab, or a plain review body with no inline comment, creates a review and no
|
|
31
|
+
# thread, so both sides of this comparison are empty and the validator crashed instead of
|
|
32
|
+
# agreeing. The agent had already read the feedback, pushed the fix and reported it; the run
|
|
33
|
+
# was marked failed, the issue was labelled stalled, and the board said nothing landed while
|
|
34
|
+
# the commit sat on the branch.
|
|
35
|
+
| ($comments | map(.body | [scan("PRRT_[A-Za-z0-9_=-]+")]) | add // [] | unique | sort) as $reported
|
|
24
36
|
| ([$items[] | select(.type == "push_to_pull_request_branch")] | length) as $pushes
|
|
25
37
|
| if $expected != $reported then "invalid"
|
|
26
38
|
elif $outcome == "implemented" and $pushes == 1 then "implemented"
|
|
27
39
|
elif ($outcome == "already-satisfied" or $outcome == "needs-human") and $pushes == 0 then $outcome
|
|
28
40
|
else "invalid"
|
|
29
|
-
end
|
|
41
|
+
end;
|
|
42
|
+
|
|
43
|
+
try decide catch "invalid"
|
|
30
44
|
' "$output_file"
|
|
@@ -48,4 +48,19 @@ check("lists every disposition it saw",
|
|
|
48
48
|
run({ "auto-merge": 1, blocked: 2 }, [], []),
|
|
49
49
|
(s) => s.includes("`blocked` · 2") && s.includes("`auto-merge` · 1"));
|
|
50
50
|
|
|
51
|
+
|
|
52
|
+
// The triage loop's entry guard. An issue carrying `stalled` is the janitor's to retry; one
|
|
53
|
+
// carrying `stalled` without `review` used to be skipped before any branch could see it, so it
|
|
54
|
+
// was neither retried nor reported. Extracted and run rather than grepped, because "which
|
|
55
|
+
// issues does this loop even look at" is exactly the kind of condition a grep reads past.
|
|
56
|
+
const guardSrc = yml.match(/if \(!labels\.includes\('review'\)[^\n]*\n/);
|
|
57
|
+
if (!guardSrc) { console.error("FAIL: housekeeping has no triage entry guard"); process.exit(1); }
|
|
58
|
+
const looksAt = (labels) => !new Function("labels", `return ${guardSrc[0].trim().replace(/^if \(/, "").replace(/\)\s*continue;$/, "")};`)(labels);
|
|
59
|
+
|
|
60
|
+
check("looks at a parked issue (review + stalled)", looksAt(["review", "stalled", "implement"]), true);
|
|
61
|
+
check("looks at a decided issue (review only)", looksAt(["review", "implement"]), true);
|
|
62
|
+
check("looks at an orphaned stall (stalled, no review)", looksAt(["stalled", "implement"]), true);
|
|
63
|
+
check("ignores an issue with neither", looksAt(["implement", "sp-2"]), false);
|
|
64
|
+
check("ignores a plain refined issue", looksAt(["refined"]), false);
|
|
65
|
+
|
|
51
66
|
process.exit(failed);
|
|
@@ -1319,6 +1319,52 @@ level=low")
|
|
|
1319
1319
|
if [ "$BLAST_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1320
1320
|
fi
|
|
1321
1321
|
|
|
1322
|
+
# apply-review's validator compares the unresolved review threads on a pull request against the
|
|
1323
|
+
# thread ids the agent reported handling. Two jq shapes broke it for every input: `add` on an
|
|
1324
|
+
# empty array is null, so a pull-request-level review with no inline thread crashed on `unique`,
|
|
1325
|
+
# and `scan` without a capture group emits strings, so `add` concatenated them and `unique`
|
|
1326
|
+
# crashed on a string. The agent had already pushed the fix; the run was marked failed and the
|
|
1327
|
+
# issue labelled stalled while the commit sat on the branch.
|
|
1328
|
+
REVIEW_VALIDATOR="${HERE}/../validate-review-output/validate-review-output.sh"
|
|
1329
|
+
if [ -f "$REVIEW_VALIDATOR" ] && worker_installed apply-review; then
|
|
1330
|
+
REVIEW_OK=1
|
|
1331
|
+
rv_fixture="${TMPDIR:-/tmp}/route-matrix-review-$$"
|
|
1332
|
+
|
|
1333
|
+
rv_case() {
|
|
1334
|
+
local name="$1" want="$2" threads="$3" items="$4" got
|
|
1335
|
+
printf '%s' "$threads" > "${rv_fixture}-threads.json"
|
|
1336
|
+
printf '%s' "$items" > "${rv_fixture}-out.json"
|
|
1337
|
+
# `|| true`: a validator that crashes must show up as a failed case, not kill this script.
|
|
1338
|
+
got=$(bash "$REVIEW_VALIDATOR" "${rv_fixture}-out.json" 181 "${rv_fixture}-threads.json" 2>&1 || true)
|
|
1339
|
+
if [ "$got" != "$want" ]; then
|
|
1340
|
+
REVIEW_OK=0
|
|
1341
|
+
echo "FAIL: the apply-review validator called '${name}' ${got}, expected ${want}" >&2
|
|
1342
|
+
fi
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
rv_impl() { printf '{"items":[{"type":"add_comment","item_number":181,"body":"**Review outcome:** implemented %s"},{"type":"push_to_pull_request_branch","pr_number":181}]}' "$1"; }
|
|
1346
|
+
rv_open() { printf '{"id":"%s","isResolved":false,"isOutdated":false}' "$1"; }
|
|
1347
|
+
|
|
1348
|
+
# The case that was failing in production: requesting changes from the Files tab creates a
|
|
1349
|
+
# review and no thread, so both sides are empty and must agree rather than crash.
|
|
1350
|
+
rv_case "a review with no threads at all" implemented "[]" "$(rv_impl '')"
|
|
1351
|
+
rv_case "no threads and no push needed" needs-human "[]" '{"items":[{"type":"add_comment","item_number":181,"body":"**Review outcome:** needs-human"}]}'
|
|
1352
|
+
rv_case "one thread, reported" implemented "[$(rv_open PRRT_aaa)]" "$(rv_impl 'PRRT_aaa')"
|
|
1353
|
+
rv_case "one thread, silently skipped" invalid "[$(rv_open PRRT_aaa)]" "$(rv_impl '')"
|
|
1354
|
+
rv_case "a thread id that was never opened" invalid "[$(rv_open PRRT_aaa)]" "$(rv_impl 'PRRT_bbb')"
|
|
1355
|
+
rv_case "two threads, both reported" implemented "[$(rv_open PRRT_a),$(rv_open PRRT_b)]" "$(rv_impl 'PRRT_a and PRRT_b')"
|
|
1356
|
+
rv_case "two threads, only one reported" invalid "[$(rv_open PRRT_a),$(rv_open PRRT_b)]" "$(rv_impl 'PRRT_a')"
|
|
1357
|
+
rv_case "a resolved thread is not expected" implemented '[{"id":"PRRT_aaa","isResolved":true,"isOutdated":false}]' "$(rv_impl '')"
|
|
1358
|
+
rv_case "implemented without the push" invalid "[]" '{"items":[{"type":"add_comment","item_number":181,"body":"**Review outcome:** implemented"}]}'
|
|
1359
|
+
# Nothing addressed to this pull request at all. `map` over no comments is [], `add` on that is
|
|
1360
|
+
# null, and `unique` dies: the run fails instead of reporting the output as unusable. This is
|
|
1361
|
+
# the case the `// []` guards; the bracketed scan alone does not reach it.
|
|
1362
|
+
rv_case "a comment aimed at another pull request" invalid "[]" '{"items":[{"type":"add_comment","item_number":999,"body":"**Review outcome:** implemented"},{"type":"push_to_pull_request_branch","pr_number":181}]}'
|
|
1363
|
+
|
|
1364
|
+
rm -f "${rv_fixture}-threads.json" "${rv_fixture}-out.json"
|
|
1365
|
+
if [ "$REVIEW_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1366
|
+
fi
|
|
1367
|
+
|
|
1322
1368
|
GATE_VALIDATOR="${HERE}/../validate-merge-gate-output/validate-merge-gate-output.sh"
|
|
1323
1369
|
if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
1324
1370
|
VALIDATOR_OK=1
|
|
@@ -566,6 +566,13 @@ jobs:
|
|
|
566
566
|
# gate is about to invalidate. Serialising the belt means each gate sees a main that
|
|
567
567
|
# only changes between gates, every merge re-arms the siblings through
|
|
568
568
|
# detect-pr-conflicts's rescan, and the queue drains one pull request at a time.
|
|
569
|
+
#
|
|
570
|
+
# `cancel-in-progress: false` protects the RUNNING gate, not the waiting ones: GitHub keeps
|
|
571
|
+
# exactly one pending run per group and cancels the previously pending one when a new run
|
|
572
|
+
# arrives. That is fine for the path this belt is built for, where detect-pr-conflicts re-arms
|
|
573
|
+
# one sibling per merge. It is a trap for a person dispatching by hand: labelling four pull
|
|
574
|
+
# requests `merge-gate` in one go leaves one running, one pending and two silently cancelled,
|
|
575
|
+
# with nothing on the pull requests to say so. Dispatch one, let it finish, then the next.
|
|
569
576
|
call-merge-gate:
|
|
570
577
|
needs: classify
|
|
571
578
|
if: needs.classify.outputs.route == 'merge-gate'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plainconceptsplatform/workflows",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "Install and update Platform GitHub agentic workflows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -25,20 +25,21 @@
|
|
|
25
25
|
"loops",
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsc --project tsconfig.build.json && node ./scripts/copy-loops.mjs",
|
|
30
|
-
"test": "vitest run",
|
|
31
|
-
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
32
|
-
"release": "pnpm build && pnpm test && npm publish --access public",
|
|
33
|
-
"prepack": "node ./scripts/copy-loops.mjs"
|
|
34
|
-
},
|
|
35
28
|
"engines": {
|
|
36
29
|
"node": ">=20.19"
|
|
37
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"yaml": "^2.9.0"
|
|
33
|
+
},
|
|
38
34
|
"devDependencies": {
|
|
39
35
|
"@types/node": "^22.0.0",
|
|
40
36
|
"typescript": "^5.8.0",
|
|
41
|
-
"vitest": "^3.0.0"
|
|
42
|
-
|
|
37
|
+
"vitest": "^3.0.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc --project tsconfig.build.json && node ./scripts/copy-loops.mjs",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
43
|
+
"release": "pnpm build && pnpm test && npm publish --access public"
|
|
43
44
|
}
|
|
44
|
-
}
|
|
45
|
+
}
|