@plainconceptsplatform/workflows 0.20.1 → 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.
@@ -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
- if (!labels.includes('review')) continue;
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,
@@ -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
- | ($comments | map(.body | scan("PRRT_[A-Za-z0-9_=-]+")) | add | unique | sort) as $reported
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.1",
3
+ "version": "0.20.2",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",