@plainconceptsplatform/workflows 0.17.0 → 0.19.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/catalog-installation.js +13 -12
- package/dist/worker-env.js +10 -21
- package/loops/actions/identify-gate-subject/action.yml +134 -122
- package/loops/actions/require-open-issue/action.yml +59 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +322 -0
- package/loops/workflows/agent-apply-review.md +33 -3
- package/loops/workflows/agent-audit.md +36 -0
- package/loops/workflows/agent-implement.md +77 -5
- package/loops/workflows/agent-merge-gate.md +1 -0
- package/loops/workflows/agent-refine.md +33 -1
- package/loops/workflows/agent-release.md +4 -4
- package/loops/workflows/agent-triage.md +33 -1
- package/loops/workflows/work-router.yml +25 -11
- package/package.json +2 -2
|
@@ -306,18 +306,19 @@ async function preCommitHookUpdate(repositoryPath) {
|
|
|
306
306
|
}
|
|
307
307
|
const content = (await readFile(hookPath, "utf8"))
|
|
308
308
|
.replace("pnpm exec if git diff --cached --name-only -- .github | grep -q .; then", "if git diff --cached --name-only -- .github | grep -q .; then");
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
309
|
+
// Replace whatever managed block is there, never append beside it. The previous version
|
|
310
|
+
// recognised only two exact shapes -- the current block and the pre-`if` legacy lines -- and
|
|
311
|
+
// appended when it matched neither. A consumer holding an *older* wrapped block therefore
|
|
312
|
+
// gained a second one, so the compiler ran twice on every commit and one of the two staged a
|
|
313
|
+
// lock path that exists nowhere. All five consumers were carrying two blocks by the time this
|
|
314
|
+
// was noticed, and every future edit to `managedLines` would have added another. Nothing
|
|
315
|
+
// failed: running the compiler twice is only wasteful, so it never surfaced.
|
|
316
|
+
const managedBlock = /^[ \t]*if git diff --cached --name-only -- \.github \| grep -q \.; then\n(?:.*\n)*?[ \t]*fi\n?/gm;
|
|
317
|
+
const legacyLines = `${compileLine}\n${stageLine}\n${actionLockLine}\n`;
|
|
318
|
+
let stripped = content.replace(managedBlock, block => block.includes("compile-agent-workflows") ? "" : block);
|
|
319
|
+
stripped = stripped.split(legacyLines).join("");
|
|
320
|
+
const body = stripped.replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
|
|
321
|
+
return { target, content: body === "" ? managedLines : `${body}\n${managedLines}` };
|
|
321
322
|
}
|
|
322
323
|
export async function runCompileIfAvailable(repositoryPath) {
|
|
323
324
|
const script = join(repositoryPath, "scripts", "compile-agent-workflows.mjs");
|
package/dist/worker-env.js
CHANGED
|
@@ -120,26 +120,15 @@ export function mergeWorkerEnv(packageContent, consumerContent, baselineContent,
|
|
|
120
120
|
function engineEndpoint(content) {
|
|
121
121
|
return /^ OPENAI_BASE_URL: (.+)$/m.exec(content)?.[1];
|
|
122
122
|
}
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return [...found];
|
|
133
|
-
}
|
|
134
|
-
function preserveRunnerPool(packageContent, consumerContent) {
|
|
135
|
-
const mine = runnerPools(consumerContent);
|
|
136
|
-
const theirs = runnerPools(packageContent);
|
|
137
|
-
// Only an unambiguous swap. A consumer naming several pools has drifted rather than
|
|
138
|
-
// decided, and guessing which it meant is worse than leaving the package's.
|
|
139
|
-
if (mine.length !== 1 || theirs.length !== 1 || mine[0] === theirs[0])
|
|
140
|
-
return packageContent;
|
|
141
|
-
return packageContent.replace(/^(\s*runs-on(?:-slim)?: )(\S+)(\s*)$/gm, (line, prefix, pool, tail) => pool === "ubuntu-latest" ? line : `${prefix}${mine[0]}${tail}`);
|
|
142
|
-
}
|
|
123
|
+
// The runner pool used to be preserved from the consumer here: a repository naming a single
|
|
124
|
+
// non-ubuntu pool had every worker rewritten to it. That is how Pliny-Bot ended up running
|
|
125
|
+
// triage, refine, implement, apply-review and audit on RunnerLandingZone while its merge-gate
|
|
126
|
+
// and release stayed on agents-arc -- the pool was applied to the workers installed at the time
|
|
127
|
+
// and never to the ones added later, and nothing reported the split. The pool is package-owned
|
|
128
|
+
// now: the agent jobs belong on the Azure fleet (agents-arc, runner group `agentic`), and the
|
|
129
|
+
// router and release worker name RunnerLandingZone. The route matrix asserts both, so a repo
|
|
130
|
+
// cannot drift back without a red run. Per the standing rule for this package, anything a
|
|
131
|
+
// consumer really must vary belongs in `env:` at the top of the file, not in `runs-on`.
|
|
143
132
|
// GitHub evaluates no expression in a `workflow_run.workflows:` list or in a `cron:`, so the two
|
|
144
133
|
// router values that are also needed there cannot be read from `env:` at those two lines. The
|
|
145
134
|
// installer copies them in instead, which is what keeps the pair in step across an update; the
|
|
@@ -175,5 +164,5 @@ export function mergeWorker(packageContent, consumerContent, baselineContent) {
|
|
|
175
164
|
if (endpoint !== undefined) {
|
|
176
165
|
result = result.replace(/^ OPENAI_BASE_URL: .+$/m, ` OPENAI_BASE_URL: ${endpoint}`);
|
|
177
166
|
}
|
|
178
|
-
return { content:
|
|
167
|
+
return { content: result, report };
|
|
179
168
|
}
|
|
@@ -1,122 +1,134 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/identify-gate-subject/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Identify gate subject
|
|
3
|
-
description: Confirm the pull request is one we authored, resolve the issue it closes, and check that issue carries the required label.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token with issues and pull-requests read access.
|
|
7
|
-
required: true
|
|
8
|
-
pr-number:
|
|
9
|
-
description: Pull request number to gate.
|
|
10
|
-
required: true
|
|
11
|
-
ci-conclusion:
|
|
12
|
-
description: Conclusion reported by the CI run that triggered this gate. If empty, fetches from the PR's latest CI run.
|
|
13
|
-
required: false
|
|
14
|
-
default: ''
|
|
15
|
-
|
|
16
|
-
description:
|
|
17
|
-
required: false
|
|
18
|
-
default: ''
|
|
19
|
-
|
|
20
|
-
description:
|
|
21
|
-
required: false
|
|
22
|
-
default: ''
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/identify-gate-subject/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Identify gate subject
|
|
3
|
+
description: Confirm the pull request is one we authored, resolve the issue it closes, and check that issue carries the required label.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token with issues and pull-requests read access.
|
|
7
|
+
required: true
|
|
8
|
+
pr-number:
|
|
9
|
+
description: Pull request number to gate.
|
|
10
|
+
required: true
|
|
11
|
+
ci-conclusion:
|
|
12
|
+
description: Conclusion reported by the CI run that triggered this gate. If empty, fetches from the PR's latest CI run.
|
|
13
|
+
required: false
|
|
14
|
+
default: ''
|
|
15
|
+
ci-run-id:
|
|
16
|
+
description: CI run ID reported by the run that triggered this gate. If empty, resolved from the PR's latest CI run.
|
|
17
|
+
required: false
|
|
18
|
+
default: ''
|
|
19
|
+
linked-issue:
|
|
20
|
+
description: Issue the pull request closes, when the router already resolved it.
|
|
21
|
+
required: false
|
|
22
|
+
default: ''
|
|
23
|
+
require-label:
|
|
24
|
+
description: Label the closing issue must carry. Empty disables the check.
|
|
25
|
+
required: false
|
|
26
|
+
default: ''
|
|
27
|
+
outputs:
|
|
28
|
+
found:
|
|
29
|
+
description: Whether a valid gate subject was found.
|
|
30
|
+
value: ${{ steps.identify.outputs.found }}
|
|
31
|
+
pr:
|
|
32
|
+
description: Pull request number.
|
|
33
|
+
value: ${{ steps.identify.outputs.pr }}
|
|
34
|
+
issue:
|
|
35
|
+
description: Issue number the pull request closes.
|
|
36
|
+
value: ${{ steps.identify.outputs.issue }}
|
|
37
|
+
conclusion:
|
|
38
|
+
description: CI conclusion, passed through unchanged or fetched from latest CI run.
|
|
39
|
+
value: ${{ steps.identify.outputs.conclusion }}
|
|
40
|
+
run-id:
|
|
41
|
+
description: CI run ID when fetched.
|
|
42
|
+
value: ${{ steps.identify.outputs.run-id }}
|
|
43
|
+
runs:
|
|
44
|
+
using: composite
|
|
45
|
+
steps:
|
|
46
|
+
- name: Identify pull request and closing issue
|
|
47
|
+
id: identify
|
|
48
|
+
shell: bash
|
|
49
|
+
env:
|
|
50
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
51
|
+
REPO: ${{ github.repository }}
|
|
52
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
53
|
+
CONCLUSION: ${{ inputs.ci-conclusion }}
|
|
54
|
+
CI_RUN_ID: ${{ inputs.ci-run-id }}
|
|
55
|
+
LINKED_ISSUE: ${{ inputs.linked-issue }}
|
|
56
|
+
REQUIRE_LABEL: ${{ inputs.require-label }}
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
none() {
|
|
61
|
+
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
62
|
+
echo "$1"
|
|
63
|
+
exit 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pr="$(gh pr view "$PR_NUMBER" --repo "$REPO" \
|
|
67
|
+
--json state,author,closingIssuesReferences,body)"
|
|
68
|
+
|
|
69
|
+
[ "$(jq -r '.state' <<<"$pr")" = "OPEN" ] || none "PR #$PR_NUMBER is not open"
|
|
70
|
+
|
|
71
|
+
# Use GitHub's bot flag because App login formats differ across APIs.
|
|
72
|
+
if [ "$(jq -r '.author.is_bot' <<<"$pr")" != "true" ]; then
|
|
73
|
+
none "PR #$PR_NUMBER was authored by $(jq -r '.author.login' <<<"$pr"), a human."
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
issue="$LINKED_ISSUE"
|
|
77
|
+
|
|
78
|
+
if [ -z "$issue" ]; then
|
|
79
|
+
issue="$(jq -r '.closingIssuesReferences[0].number // empty' <<<"$pr")"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ -z "$issue" ]; then
|
|
83
|
+
issue="$(jq -r '.body // ""' <<<"$pr" |
|
|
84
|
+
grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?) +#[0-9]+' |
|
|
85
|
+
grep -oE '[0-9]+' | head -n 1 || true)"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
[ -n "$issue" ] || none "PR #$PR_NUMBER does not close a numbered issue"
|
|
89
|
+
|
|
90
|
+
if [ -n "$REQUIRE_LABEL" ]; then
|
|
91
|
+
labels="$(gh issue view "$issue" --repo "$REPO" --json labels \
|
|
92
|
+
--jq '[.labels[].name]')"
|
|
93
|
+
|
|
94
|
+
if ! jq -e --arg name "$REQUIRE_LABEL" 'index($name)' >/dev/null <<<"$labels"; then
|
|
95
|
+
none "issue #$issue does not carry $REQUIRE_LABEL (has: $(jq -r 'join(", ")' <<<"$labels"))"
|
|
96
|
+
fi
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# The run ID is what lets the gate read the failing job and its logs, so it has to be
|
|
100
|
+
# resolved on every path -- not only the label-triggered one. It used to be set inside
|
|
101
|
+
# `if [ -z "$ci_conclusion" ]`, which meant the caller that *knew* the ID got an empty
|
|
102
|
+
# one: the router passes ci-conclusion and ci-run-id together, so the branch was skipped
|
|
103
|
+
# and the value discarded. The gate then wrote `failed-jobs.json` as `[]`, and the agent,
|
|
104
|
+
# with no failing job name and no logs, escalated to `review`. Pliny-Bot #129 and #131
|
|
105
|
+
# both stalled on a human that way while CI was legitimately red and remediable.
|
|
106
|
+
ci_conclusion="$CONCLUSION"
|
|
107
|
+
ci_run_id="$CI_RUN_ID"
|
|
108
|
+
if [ -z "$ci_conclusion" ] || [ -z "$ci_run_id" ]; then
|
|
109
|
+
head_sha="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
|
|
110
|
+
# A run keeps the name it had when it ran, so a repository whose CI was renamed still
|
|
111
|
+
# carries runs under the old casing. Fold the name before comparing.
|
|
112
|
+
ci_run="$(gh api "repos/$REPO/actions/runs?head_sha=$head_sha&per_page=100" --jq '[.workflow_runs[] | select((.name | ascii_downcase) == "app: ci" or (.name | ascii_downcase) == "ci")] | sort_by(.created_at) | last')"
|
|
113
|
+
|
|
114
|
+
if [ -n "$ci_run" ] && [ "$ci_run" != "null" ]; then
|
|
115
|
+
[ -n "$ci_conclusion" ] || ci_conclusion="$(jq -r '.conclusion // "pending"' <<<"$ci_run")"
|
|
116
|
+
[ -n "$ci_run_id" ] || ci_run_id="$(jq -r '.id' <<<"$ci_run")"
|
|
117
|
+
echo "::notice::Resolved CI status: $ci_conclusion (run $ci_run_id)"
|
|
118
|
+
elif [ -z "$ci_conclusion" ]; then
|
|
119
|
+
ci_conclusion="unknown"
|
|
120
|
+
echo "::warning::No CI run found for PR #$PR_NUMBER"
|
|
121
|
+
else
|
|
122
|
+
echo "::warning::CI concluded $ci_conclusion but no run was found for PR #$PR_NUMBER; the gate has no failure evidence"
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
echo "found=true"
|
|
128
|
+
echo "pr=$PR_NUMBER"
|
|
129
|
+
echo "issue=$issue"
|
|
130
|
+
echo "conclusion=$ci_conclusion"
|
|
131
|
+
echo "run-id=$ci_run_id"
|
|
132
|
+
} >> "$GITHUB_OUTPUT"
|
|
133
|
+
|
|
134
|
+
echo "PR #$PR_NUMBER closes #$issue; CI concluded $ci_conclusion"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/require-open-issue/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Require an open issue
|
|
3
|
+
description: Report whether the issue is still open, read now rather than from the event that started the run.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token with issues read access.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue to check.
|
|
10
|
+
required: true
|
|
11
|
+
outputs:
|
|
12
|
+
open:
|
|
13
|
+
description: '"true" when the issue is still open, "false" when it is closed or unreadable.'
|
|
14
|
+
value: ${{ steps.check.outputs.open }}
|
|
15
|
+
runs:
|
|
16
|
+
using: composite
|
|
17
|
+
steps:
|
|
18
|
+
# The classifier already refuses a closed issue, but it can only read
|
|
19
|
+
# `github.event.issue.state`, which is the state at the moment the event fired and is absent
|
|
20
|
+
# altogether on a workflow_dispatch. This fleet queues for a runner for ten minutes and more,
|
|
21
|
+
# so an issue can be closed between the event and the pickup. Numa #659: a comment at
|
|
22
|
+
# 09:02:22 dispatched refine, the issue was closed a second later, the run reached this point
|
|
23
|
+
# at 09:15:53 and refined a closed issue for thirty-eight minutes, ending by labelling it
|
|
24
|
+
# `refined`, `implement` and `sp-5`. Every job reported success.
|
|
25
|
+
- name: Read the issue state now
|
|
26
|
+
id: check
|
|
27
|
+
shell: bash
|
|
28
|
+
env:
|
|
29
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
30
|
+
REPO: ${{ github.repository }}
|
|
31
|
+
ISSUE: ${{ inputs.issue-number }}
|
|
32
|
+
run: |
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
if [ -z "${ISSUE:-}" ]; then
|
|
35
|
+
echo "open=true" >> "$GITHUB_OUTPUT"
|
|
36
|
+
echo "No issue number given; nothing to check."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
# Three tries: a transient API failure must not park work a person then has to restart.
|
|
40
|
+
# Defaulting to open on an unreadable issue keeps the worker's existing behaviour, which
|
|
41
|
+
# is the safer side of this check -- it costs a wasted run, never a lost one.
|
|
42
|
+
state=""
|
|
43
|
+
for _ in 1 2 3; do
|
|
44
|
+
if state=$(gh issue view "$ISSUE" --repo "$REPO" --json state --jq '.state'); then break; fi
|
|
45
|
+
sleep 5
|
|
46
|
+
done
|
|
47
|
+
case "${state:-}" in
|
|
48
|
+
CLOSED|closed)
|
|
49
|
+
echo "open=false" >> "$GITHUB_OUTPUT"
|
|
50
|
+
echo "::notice::Issue #${ISSUE} was closed after this run was dispatched; stopping before any work is done."
|
|
51
|
+
;;
|
|
52
|
+
"")
|
|
53
|
+
echo "open=true" >> "$GITHUB_OUTPUT"
|
|
54
|
+
echo "::warning::Could not read the state of issue #${ISSUE} after three tries; continuing."
|
|
55
|
+
;;
|
|
56
|
+
*)
|
|
57
|
+
echo "open=true" >> "$GITHUB_OUTPUT"
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
@@ -814,6 +814,54 @@ if worker_installed merge-gate; then
|
|
|
814
814
|
fi
|
|
815
815
|
|
|
816
816
|
if worker_installed implement; then
|
|
817
|
+
# "No pull request" has two causes and they need different words. gh-aw pushes through the
|
|
818
|
+
# GraphQL signed-commits API, which rebases onto the current parent, so a `main` that moved
|
|
819
|
+
# under a long run conflicts; gh-aw then keeps the work by filing the patch as an issue rather
|
|
820
|
+
# than dropping it. Numa #657 hit this: a 50 KB patch that passed every validation gate, filed
|
|
821
|
+
# as issue #658, while the worker told the reader "nothing landed" and flagged a retry that
|
|
822
|
+
# would conflict the same way. The two paths must stay distinguishable, and both must be driven
|
|
823
|
+
# by a job output rather than by parsing prose.
|
|
824
|
+
#
|
|
825
|
+
# The discriminator is the item counter. `code_push_failure_count` looks like the right signal
|
|
826
|
+
# and is not: the deliberate reproduction on dogfood #10 filed the patch as #11 and still
|
|
827
|
+
# reported `Status: success`, `Successful: 1` and a resolved `GH_AW_CODE_PUSH_FAILURE_COUNT: 0`,
|
|
828
|
+
# so a worker gated on that count posts "nothing landed" over the top of a patch that exists.
|
|
829
|
+
# `create_pull_request` is the only safe output implement permits, so one succeeded item with
|
|
830
|
+
# no pull request number means the push fell back; nothing produced leaves the counter at 0.
|
|
831
|
+
PUSH_FALLBACK_OK=1
|
|
832
|
+
if ! grep -qF 'process_safe_outputs_items_succeeded' "$IMPLEMENT_WORKER_MD"; then
|
|
833
|
+
PUSH_FALLBACK_OK=0
|
|
834
|
+
echo "FAIL: implement does not read process_safe_outputs_items_succeeded, so a conflicted push reads as 'nothing landed'" >&2
|
|
835
|
+
fi
|
|
836
|
+
# Regating on the count that gh-aw leaves at 0 through a fallback is the specific regression.
|
|
837
|
+
# Matched on the `if:` line only: the comment above the branch names the count to explain why
|
|
838
|
+
# it is the wrong signal, and a bare symbol grep would fire on that prose instead of the guard.
|
|
839
|
+
if [ "$(count -cE '^ *if:.*code_push_failure_count' "$IMPLEMENT_WORKER_MD")" -ne 0 ]; then
|
|
840
|
+
PUSH_FALLBACK_OK=0
|
|
841
|
+
echo "FAIL: implement gates a no-pull-request path on code_push_failure_count, which is 0 when gh-aw files the patch as an issue" >&2
|
|
842
|
+
fi
|
|
843
|
+
for needed in 'PUSH_CONFLICT_COMMENT' 'NO_PULL_REQUEST_COMMENT'; do
|
|
844
|
+
grep -qF "env.${needed}" "$IMPLEMENT_WORKER_MD" || {
|
|
845
|
+
PUSH_FALLBACK_OK=0
|
|
846
|
+
echo "FAIL: implement no longer says env.${needed} on any path" >&2
|
|
847
|
+
}
|
|
848
|
+
done
|
|
849
|
+
# Collapsing them back into one message is the regression this guards: each is defined once in
|
|
850
|
+
# the env block and printed on exactly one path, so two usages of either means the conditions
|
|
851
|
+
# have been merged or duplicated.
|
|
852
|
+
if [ "$(count -cF 'env.PUSH_CONFLICT_COMMENT' "$IMPLEMENT_WORKER_MD")" -ne 1 ] ||
|
|
853
|
+
[ "$(count -cF 'env.NO_PULL_REQUEST_COMMENT' "$IMPLEMENT_WORKER_MD")" -ne 1 ]; then
|
|
854
|
+
PUSH_FALLBACK_OK=0
|
|
855
|
+
echo "FAIL: implement should print each no-pull-request message on exactly one path" >&2
|
|
856
|
+
fi
|
|
857
|
+
# And the two paths must be mutually exclusive, or a conflicted push gets both comments.
|
|
858
|
+
if [ "$(count -cF "process_safe_outputs_items_succeeded != '0'" "$IMPLEMENT_WORKER_MD")" -lt 2 ] ||
|
|
859
|
+
[ "$(count -cF "process_safe_outputs_items_succeeded == '0'" "$IMPLEMENT_WORKER_MD")" -lt 2 ]; then
|
|
860
|
+
PUSH_FALLBACK_OK=0
|
|
861
|
+
echo "FAIL: the conflicted-push and no-patch paths in implement are not mutually exclusive" >&2
|
|
862
|
+
fi
|
|
863
|
+
if [ "$PUSH_FALLBACK_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
864
|
+
|
|
817
865
|
# A provider outage kills a run in a couple of minutes with no answer, and the same issue used
|
|
818
866
|
# to be handed to a human for it. The implement worker retries those and only those: a run that
|
|
819
867
|
# worked for half an hour and then failed produced an answer that was wrong, and repeating it
|
|
@@ -1118,6 +1166,56 @@ if [ -f "$AUDIT_CLOSE_YML" ] && worker_installed audit; then
|
|
|
1118
1166
|
if [ "$AC_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1119
1167
|
fi
|
|
1120
1168
|
|
|
1169
|
+
echo "── Merge gate validator ──────────────────────────────────────────────────"
|
|
1170
|
+
|
|
1171
|
+
# The validator decides whether a gate run merges, remediates, parks, or is thrown away, and
|
|
1172
|
+
# until now nothing executed it -- this file only read it. Its `remediated` rule was changed on
|
|
1173
|
+
# reasoning alone, and the worker has not run in production since, so these fixtures are the only
|
|
1174
|
+
# evidence the change is right. Executing the real script is the same technique that finally
|
|
1175
|
+
# caught the belt's jq bug, which every reading assertion had walked past.
|
|
1176
|
+
GATE_VALIDATOR="${HERE}/../validate-merge-gate-output/validate-merge-gate-output.sh"
|
|
1177
|
+
if [ -f "$GATE_VALIDATOR" ] && worker_installed merge-gate; then
|
|
1178
|
+
VALIDATOR_OK=1
|
|
1179
|
+
gate_fixture="${TMPDIR:-/tmp}/route-matrix-gate-$$.json"
|
|
1180
|
+
|
|
1181
|
+
gate_case() {
|
|
1182
|
+
local name="$1" want="$2" json="$3" conclusion="$4"
|
|
1183
|
+
printf '%s' "$json" > "$gate_fixture"
|
|
1184
|
+
local got
|
|
1185
|
+
got=$(bash "$GATE_VALIDATOR" "$gate_fixture" 7 "$conclusion" 2>&1)
|
|
1186
|
+
if [ "$got" != "$want" ]; then
|
|
1187
|
+
VALIDATOR_OK=0
|
|
1188
|
+
echo "FAIL: the merge-gate validator called '${name}' ${got}, expected ${want}" >&2
|
|
1189
|
+
fi
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
gate_verdict='{"type":"add_comment","item_number":7,"body":"<!-- agent-merge-gate -->\n**Verdict:** VERB"}'
|
|
1193
|
+
gate_push='{"type":"push_to_pull_request_branch","pr_number":9}'
|
|
1194
|
+
gate_items() { printf '{"items":[%s]}' "$1"; }
|
|
1195
|
+
gate_comment() { printf '%s' "${gate_verdict/VERB/$1}"; }
|
|
1196
|
+
|
|
1197
|
+
gate_case "merge on green with no push" merge "$(gate_items "$(gate_comment merge)")" success
|
|
1198
|
+
gate_case "merge on a failed CI run" invalid "$(gate_items "$(gate_comment merge)")" failure
|
|
1199
|
+
gate_case "merge carrying a push" invalid "$(gate_items "$(gate_comment merge),${gate_push}")" success
|
|
1200
|
+
# The case the rule exists for. A conflicting pull request has no merge ref, so GitHub never
|
|
1201
|
+
# runs CI on that head and the belt falls back to the branch's last verdict, usually success.
|
|
1202
|
+
# Requiring conclusion == failure here discarded the resolved merge commit the agent had just
|
|
1203
|
+
# pushed, and the belt re-dispatched on the same verdict up to six times.
|
|
1204
|
+
gate_case "remediated with one push, CI green" remediated "$(gate_items "$(gate_comment remediated),${gate_push}")" success
|
|
1205
|
+
gate_case "remediated with one push, CI red" remediated "$(gate_items "$(gate_comment remediated),${gate_push}")" failure
|
|
1206
|
+
gate_case "remediated with no push" invalid "$(gate_items "$(gate_comment remediated)")" failure
|
|
1207
|
+
gate_case "remediated with two pushes" invalid "$(gate_items "$(gate_comment remediated),${gate_push},${gate_push}")" failure
|
|
1208
|
+
gate_case "review with no push" review "$(gate_items "$(gate_comment review)")" failure
|
|
1209
|
+
gate_case "review carrying a push" invalid "$(gate_items "$(gate_comment review),${gate_push}")" failure
|
|
1210
|
+
gate_case "a verdict aimed at another issue" invalid '{"items":[{"type":"add_comment","item_number":99,"body":"<!-- agent-merge-gate -->\n**Verdict:** merge"}]}' success
|
|
1211
|
+
gate_case "no verdict in the output" invalid '{"items":[{"type":"add_comment","item_number":7,"body":"just a note"}]}' success
|
|
1212
|
+
gate_case "an empty item list" invalid '{"items":[]}' success
|
|
1213
|
+
gate_case "output that is not an item list" invalid '{"nope":true}' success
|
|
1214
|
+
|
|
1215
|
+
rm -f "$gate_fixture"
|
|
1216
|
+
if [ "$VALIDATOR_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1217
|
+
fi
|
|
1218
|
+
|
|
1121
1219
|
echo "── Merge gate park ───────────────────────────────────────────────────────"
|
|
1122
1220
|
|
|
1123
1221
|
# A gate verdict parks the code it was given on. Both dispatch paths -- detect-pr-conflicts and
|
|
@@ -1392,6 +1490,230 @@ else
|
|
|
1392
1490
|
printf '%s\n' "$password_hits" >&2
|
|
1393
1491
|
fi
|
|
1394
1492
|
|
|
1493
|
+
echo "── Stale dispatch and empty runs ─────────────────────────────────────────"
|
|
1494
|
+
|
|
1495
|
+
# A route dispatched while an issue was open must not execute after it is closed. The classifier
|
|
1496
|
+
# refuses a closed issue, but it can only read `github.event.issue.state`: the state when the
|
|
1497
|
+
# event fired, and absent altogether on a workflow_dispatch. This fleet queues for a runner for
|
|
1498
|
+
# ten minutes and more. Numa #659 was closed one second after a comment dispatched refine; the run
|
|
1499
|
+
# reached `reserve` thirteen minutes later, took the reservation, and refined a closed issue for
|
|
1500
|
+
# thirty-eight minutes, ending by labelling it `refined`, `implement` and `sp-5`. Every job
|
|
1501
|
+
# reported success. Asserted per worker because the gate is three lines in each and none of them
|
|
1502
|
+
# failing produces a red run.
|
|
1503
|
+
STALE_DISPATCH_OK=1
|
|
1504
|
+
for route in refine implement triage apply-review; do
|
|
1505
|
+
worker_md="${WORKFLOWS_DIR}/agent-${route}.md"
|
|
1506
|
+
[ -f "$worker_md" ] || continue
|
|
1507
|
+
if ! grep -q '^ still_open:' "$worker_md"; then
|
|
1508
|
+
STALE_DISPATCH_OK=0
|
|
1509
|
+
echo "FAIL: agent-${route} has no still_open job; a route dispatched before the issue closed would run on it anyway" >&2
|
|
1510
|
+
continue
|
|
1511
|
+
fi
|
|
1512
|
+
# The reservation must be gated, or bot-working lands on a closed issue, and the agent must be
|
|
1513
|
+
# gated through the worker's own `if:`. Two distinct call sites.
|
|
1514
|
+
if [ "$(count -cF "needs.still_open.outputs.open == 'true'" "$worker_md")" -lt 2 ]; then
|
|
1515
|
+
STALE_DISPATCH_OK=0
|
|
1516
|
+
echo "FAIL: agent-${route} does not gate both the reservation and the agent on still_open" >&2
|
|
1517
|
+
fi
|
|
1518
|
+
# The gate job must carry no `needs:` of its own. gh-aw hoists exactly those custom jobs into
|
|
1519
|
+
# the activation job's dependencies, which is what lets the top-level `if:` read their outputs;
|
|
1520
|
+
# a gate job that gained a dependency would stop being hoisted and the clause would silently
|
|
1521
|
+
# evaluate to empty, which is always true. Asserted on the job block, not on the file, because
|
|
1522
|
+
# `needs: [still_open]` also appears on the reserve job and a file-wide search for it passed
|
|
1523
|
+
# for two workers that never declared anything.
|
|
1524
|
+
# activation must be given the dependency, or the top-level `if:` reads an empty value and the
|
|
1525
|
+
# clause is false: the agent never runs at all. gh-aw does not hoist this job, and the entry
|
|
1526
|
+
# lives at two spaces under `on:`, which is where gh-aw reads activation's dependency list.
|
|
1527
|
+
# Matched inside the `on:` block, because the same text appears on the reserve job and a
|
|
1528
|
+
# file-wide search for it passed for two workers that had declared nothing.
|
|
1529
|
+
on_block=$(awk '/^on:/{f=1; next} f && /^[a-z][a-z-]*:/{exit} f' "$worker_md")
|
|
1530
|
+
if ! printf '%s
|
|
1531
|
+
' "$on_block" | grep -qE '^ needs: \[.*still_open'; then
|
|
1532
|
+
STALE_DISPATCH_OK=0
|
|
1533
|
+
echo "FAIL: agent-${route} does not list still_open under on.needs, so activation reads an empty value and the agent never runs" >&2
|
|
1534
|
+
fi
|
|
1535
|
+
gate_block=$(awk '/^ still_open:/{f=1; next} f && /^ [a-z_]+:/{exit} f' "$worker_md")
|
|
1536
|
+
if printf '%s
|
|
1537
|
+
' "$gate_block" | grep -qE '^ needs:'; then
|
|
1538
|
+
STALE_DISPATCH_OK=0
|
|
1539
|
+
echo "FAIL: agent-${route}'s still_open job declares needs:, so gh-aw will not hoist it and the top-level if: reads an empty value" >&2
|
|
1540
|
+
fi
|
|
1541
|
+
done
|
|
1542
|
+
if [ "$STALE_DISPATCH_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1543
|
+
|
|
1544
|
+
# An audit that emitted neither a report nor a noop used to end green: `conclude` requires a
|
|
1545
|
+
# processed item, and a skipped job is not a failure. A whole agent run produced nothing and said
|
|
1546
|
+
# nothing. Both halves of the condition are asserted: the count alone would fire on a legitimate
|
|
1547
|
+
# `noop` if noop does not increment it, and the noop_message alone would miss the empty run.
|
|
1548
|
+
if worker_installed audit; then
|
|
1549
|
+
AUDIT_EMPTY_OK=1
|
|
1550
|
+
AUDIT_WORKER_MD="${WORKFLOWS_DIR}/agent-audit.md"
|
|
1551
|
+
grep -q '^ empty_run:' "$AUDIT_WORKER_MD" ||
|
|
1552
|
+
{ AUDIT_EMPTY_OK=0; echo "FAIL: the audit has no empty_run job; a run that files nothing reports success" >&2; }
|
|
1553
|
+
grep -qF "process_safe_outputs_processed_count == '0'" "$AUDIT_WORKER_MD" ||
|
|
1554
|
+
{ AUDIT_EMPTY_OK=0; echo "FAIL: the audit's empty_run does not test the processed count" >&2; }
|
|
1555
|
+
# empty_run must not depend on gh-aw's `conclusion` job. That job needs every custom job in the
|
|
1556
|
+
# worker, so naming it is a cycle and the whole workflow fails to compile -- which is how the
|
|
1557
|
+
# first attempt at this was caught. It also means `noop_message`, the one output that would say
|
|
1558
|
+
# a clean audit deliberately filed nothing, cannot be read from here.
|
|
1559
|
+
if grep -qE '^ needs: \[.*conclusion' "$AUDIT_WORKER_MD"; then
|
|
1560
|
+
AUDIT_EMPTY_OK=0
|
|
1561
|
+
echo "FAIL: the audit's empty_run depends on the conclusion job, which is a dependency cycle and will not compile" >&2
|
|
1562
|
+
fi
|
|
1563
|
+
if [ "$AUDIT_EMPTY_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1564
|
+
fi
|
|
1565
|
+
|
|
1566
|
+
echo "── Runner pools ──────────────────────────────────────────────────────────"
|
|
1567
|
+
|
|
1568
|
+
# Where every job runs, stated once and asserted, because GitHub gives a wrong pool no error: a
|
|
1569
|
+
# job addressed to a label no runner carries simply queues, and a job addressed to the wrong pool
|
|
1570
|
+
# runs in the wrong place. The pool was a preserved consumer value until 0.19.0, and that is how
|
|
1571
|
+
# one repository came to run triage, refine, implement, apply-review and audit on
|
|
1572
|
+
# RunnerLandingZone while merge-gate and release stayed on agents-arc: the override reached the
|
|
1573
|
+
# workers installed at the time and never the ones added later. Nothing reported the split.
|
|
1574
|
+
#
|
|
1575
|
+
# agent jobs of every worker except release -> agents-arc (the Azure fleet in
|
|
1576
|
+
# agentrunner-pro-rg-01,
|
|
1577
|
+
# runner group `agentic`)
|
|
1578
|
+
# agent-release.md and work-router.yml -> RunnerLandingZone
|
|
1579
|
+
# ubuntu-latest -> GitHub's own runner, chosen by nobody, left
|
|
1580
|
+
# wherever the package already has it
|
|
1581
|
+
AGENT_POOL="agents-arc"
|
|
1582
|
+
PLUMBING_POOL="RunnerLandingZone"
|
|
1583
|
+
POOL_OK=1
|
|
1584
|
+
|
|
1585
|
+
# `|| true` on both greps: a file naming no pool at all, or only ubuntu-latest, makes grep exit 1,
|
|
1586
|
+
# and under `set -o pipefail` that aborts the whole matrix instead of failing this one assertion.
|
|
1587
|
+
# A router mutated to ubuntu-latest everywhere did exactly that: the suite died without printing,
|
|
1588
|
+
# so the mutation looked caught when in fact nothing had been checked.
|
|
1589
|
+
pools_named() {
|
|
1590
|
+
{ grep -hoE '^[[:space:]]*runs-on(-slim)?: [^[:space:]]+' "$1" 2>/dev/null || true; } \
|
|
1591
|
+
| sed 's/.*: //' | { grep -v '^ubuntu-latest$' || true; } | sort -u
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
for worker_md in "${WORKFLOWS_DIR}"/agent-*.md; do
|
|
1595
|
+
[ -f "$worker_md" ] || continue
|
|
1596
|
+
worker_name="$(basename "$worker_md" .md)"
|
|
1597
|
+
want="$AGENT_POOL"
|
|
1598
|
+
[ "$worker_name" = "agent-release" ] && want="$PLUMBING_POOL"
|
|
1599
|
+
got="$(pools_named "$worker_md" | tr '\n' ' ' | sed 's/ $//')"
|
|
1600
|
+
if [ "$got" != "$want" ]; then
|
|
1601
|
+
POOL_OK=0
|
|
1602
|
+
echo "FAIL: ${worker_name} names runner pool(s) '${got}' but must name only '${want}'" >&2
|
|
1603
|
+
fi
|
|
1604
|
+
done
|
|
1605
|
+
|
|
1606
|
+
router_pools="$(pools_named "$ROUTER_YML" | tr '\n' ' ' | sed 's/ $//')"
|
|
1607
|
+
if [ "$router_pools" != "$PLUMBING_POOL" ]; then
|
|
1608
|
+
POOL_OK=0
|
|
1609
|
+
echo "FAIL: the router names runner pool(s) '${router_pools}' but must name only '${PLUMBING_POOL}'" >&2
|
|
1610
|
+
fi
|
|
1611
|
+
|
|
1612
|
+
# And the pool must not be reintroduced as a per-consumer value: that is the mechanism that let
|
|
1613
|
+
# the split happen, and it left no trace anywhere.
|
|
1614
|
+
if [ -d "${HERE}/../../../cli/src" ] && grep -rqE 'preserveRunnerPool|runnerPools' "${HERE}/../../../cli/src" 2>/dev/null; then
|
|
1615
|
+
POOL_OK=0
|
|
1616
|
+
echo "FAIL: the installer preserves a consumer's runner pool again; the pool is the package's to set" >&2
|
|
1617
|
+
fi
|
|
1618
|
+
|
|
1619
|
+
if [ "$POOL_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1620
|
+
|
|
1621
|
+
echo "── Worker input wiring ───────────────────────────────────────────────────"
|
|
1622
|
+
|
|
1623
|
+
# A worker that declares a workflow_call input and never reads it is the shape of the worst
|
|
1624
|
+
# outage this pipeline has had. The router resolved the CI run ID, passed it as `ci-run-id`, and
|
|
1625
|
+
# the merge gate declared the input and then called identify-gate-subject without it -- so
|
|
1626
|
+
# `RUN_ID` was always empty, the evidence step wrote `failed-jobs.json` as `[]`, and the agent,
|
|
1627
|
+
# holding no failing job name and no logs, chose `review` every time CI went red. Pliny-Bot #129,
|
|
1628
|
+
# #130 and #131 all ended up waiting on a human with CI legitimately red and remediable, and the
|
|
1629
|
+
# housekeeping digest reported it as work needing a person. Nothing went red: every job succeeded
|
|
1630
|
+
# at doing nothing. This asserts the class, because reading each worker by hand is how it was
|
|
1631
|
+
# missed for as long as it was.
|
|
1632
|
+
#
|
|
1633
|
+
# `agent-audit:trigger-kind` is exempt and stays listed rather than deleted: it is a
|
|
1634
|
+
# workflow_dispatch choice a person picks on the router, threaded through for symmetry, with no
|
|
1635
|
+
# behaviour attached at the far end. Removing it would take a dispatch option away from people,
|
|
1636
|
+
# so it is recorded as known-inert instead. Any *other* unread input fails.
|
|
1637
|
+
DEAD_INPUT_EXEMPT="agent-audit:trigger-kind"
|
|
1638
|
+
INPUT_WIRING_OK=1
|
|
1639
|
+
for worker_md in "${WORKFLOWS_DIR}"/agent-*.md; do
|
|
1640
|
+
[ -f "$worker_md" ] || continue
|
|
1641
|
+
worker_name="$(basename "$worker_md" .md)"
|
|
1642
|
+
# The declared inputs: the `inputs:` mapping under `on: workflow_call:`, whose keys sit at six
|
|
1643
|
+
# spaces. Stop at the first line indented less than that which is not blank.
|
|
1644
|
+
declared=$(awk '
|
|
1645
|
+
/^on:/ { in_on = 1; next }
|
|
1646
|
+
in_on && /^[a-z#]/ { exit }
|
|
1647
|
+
in_on && /^ workflow_call:/ { in_wc = 1; next }
|
|
1648
|
+
in_wc && /^ inputs:/ { in_inputs = 1; next }
|
|
1649
|
+
in_inputs && /^ [a-z]/ { in_inputs = 0 }
|
|
1650
|
+
in_inputs && /^ [a-z0-9_-]+:[[:space:]]*$/ {
|
|
1651
|
+
gsub(/[ :]/, "", $0); print $0
|
|
1652
|
+
}
|
|
1653
|
+
' "$worker_md")
|
|
1654
|
+
for input_name in $declared; do
|
|
1655
|
+
case "${worker_name}:${input_name}" in
|
|
1656
|
+
"$DEAD_INPUT_EXEMPT") continue ;;
|
|
1657
|
+
esac
|
|
1658
|
+
if [ "$(count -cE "inputs\.${input_name}([^a-zA-Z0-9_-]|\$)" "$worker_md")" -eq 0 ]; then
|
|
1659
|
+
INPUT_WIRING_OK=0
|
|
1660
|
+
echo "FAIL: ${worker_name} declares the input '${input_name}' and never reads it; the router's value is discarded and the job succeeds at doing nothing" >&2
|
|
1661
|
+
fi
|
|
1662
|
+
done
|
|
1663
|
+
done
|
|
1664
|
+
if [ "$INPUT_WIRING_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1665
|
+
|
|
1666
|
+
# The belt dispatches one gate per tick and stops. The gate's concurrency group holds a single
|
|
1667
|
+
# pending run, so GitHub cancels every earlier pending dispatch in it -- `cancel-in-progress:
|
|
1668
|
+
# false` only protects a run that has already started. A loop without this `break` dispatched one
|
|
1669
|
+
# gate per eligible pull request, ran the last, and discarded the rest with no comment, no
|
|
1670
|
+
# recorded attempt, and nothing on the pull request to show it had been skipped. Asserted because
|
|
1671
|
+
# removing the break produces no error anywhere: the extra dispatches all return 204.
|
|
1672
|
+
BELT_ONE_PER_TICK_OK=1
|
|
1673
|
+
belt_dispatch_block=$(awk '
|
|
1674
|
+
/Dispatching Merge Gate for PR/ { found = 1 }
|
|
1675
|
+
found { print }
|
|
1676
|
+
found && /^ *done$/ { exit }
|
|
1677
|
+
' "$ROUTER_YML")
|
|
1678
|
+
if [ -z "$belt_dispatch_block" ]; then
|
|
1679
|
+
BELT_ONE_PER_TICK_OK=0
|
|
1680
|
+
echo "FAIL: the merge belt's gate dispatch could not be located, so its one-per-tick guard cannot be checked" >&2
|
|
1681
|
+
elif ! printf '%s\n' "$belt_dispatch_block" | grep -qE '^ *break$'; then
|
|
1682
|
+
BELT_ONE_PER_TICK_OK=0
|
|
1683
|
+
echo "FAIL: the merge belt dispatches a gate per eligible pull request and never breaks; only the last stays pending and the rest are cancelled in silence" >&2
|
|
1684
|
+
fi
|
|
1685
|
+
# And the group it relies on must still be the serialising one.
|
|
1686
|
+
# Anchored to the whole line: a substring search matched `merge-belt-renamed` and stayed green
|
|
1687
|
+
# through a mutation that removed the serialisation the break depends on.
|
|
1688
|
+
grep -qE '^ *group: merge-belt *$' "$ROUTER_YML" ||
|
|
1689
|
+
{ BELT_ONE_PER_TICK_OK=0; echo "FAIL: the merge gate is no longer serialised on the merge-belt concurrency group" >&2; }
|
|
1690
|
+
if [ "$BELT_ONE_PER_TICK_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1691
|
+
|
|
1692
|
+
# The specific wiring that broke, asserted end to end: the router resolves the run ID, the gate
|
|
1693
|
+
# forwards it, and the action seeds from it rather than only from its own lookup.
|
|
1694
|
+
if worker_installed merge-gate; then
|
|
1695
|
+
GATE_RUN_ID_OK=1
|
|
1696
|
+
grep -Fq 'ci-run-id: ${{ needs.classify.outputs.ci-run-id }}' "$ROUTER_YML" ||
|
|
1697
|
+
{ GATE_RUN_ID_OK=0; echo "FAIL: the router no longer passes ci-run-id to the merge gate" >&2; }
|
|
1698
|
+
grep -Fq 'ci-run-id: ${{ inputs.ci-run-id }}' "$MERGE_GATE_WORKER_MD" ||
|
|
1699
|
+
{ GATE_RUN_ID_OK=0; echo "FAIL: the merge gate does not forward ci-run-id to identify-gate-subject, so it has no CI failure evidence" >&2; }
|
|
1700
|
+
GATE_SUBJECT_ACTION="${HERE}/../identify-gate-subject/action.yml"
|
|
1701
|
+
if [ -f "$GATE_SUBJECT_ACTION" ]; then
|
|
1702
|
+
grep -Fq 'ci_run_id="$CI_RUN_ID"' "$GATE_SUBJECT_ACTION" ||
|
|
1703
|
+
{ GATE_RUN_ID_OK=0; echo "FAIL: identify-gate-subject does not seed the run ID from its input, so a caller that knows it is ignored" >&2; }
|
|
1704
|
+
# The regression: resolving the ID only when the conclusion is missing.
|
|
1705
|
+
# Anchored to the start of the line: the repaired code keeps an `elif [ -z "$ci_conclusion" ]`
|
|
1706
|
+
# branch for the caller that genuinely has no verdict, and a fixed-string search matched that
|
|
1707
|
+
# `elif` as a substring -- the same way this file's own explanatory comments have tripped
|
|
1708
|
+
# three earlier assertions.
|
|
1709
|
+
if [ "$(count -cE '^ *if \[ -z "\$ci_conclusion" \]; then' "$GATE_SUBJECT_ACTION")" -ne 0 ]; then
|
|
1710
|
+
GATE_RUN_ID_OK=0
|
|
1711
|
+
echo "FAIL: identify-gate-subject resolves the CI run only when the conclusion is unknown; a router that passes both gets an empty run ID" >&2
|
|
1712
|
+
fi
|
|
1713
|
+
fi
|
|
1714
|
+
if [ "$GATE_RUN_ID_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1715
|
+
fi
|
|
1716
|
+
|
|
1395
1717
|
echo
|
|
1396
1718
|
if [ "$FAIL" -eq 0 ]; then
|
|
1397
1719
|
echo "Route matrix: ${PASS} passed"
|
|
@@ -49,7 +49,37 @@ on:
|
|
|
49
49
|
|
|
50
50
|
# Rung 4. Router has classified the event; this job validates PR ownership and checks for
|
|
51
51
|
# substantive feedback. A custom job, not `on.steps`, because the prompt needs these values.
|
|
52
|
+
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
53
|
+
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
54
|
+
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
55
|
+
# catches it after compilation; this is the line it asks for, the same one the merge gate
|
|
56
|
+
# uses for protected_changes.
|
|
57
|
+
needs: [still_open]
|
|
52
58
|
jobs:
|
|
59
|
+
# A route dispatched while the issue was open must not execute after it has been closed. The
|
|
60
|
+
# classifier can only see `github.event.issue.state`, which is the state when the event fired
|
|
61
|
+
# and is absent on a workflow_dispatch, and this fleet queues for a runner for ten minutes and
|
|
62
|
+
# more. Numa #659 was closed one second after a comment dispatched refine; the run reached
|
|
63
|
+
# `reserve` thirteen minutes later and refined a closed issue to completion. Read now, once,
|
|
64
|
+
# and gate both the reservation and the agent on it.
|
|
65
|
+
still_open:
|
|
66
|
+
runs-on: agents-arc
|
|
67
|
+
permissions:
|
|
68
|
+
contents: read
|
|
69
|
+
issues: read
|
|
70
|
+
outputs:
|
|
71
|
+
open: ${{ steps.state.outputs.open }}
|
|
72
|
+
steps:
|
|
73
|
+
- name: Checkout workflow actions
|
|
74
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
75
|
+
with:
|
|
76
|
+
persist-credentials: false
|
|
77
|
+
- name: Read the issue state
|
|
78
|
+
id: state
|
|
79
|
+
uses: ./.github/actions/require-open-issue
|
|
80
|
+
with:
|
|
81
|
+
token: ${{ github.token }}
|
|
82
|
+
issue-number: ${{ inputs.issue-number }}
|
|
53
83
|
subject:
|
|
54
84
|
runs-on: agents-arc
|
|
55
85
|
permissions:
|
|
@@ -118,8 +148,8 @@ jobs:
|
|
|
118
148
|
echo "PR #$PR has $unresolved unresolved thread(s)"
|
|
119
149
|
|
|
120
150
|
reserve:
|
|
121
|
-
needs: subject
|
|
122
|
-
if: needs.subject.outputs.found == 'true' && needs.subject.outputs.issue != ''
|
|
151
|
+
needs: [subject, still_open]
|
|
152
|
+
if: needs.subject.outputs.found == 'true' && needs.subject.outputs.issue != '' && needs.still_open.outputs.open == 'true'
|
|
123
153
|
runs-on: agents-arc
|
|
124
154
|
permissions:
|
|
125
155
|
contents: read
|
|
@@ -290,7 +320,7 @@ jobs:
|
|
|
290
320
|
${{ env.INCOMPLETE_COMMENT }}
|
|
291
321
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
292
322
|
|
|
293
|
-
if: needs.subject.outputs.found == 'true'
|
|
323
|
+
if: needs.subject.outputs.found == 'true' && needs.still_open.outputs.open == 'true'
|
|
294
324
|
|
|
295
325
|
runs-on: agents-arc
|
|
296
326
|
runs-on-slim: agents-arc
|
|
@@ -119,6 +119,42 @@ jobs:
|
|
|
119
119
|
bug
|
|
120
120
|
refine
|
|
121
121
|
|
|
122
|
+
# An audit that produced nothing reported success. The whole run -- a full agent, its tokens,
|
|
123
|
+
# its half hour -- ended with `conclude` skipped, because that job requires a processed item,
|
|
124
|
+
# and a skipped job leaves the run green. Numa's audit on 2026-09-10 did exactly that:
|
|
125
|
+
# `agent_output.json` 24 bytes, `safe-output-items.jsonl` empty, every job success, no report
|
|
126
|
+
# filed and nothing anywhere saying so. The next scheduled audit would have looked identical.
|
|
127
|
+
#
|
|
128
|
+
# This fires when the agent succeeded and the safe-outputs handler processed nothing at all.
|
|
129
|
+
#
|
|
130
|
+
# Step 6 of the prompt gives a clean codebase its own outcome -- call `noop` and stop -- and
|
|
131
|
+
# that outcome must not be reported as a failure. `noop` is a registered safe output here
|
|
132
|
+
# (`noop: max 1`), so the handler receives it as a message and the processed count is not zero,
|
|
133
|
+
# which keeps a deliberately empty audit out of this branch. That is read from the handler
|
|
134
|
+
# configuration rather than observed in a run: no audit in these repositories has yet emitted a
|
|
135
|
+
# noop. The direct signal, `noop_message`, belongs to gh-aw's `conclusion` job, and that job
|
|
136
|
+
# depends on every custom job here, so naming it is a dependency cycle and the worker will not
|
|
137
|
+
# compile -- the first version of this job was rejected for exactly that.
|
|
138
|
+
#
|
|
139
|
+
# If a genuinely clean audit ever fails here, that is the assumption breaking, and the fix is to
|
|
140
|
+
# carry the noop through a job this one can depend on rather than to widen the condition.
|
|
141
|
+
empty_run:
|
|
142
|
+
needs: [agent, safe_outputs]
|
|
143
|
+
if: >
|
|
144
|
+
needs.agent.result == 'success' &&
|
|
145
|
+
needs.safe_outputs.outputs.process_safe_outputs_processed_count == '0'
|
|
146
|
+
runs-on: agents-arc
|
|
147
|
+
permissions:
|
|
148
|
+
contents: read
|
|
149
|
+
steps:
|
|
150
|
+
- name: Report an audit that produced no outcome
|
|
151
|
+
env:
|
|
152
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
153
|
+
run: |
|
|
154
|
+
set -euo pipefail
|
|
155
|
+
echo "::error::The audit agent finished without emitting a report or a noop. The prompt requires one of the two: create_issue with the findings, or noop when the codebase is clean. Nothing was filed and no reason was given, so this run is a failure rather than a clean audit. The next scheduled audit will try again. ${RUN_URL}"
|
|
156
|
+
exit 1
|
|
157
|
+
|
|
122
158
|
safe-outputs:
|
|
123
159
|
# A failed run is already a red run. An issue per failure buries the real backlog
|
|
124
160
|
# under noise nobody closes.
|
|
@@ -17,6 +17,13 @@ env:
|
|
|
17
17
|
STALLED_LABEL: stalled
|
|
18
18
|
PR_PENDING_LABEL: pr-pending
|
|
19
19
|
NO_PULL_REQUEST_COMMENT: "The implementation run finished without producing a pull request. Nothing was lost, but nothing landed either: the issue keeps `implement` and is flagged for a retry."
|
|
20
|
+
# Said when the agent DID write the code and the push failed. gh-aw pushes through the GraphQL
|
|
21
|
+
# signed-commits API, which rebases onto the current parent, so a `main` that moved under a long
|
|
22
|
+
# run conflicts; gh-aw keeps the work by filing the patch as an issue rather than dropping it,
|
|
23
|
+
# and comments the link on this issue itself. Telling someone "nothing landed" over the top of
|
|
24
|
+
# that sends them to reimplement work that already exists. One line: the compiler flattens a
|
|
25
|
+
# multi-line env value.
|
|
26
|
+
PUSH_CONFLICT_COMMENT: "The implementation produced a patch, but pushing it failed: it no longer applies to `main`, which moved while this ran. gh-aw filed the patch as a separate issue rather than losing it, and linked it in its own comment above. The work is there and needs rebasing onto current `main`, not writing again."
|
|
20
27
|
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
21
28
|
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
22
29
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
@@ -70,7 +77,37 @@ on:
|
|
|
70
77
|
required: false
|
|
71
78
|
type: string
|
|
72
79
|
default: '0'
|
|
80
|
+
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
81
|
+
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
82
|
+
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
83
|
+
# catches it after compilation; this is the line it asks for, the same one the merge gate
|
|
84
|
+
# uses for protected_changes.
|
|
85
|
+
needs: [still_open]
|
|
73
86
|
jobs:
|
|
87
|
+
# A route dispatched while the issue was open must not execute after it has been closed. The
|
|
88
|
+
# classifier can only see `github.event.issue.state`, which is the state when the event fired
|
|
89
|
+
# and is absent on a workflow_dispatch, and this fleet queues for a runner for ten minutes and
|
|
90
|
+
# more. Numa #659 was closed one second after a comment dispatched refine; the run reached
|
|
91
|
+
# `reserve` thirteen minutes later and refined a closed issue to completion. Read now, once,
|
|
92
|
+
# and gate both the reservation and the agent on it.
|
|
93
|
+
still_open:
|
|
94
|
+
runs-on: agents-arc
|
|
95
|
+
permissions:
|
|
96
|
+
contents: read
|
|
97
|
+
issues: read
|
|
98
|
+
outputs:
|
|
99
|
+
open: ${{ steps.state.outputs.open }}
|
|
100
|
+
steps:
|
|
101
|
+
- name: Checkout workflow actions
|
|
102
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
103
|
+
with:
|
|
104
|
+
persist-credentials: false
|
|
105
|
+
- name: Read the issue state
|
|
106
|
+
id: state
|
|
107
|
+
uses: ./.github/actions/require-open-issue
|
|
108
|
+
with:
|
|
109
|
+
token: ${{ github.token }}
|
|
110
|
+
issue-number: ${{ inputs.issue-number }}
|
|
74
111
|
eligibility:
|
|
75
112
|
runs-on: agents-arc
|
|
76
113
|
permissions:
|
|
@@ -107,8 +144,8 @@ jobs:
|
|
|
107
144
|
echo "eligible=true" >> "$GITHUB_OUTPUT"
|
|
108
145
|
|
|
109
146
|
reserve:
|
|
110
|
-
needs: [eligibility]
|
|
111
|
-
if: needs.eligibility.outputs.eligible == 'true'
|
|
147
|
+
needs: [eligibility, still_open]
|
|
148
|
+
if: needs.eligibility.outputs.eligible == 'true' && needs.still_open.outputs.open == 'true'
|
|
112
149
|
runs-on: agents-arc
|
|
113
150
|
permissions:
|
|
114
151
|
contents: read
|
|
@@ -227,8 +264,43 @@ jobs:
|
|
|
227
264
|
# comment, and no bot-working — which also hid it from the hourly stale-reservation sweep.
|
|
228
265
|
# Comments do not re-trigger implement, so nothing on any path would ever look at it again.
|
|
229
266
|
# It was the only failure in the fleet that signalled nobody at all.
|
|
267
|
+
#
|
|
268
|
+
# There are two ways to reach "no pull request", and telling a person they are the same
|
|
269
|
+
# thing wastes their time. gh-aw pushes through the GraphQL signed-commits API, which
|
|
270
|
+
# rebases the commit range onto the current parent; when `main` has moved under a long run
|
|
271
|
+
# the rebase conflicts, and gh-aw keeps the work by filing the patch as an issue instead of
|
|
272
|
+
# dropping it, commenting the link on this issue itself. Saying "nothing landed" over the
|
|
273
|
+
# top of that is false: the patch exists and needs rebasing, not reimplementing. Seen on
|
|
274
|
+
# Numa #657, where the same change had landed on main by hand while the agent was writing
|
|
275
|
+
# it, and reproduced deliberately on dogfood #10 -> #11.
|
|
276
|
+
#
|
|
277
|
+
# The signal is the item counter, not `code_push_failure_count`. gh-aw treats the fallback
|
|
278
|
+
# as a *successful* outcome for the item -- the dogfood run logged `Status: success`,
|
|
279
|
+
# `Successful: 1` and a resolved `GH_AW_CODE_PUSH_FAILURE_COUNT: 0` while filing #11 -- so
|
|
280
|
+
# gating on that count posted the wrong message. `create_pull_request` is the only safe
|
|
281
|
+
# output this worker permits, so one succeeded item with no pull request number can only
|
|
282
|
+
# mean the push fell back to an issue. Nothing produced at all leaves the counter at 0.
|
|
283
|
+
- name: Flag a patch that could not be pushed
|
|
284
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '0' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != ''
|
|
285
|
+
uses: ./.github/actions/add-issue-labels
|
|
286
|
+
with:
|
|
287
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
288
|
+
issue-number: ${{ inputs.issue-number }}
|
|
289
|
+
labels: |-
|
|
290
|
+
${{ env.REVIEW_LABEL }}
|
|
291
|
+
${{ env.STALLED_LABEL }}
|
|
292
|
+
- name: Say where the patch went
|
|
293
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != '0' && needs.safe_outputs.outputs.process_safe_outputs_items_succeeded != ''
|
|
294
|
+
uses: ./.github/actions/create-issue-comment
|
|
295
|
+
with:
|
|
296
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
297
|
+
issue-number: ${{ inputs.issue-number }}
|
|
298
|
+
body: |
|
|
299
|
+
${{ env.IMPLEMENT_MARKER }}
|
|
300
|
+
${{ env.PUSH_CONFLICT_COMMENT }}
|
|
301
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
230
302
|
- name: Flag a run that produced no pull request
|
|
231
|
-
if: needs.safe_outputs.outputs.created_pr_number == ''
|
|
303
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '')
|
|
232
304
|
uses: ./.github/actions/add-issue-labels
|
|
233
305
|
with:
|
|
234
306
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -237,7 +309,7 @@ jobs:
|
|
|
237
309
|
${{ env.REVIEW_LABEL }}
|
|
238
310
|
${{ env.STALLED_LABEL }}
|
|
239
311
|
- name: Say so on the issue
|
|
240
|
-
if: needs.safe_outputs.outputs.created_pr_number == ''
|
|
312
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '')
|
|
241
313
|
uses: ./.github/actions/create-issue-comment
|
|
242
314
|
with:
|
|
243
315
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -436,7 +508,7 @@ jobs:
|
|
|
436
508
|
needs: [eligibility]
|
|
437
509
|
if: needs.eligibility.outputs.eligible == 'true'
|
|
438
510
|
|
|
439
|
-
if: inputs.issue-number != ''
|
|
511
|
+
if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true'
|
|
440
512
|
|
|
441
513
|
runs-on: agents-arc
|
|
442
514
|
runs-on-slim: agents-arc
|
|
@@ -114,6 +114,7 @@ jobs:
|
|
|
114
114
|
token: ${{ github.token }}
|
|
115
115
|
pr-number: ${{ inputs.pr-number }}
|
|
116
116
|
ci-conclusion: ${{ inputs.ci-conclusion }}
|
|
117
|
+
ci-run-id: ${{ inputs.ci-run-id }}
|
|
117
118
|
linked-issue: ${{ inputs.linked-issue }}
|
|
118
119
|
require-label: ${{ env.IMPLEMENT_LABEL }}
|
|
119
120
|
- name: Block a pull request with requested changes
|
|
@@ -76,9 +76,41 @@ on:
|
|
|
76
76
|
required: false
|
|
77
77
|
type: string
|
|
78
78
|
default: first
|
|
79
|
+
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
80
|
+
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
81
|
+
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
82
|
+
# catches it after compilation; this is the line it asks for, the same one the merge gate
|
|
83
|
+
# uses for protected_changes.
|
|
84
|
+
needs: [still_open]
|
|
79
85
|
|
|
80
86
|
jobs:
|
|
87
|
+
# A route dispatched while the issue was open must not execute after it has been closed. The
|
|
88
|
+
# classifier can only see `github.event.issue.state`, which is the state when the event fired
|
|
89
|
+
# and is absent on a workflow_dispatch, and this fleet queues for a runner for ten minutes and
|
|
90
|
+
# more. Numa #659 was closed one second after a comment dispatched refine; the run reached
|
|
91
|
+
# `reserve` thirteen minutes later and refined a closed issue to completion. Read now, once,
|
|
92
|
+
# and gate both the reservation and the agent on it.
|
|
93
|
+
still_open:
|
|
94
|
+
runs-on: agents-arc
|
|
95
|
+
permissions:
|
|
96
|
+
contents: read
|
|
97
|
+
issues: read
|
|
98
|
+
outputs:
|
|
99
|
+
open: ${{ steps.state.outputs.open }}
|
|
100
|
+
steps:
|
|
101
|
+
- name: Checkout workflow actions
|
|
102
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
103
|
+
with:
|
|
104
|
+
persist-credentials: false
|
|
105
|
+
- name: Read the issue state
|
|
106
|
+
id: state
|
|
107
|
+
uses: ./.github/actions/require-open-issue
|
|
108
|
+
with:
|
|
109
|
+
token: ${{ github.token }}
|
|
110
|
+
issue-number: ${{ inputs.issue-number }}
|
|
81
111
|
reserve:
|
|
112
|
+
needs: [still_open]
|
|
113
|
+
if: needs.still_open.outputs.open == 'true'
|
|
82
114
|
runs-on: agents-arc
|
|
83
115
|
permissions:
|
|
84
116
|
contents: read
|
|
@@ -364,7 +396,7 @@ jobs:
|
|
|
364
396
|
${{ env.INCOMPLETE_COMMENT }}
|
|
365
397
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
366
398
|
|
|
367
|
-
if: inputs.issue-number != ''
|
|
399
|
+
if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true'
|
|
368
400
|
|
|
369
401
|
runs-on: agents-arc
|
|
370
402
|
runs-on-slim: agents-arc
|
|
@@ -29,8 +29,8 @@ on:
|
|
|
29
29
|
type: string
|
|
30
30
|
default: "auto"
|
|
31
31
|
|
|
32
|
-
runs-on:
|
|
33
|
-
runs-on-slim:
|
|
32
|
+
runs-on: RunnerLandingZone
|
|
33
|
+
runs-on-slim: RunnerLandingZone
|
|
34
34
|
|
|
35
35
|
secrets:
|
|
36
36
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
@@ -98,7 +98,7 @@ jobs:
|
|
|
98
98
|
always() &&
|
|
99
99
|
needs.agent.result == 'success' &&
|
|
100
100
|
needs.safe_outputs.result == 'success'
|
|
101
|
-
runs-on:
|
|
101
|
+
runs-on: RunnerLandingZone
|
|
102
102
|
permissions:
|
|
103
103
|
contents: write
|
|
104
104
|
steps:
|
|
@@ -205,7 +205,7 @@ jobs:
|
|
|
205
205
|
if: >
|
|
206
206
|
always() &&
|
|
207
207
|
(needs.agent.result != 'success' || needs.safe_outputs.result != 'success')
|
|
208
|
-
runs-on:
|
|
208
|
+
runs-on: RunnerLandingZone
|
|
209
209
|
permissions:
|
|
210
210
|
contents: read
|
|
211
211
|
steps:
|
|
@@ -70,9 +70,41 @@ on:
|
|
|
70
70
|
required: false
|
|
71
71
|
type: string
|
|
72
72
|
default: first
|
|
73
|
+
# The gate job that the top-level `if:` reads. gh-aw folds that `if:` into the generated
|
|
74
|
+
# activation job but gives activation no dependency on the job, so the reference resolves
|
|
75
|
+
# to '' and the clause is false -- the agent would never run. The package's own validator
|
|
76
|
+
# catches it after compilation; this is the line it asks for, the same one the merge gate
|
|
77
|
+
# uses for protected_changes.
|
|
78
|
+
needs: [still_open]
|
|
73
79
|
|
|
74
80
|
jobs:
|
|
81
|
+
# A route dispatched while the issue was open must not execute after it has been closed. The
|
|
82
|
+
# classifier can only see `github.event.issue.state`, which is the state when the event fired
|
|
83
|
+
# and is absent on a workflow_dispatch, and this fleet queues for a runner for ten minutes and
|
|
84
|
+
# more. Numa #659 was closed one second after a comment dispatched refine; the run reached
|
|
85
|
+
# `reserve` thirteen minutes later and refined a closed issue to completion. Read now, once,
|
|
86
|
+
# and gate both the reservation and the agent on it.
|
|
87
|
+
still_open:
|
|
88
|
+
runs-on: agents-arc
|
|
89
|
+
permissions:
|
|
90
|
+
contents: read
|
|
91
|
+
issues: read
|
|
92
|
+
outputs:
|
|
93
|
+
open: ${{ steps.state.outputs.open }}
|
|
94
|
+
steps:
|
|
95
|
+
- name: Checkout workflow actions
|
|
96
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
97
|
+
with:
|
|
98
|
+
persist-credentials: false
|
|
99
|
+
- name: Read the issue state
|
|
100
|
+
id: state
|
|
101
|
+
uses: ./.github/actions/require-open-issue
|
|
102
|
+
with:
|
|
103
|
+
token: ${{ github.token }}
|
|
104
|
+
issue-number: ${{ inputs.issue-number }}
|
|
75
105
|
reserve:
|
|
106
|
+
needs: [still_open]
|
|
107
|
+
if: needs.still_open.outputs.open == 'true'
|
|
76
108
|
runs-on: agents-arc
|
|
77
109
|
permissions:
|
|
78
110
|
contents: read
|
|
@@ -296,7 +328,7 @@ jobs:
|
|
|
296
328
|
${{ env.INCOMPLETE_COMMENT }}
|
|
297
329
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
298
330
|
|
|
299
|
-
if: inputs.issue-number != ''
|
|
331
|
+
if: inputs.issue-number != '' && needs.still_open.outputs.open == 'true'
|
|
300
332
|
|
|
301
333
|
runs-on: agents-arc
|
|
302
334
|
runs-on-slim: agents-arc
|
|
@@ -242,7 +242,7 @@ permissions:
|
|
|
242
242
|
jobs:
|
|
243
243
|
# Public repositories must check repository permission before an event can start a model.
|
|
244
244
|
authorize:
|
|
245
|
-
runs-on:
|
|
245
|
+
runs-on: RunnerLandingZone
|
|
246
246
|
timeout-minutes: 5
|
|
247
247
|
permissions:
|
|
248
248
|
contents: read
|
|
@@ -345,7 +345,7 @@ jobs:
|
|
|
345
345
|
esac
|
|
346
346
|
|
|
347
347
|
classify:
|
|
348
|
-
runs-on:
|
|
348
|
+
runs-on: RunnerLandingZone
|
|
349
349
|
timeout-minutes: 5
|
|
350
350
|
permissions:
|
|
351
351
|
contents: read
|
|
@@ -402,7 +402,7 @@ jobs:
|
|
|
402
402
|
check-implement-pr:
|
|
403
403
|
needs: [classify, authorize]
|
|
404
404
|
if: needs.classify.outputs.route == 'implement' && needs.authorize.outputs.trusted == 'true'
|
|
405
|
-
runs-on:
|
|
405
|
+
runs-on: RunnerLandingZone
|
|
406
406
|
timeout-minutes: 3
|
|
407
407
|
permissions:
|
|
408
408
|
contents: read
|
|
@@ -493,7 +493,7 @@ jobs:
|
|
|
493
493
|
dispatch-triage:
|
|
494
494
|
needs: [classify, authorize]
|
|
495
495
|
if: needs.classify.outputs.route == 'triage' && needs.authorize.outputs.is_outside_collaborator == 'true'
|
|
496
|
-
runs-on:
|
|
496
|
+
runs-on: RunnerLandingZone
|
|
497
497
|
timeout-minutes: 5
|
|
498
498
|
permissions:
|
|
499
499
|
actions: write
|
|
@@ -621,7 +621,7 @@ jobs:
|
|
|
621
621
|
if: >
|
|
622
622
|
needs.classify.outputs.route == 'bot-approve' &&
|
|
623
623
|
(github.actor == 'app/github-actions' || github.actor == 'platform-devbox[bot]')
|
|
624
|
-
runs-on:
|
|
624
|
+
runs-on: RunnerLandingZone
|
|
625
625
|
timeout-minutes: 5
|
|
626
626
|
permissions:
|
|
627
627
|
actions: write
|
|
@@ -678,7 +678,7 @@ jobs:
|
|
|
678
678
|
github.event.pull_request.head.repo.full_name == github.repository
|
|
679
679
|
)
|
|
680
680
|
)
|
|
681
|
-
runs-on:
|
|
681
|
+
runs-on: RunnerLandingZone
|
|
682
682
|
timeout-minutes: 10
|
|
683
683
|
permissions:
|
|
684
684
|
contents: read
|
|
@@ -871,7 +871,7 @@ jobs:
|
|
|
871
871
|
reconcile-bot-pr-runs:
|
|
872
872
|
needs: classify
|
|
873
873
|
if: needs.classify.outputs.route == 'reconcile-bot-pr-runs'
|
|
874
|
-
runs-on:
|
|
874
|
+
runs-on: RunnerLandingZone
|
|
875
875
|
timeout-minutes: 5
|
|
876
876
|
permissions:
|
|
877
877
|
contents: read
|
|
@@ -1079,12 +1079,26 @@ jobs:
|
|
|
1079
1079
|
-f "inputs[ci-conclusion]=$ci_conclusion" \
|
|
1080
1080
|
-f "inputs[ci-run-id]=$ci_run_id" \
|
|
1081
1081
|
-f "inputs[attempts_so_far]=$attempts"
|
|
1082
|
+
|
|
1083
|
+
# One gate per tick, because that is all the concurrency group can hold. The gate
|
|
1084
|
+
# runs in `merge-belt`, and GitHub keeps only the most recent *pending* run in a
|
|
1085
|
+
# group -- `cancel-in-progress: false` does not change that, it only protects a run
|
|
1086
|
+
# that has already started. A tick that dispatched three gates therefore ran the
|
|
1087
|
+
# last and silently cancelled the other two: no comment, no attempt recorded, and
|
|
1088
|
+
# nothing on the pull request to say it had been passed over. Seen on Pliny-Bot,
|
|
1089
|
+
# where dispatching a gate for PR #135 cancelled the one queued for PR #136.
|
|
1090
|
+
#
|
|
1091
|
+
# Stopping here loses nothing: every pull request eligible this tick is still
|
|
1092
|
+
# eligible next tick, and a run that never started consumes no attempt. The belt
|
|
1093
|
+
# now does one thing and says so, instead of appearing to start work it discarded.
|
|
1094
|
+
echo "One gate dispatched this tick; any other eligible pull request waits for the next one."
|
|
1095
|
+
break
|
|
1082
1096
|
done
|
|
1083
1097
|
|
|
1084
1098
|
audit-close:
|
|
1085
1099
|
needs: classify
|
|
1086
1100
|
if: needs.classify.outputs.route == 'audit-close'
|
|
1087
|
-
runs-on:
|
|
1101
|
+
runs-on: RunnerLandingZone
|
|
1088
1102
|
timeout-minutes: 15
|
|
1089
1103
|
concurrency:
|
|
1090
1104
|
group: audit-close
|
|
@@ -1109,7 +1123,7 @@ jobs:
|
|
|
1109
1123
|
cleanup-artifacts:
|
|
1110
1124
|
needs: classify
|
|
1111
1125
|
if: needs.classify.outputs.route == 'cleanup-artifacts' && !github.event.repository.fork
|
|
1112
|
-
runs-on:
|
|
1126
|
+
runs-on: RunnerLandingZone
|
|
1113
1127
|
timeout-minutes: 15
|
|
1114
1128
|
concurrency:
|
|
1115
1129
|
group: cleanup-artifacts
|
|
@@ -1133,7 +1147,7 @@ jobs:
|
|
|
1133
1147
|
housekeeping:
|
|
1134
1148
|
needs: classify
|
|
1135
1149
|
if: needs.classify.outputs.route == 'housekeeping' && !github.event.repository.fork
|
|
1136
|
-
runs-on:
|
|
1150
|
+
runs-on: RunnerLandingZone
|
|
1137
1151
|
timeout-minutes: 15
|
|
1138
1152
|
concurrency:
|
|
1139
1153
|
group: housekeeping
|
|
@@ -1172,7 +1186,7 @@ jobs:
|
|
|
1172
1186
|
validate:
|
|
1173
1187
|
needs: classify
|
|
1174
1188
|
if: needs.classify.outputs.route == 'validate'
|
|
1175
|
-
runs-on:
|
|
1189
|
+
runs-on: RunnerLandingZone
|
|
1176
1190
|
timeout-minutes: 10
|
|
1177
1191
|
permissions:
|
|
1178
1192
|
contents: read
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plainconceptsplatform/workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Install and update Platform GitHub agentic workflows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-actions",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc --project tsconfig.build.json",
|
|
29
|
+
"build": "tsc --project tsconfig.build.json && node ./scripts/copy-loops.mjs",
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
32
32
|
"release": "pnpm build && pnpm test && npm publish --access public",
|