@plainconceptsplatform/workflows 0.4.8 → 0.4.10
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 +6 -3
- package/dist/catalog-installation.test.js +3 -3
- package/dist/route-processing.test.js +6 -2
- package/loops/actions/classify-route/classify-route.sh +3 -1
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +3 -1
- package/loops/workflows/agent-apply-review.md +1 -0
- package/loops/workflows/agent-audit.md +1 -0
- package/loops/workflows/agent-direct.md +1 -0
- package/loops/workflows/agent-implement.md +1 -0
- package/loops/workflows/agent-merge-gate.md +1 -0
- package/loops/workflows/agent-propose.md +1 -0
- package/loops/workflows/agent-refine.md +1 -0
- package/loops/workflows/work-router.yml +80 -0
- package/package.json +1 -1
|
@@ -162,16 +162,19 @@ async function preCommitHookUpdate(repositoryPath) {
|
|
|
162
162
|
const compileLine = "node scripts/compile-agent-workflows.mjs";
|
|
163
163
|
const stageLine = "git add -- .github/workflows/*.lock.yml";
|
|
164
164
|
const actionLockLine = "[ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json";
|
|
165
|
-
const managedLines =
|
|
165
|
+
const managedLines = `if git diff --cached --name-only -- .github | grep -q .; then\n ${compileLine}\n ${stageLine}\n ${actionLockLine}\nfi\n`;
|
|
166
166
|
if (!await exists(hookPath)) {
|
|
167
167
|
return { target, content: managedLines };
|
|
168
168
|
}
|
|
169
169
|
const content = await readFile(hookPath, "utf8");
|
|
170
170
|
if (content.includes("compile-agent-workflows")) {
|
|
171
|
-
|
|
171
|
+
const legacyLines = `${compileLine}\n${stageLine}\n${actionLockLine}\n`;
|
|
172
|
+
if (content.includes(managedLines))
|
|
172
173
|
return { target, content };
|
|
174
|
+
if (content.includes(legacyLines))
|
|
175
|
+
return { target, content: content.replace(legacyLines, managedLines) };
|
|
173
176
|
const suffix = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
174
|
-
return { target, content: `${content}${suffix}${
|
|
177
|
+
return { target, content: `${content}${suffix}${managedLines}` };
|
|
175
178
|
}
|
|
176
179
|
return { target, content: content.endsWith("\n") || content === ""
|
|
177
180
|
? `${content}${managedLines}`
|
|
@@ -253,20 +253,20 @@ describe("catalog installation", () => {
|
|
|
253
253
|
const repositoryPath = await createDirectory({});
|
|
254
254
|
await ensurePreCommitHook(repositoryPath);
|
|
255
255
|
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
256
|
-
.resolves.toBe("node scripts/compile-agent-workflows.mjs\
|
|
256
|
+
.resolves.toBe("if git diff --cached --name-only -- .github | grep -q .; then\n node scripts/compile-agent-workflows.mjs\n git add -- .github/workflows/*.lock.yml\n [ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\nfi\n");
|
|
257
257
|
});
|
|
258
258
|
it("keeps existing pre-commit commands and appends the compiler once", async () => {
|
|
259
259
|
const repositoryPath = await createDirectory({ ".husky/pre-commit": "pnpm lint\n" });
|
|
260
260
|
await ensurePreCommitHook(repositoryPath);
|
|
261
261
|
await ensurePreCommitHook(repositoryPath);
|
|
262
262
|
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
263
|
-
.resolves.toBe("pnpm lint\
|
|
263
|
+
.resolves.toBe("pnpm lint\nif git diff --cached --name-only -- .github | grep -q .; then\n node scripts/compile-agent-workflows.mjs\n git add -- .github/workflows/*.lock.yml\n [ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\nfi\n");
|
|
264
264
|
});
|
|
265
265
|
it("upgrades an existing compiler hook to stage generated locks", async () => {
|
|
266
266
|
const repositoryPath = await createDirectory({ ".husky/pre-commit": "node scripts/compile-agent-workflows.mjs\n" });
|
|
267
267
|
await ensurePreCommitHook(repositoryPath);
|
|
268
268
|
await expect(readFile(join(repositoryPath, ".husky", "pre-commit"), "utf8"))
|
|
269
|
-
.resolves.toContain("git
|
|
269
|
+
.resolves.toContain("if git diff --cached --name-only -- .github | grep -q .; then");
|
|
270
270
|
});
|
|
271
271
|
it("leaves consumer files untouched when staged workflow compilation fails", async () => {
|
|
272
272
|
const sourcePath = await createDirectory({
|
|
@@ -9,6 +9,7 @@ on:
|
|
|
9
9
|
- cron: "17 1 * * 1"
|
|
10
10
|
- cron: "43 3 * * *"
|
|
11
11
|
- cron: "0 6 * * *"
|
|
12
|
+
- cron: "*/5 * * * *"
|
|
12
13
|
- cron: "0 */2 * * *"
|
|
13
14
|
- cron: "29 7 * * *"
|
|
14
15
|
|
|
@@ -28,6 +29,7 @@ on:
|
|
|
28
29
|
- propose
|
|
29
30
|
- audit-close
|
|
30
31
|
- cleanup-artifacts
|
|
32
|
+
- reconcile-bot-pr-runs
|
|
31
33
|
- stale-recovery
|
|
32
34
|
- validate
|
|
33
35
|
|
|
@@ -71,6 +73,7 @@ set -euo pipefail
|
|
|
71
73
|
readonly AUDIT_CRON="17 1 * * 1"
|
|
72
74
|
readonly AUDIT_CLOSE_CRON="43 3 * * *"
|
|
73
75
|
readonly CLEANUP_ARTIFACTS_CRON="0 6 * * *"
|
|
76
|
+
readonly RECONCILE_BOT_PR_RUNS_CRON="*/5 * * * *"
|
|
74
77
|
readonly STALE_RECOVERY_CRON="0 */2 * * *"
|
|
75
78
|
readonly PROPOSE_CRON="29 7 * * *"
|
|
76
79
|
|
|
@@ -84,6 +87,7 @@ classify_route() {
|
|
|
84
87
|
"\$AUDIT_CRON") route="audit" ;;
|
|
85
88
|
"\$AUDIT_CLOSE_CRON") route="audit-close" ;;
|
|
86
89
|
"\$CLEANUP_ARTIFACTS_CRON") route="cleanup-artifacts" ;;
|
|
90
|
+
"\$RECONCILE_BOT_PR_RUNS_CRON") route="reconcile-bot-pr-runs" ;;
|
|
87
91
|
"\$STALE_RECOVERY_CRON") route="stale-recovery" ;;
|
|
88
92
|
"\$PROPOSE_CRON") route="propose" ;;
|
|
89
93
|
*) error="no route for cron '\${SCHEDULE:-}'" ;;
|
|
@@ -106,7 +110,7 @@ classify_route() {
|
|
|
106
110
|
route="\${OPERATION}"
|
|
107
111
|
trigger_kind="\${INPUT_TRIGGER_KIND:-manual}"
|
|
108
112
|
;;
|
|
109
|
-
audit-close | cleanup-artifacts | stale-recovery | validate)
|
|
113
|
+
audit-close | cleanup-artifacts | reconcile-bot-pr-runs | stale-recovery | validate)
|
|
110
114
|
route="\${OPERATION}"
|
|
111
115
|
;;
|
|
112
116
|
*)
|
|
@@ -131,7 +135,7 @@ set -euo pipefail
|
|
|
131
135
|
|
|
132
136
|
echo "── Router wiring ─────────────────────────────────────────────────────────"
|
|
133
137
|
for route in refine implement direct apply-review merge-gate audit propose bot-approve \\
|
|
134
|
-
audit-close cleanup-artifacts stale-recovery validate; do
|
|
138
|
+
audit-close cleanup-artifacts reconcile-bot-pr-runs stale-recovery validate; do
|
|
135
139
|
if grep -q "route == '\${route}'" "\$ROUTER_YML"; then
|
|
136
140
|
PASS=\$((PASS + 1))
|
|
137
141
|
else
|
|
@@ -11,6 +11,7 @@ set -euo pipefail
|
|
|
11
11
|
readonly AUDIT_CRON="17 1 * * 1"
|
|
12
12
|
readonly AUDIT_CLOSE_CRON="43 3 * * *"
|
|
13
13
|
readonly CLEANUP_ARTIFACTS_CRON="0 6 * * *"
|
|
14
|
+
readonly RECONCILE_BOT_PR_RUNS_CRON="*/5 * * * *"
|
|
14
15
|
readonly STALE_RECOVERY_CRON="0 */2 * * *"
|
|
15
16
|
# Daily, but it proposes far less often than daily: the worker holds one open
|
|
16
17
|
# proposal at a time and skips while that slot is filled. The cron is a heartbeat,
|
|
@@ -97,6 +98,7 @@ classify_route() {
|
|
|
97
98
|
"$AUDIT_CRON") route="audit" ;;
|
|
98
99
|
"$AUDIT_CLOSE_CRON") route="audit-close" ;;
|
|
99
100
|
"$CLEANUP_ARTIFACTS_CRON") route="cleanup-artifacts" ;;
|
|
101
|
+
"$RECONCILE_BOT_PR_RUNS_CRON") route="reconcile-bot-pr-runs" ;;
|
|
100
102
|
"$STALE_RECOVERY_CRON") route="stale-recovery" ;;
|
|
101
103
|
"$PROPOSE_CRON") route="propose" ;;
|
|
102
104
|
*) error="no route for cron '${SCHEDULE:-}'" ;;
|
|
@@ -141,7 +143,7 @@ classify_route() {
|
|
|
141
143
|
route="${OPERATION}"
|
|
142
144
|
trigger_kind="${INPUT_TRIGGER_KIND:-manual}"
|
|
143
145
|
;;
|
|
144
|
-
audit-close | cleanup-artifacts | stale-recovery | validate)
|
|
146
|
+
audit-close | cleanup-artifacts | reconcile-bot-pr-runs | stale-recovery | validate)
|
|
145
147
|
route="${OPERATION}"
|
|
146
148
|
;;
|
|
147
149
|
*)
|
|
@@ -148,6 +148,8 @@ assert_route "merge-gate dispatch accepts a positive pull request" merge-gate \
|
|
|
148
148
|
EVENT=workflow_dispatch OPERATION=merge-gate INPUT_PR_NUMBER=7
|
|
149
149
|
assert_route "stale-recovery dispatch needs no numbers" stale-recovery \
|
|
150
150
|
EVENT=workflow_dispatch OPERATION=stale-recovery
|
|
151
|
+
assert_route "reconcile-bot-pr-runs dispatch needs no numbers" reconcile-bot-pr-runs \
|
|
152
|
+
EVENT=workflow_dispatch OPERATION=reconcile-bot-pr-runs
|
|
151
153
|
assert_route "an unknown operation routes nowhere" none \
|
|
152
154
|
EVENT=workflow_dispatch OPERATION=deploy-everything
|
|
153
155
|
assert "a scheduled audit reports its trigger kind" scheduled \
|
|
@@ -184,7 +186,7 @@ for route in refine implement direct apply-review; do
|
|
|
184
186
|
done
|
|
185
187
|
|
|
186
188
|
for route in refine implement direct apply-review merge-gate audit propose bot-approve \
|
|
187
|
-
audit-close cleanup-artifacts stale-recovery validate; do
|
|
189
|
+
audit-close cleanup-artifacts reconcile-bot-pr-runs stale-recovery validate; do
|
|
188
190
|
if grep -q "route == '${route}'" "$ROUTER_YML"; then
|
|
189
191
|
PASS=$((PASS + 1))
|
|
190
192
|
else
|
|
@@ -23,6 +23,7 @@ name: "Agent: Apply Review"
|
|
|
23
23
|
# Router-only worker. The Work Router owns triggers, classification, and rung 1-2 checks.
|
|
24
24
|
# This workflow receives the classified inputs and runs rung 3+.
|
|
25
25
|
imports:
|
|
26
|
+
- github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
|
|
26
27
|
- shared/platform-defaults.md
|
|
27
28
|
- shared/opencode-ci.md
|
|
28
29
|
|
|
@@ -24,6 +24,7 @@ name: "Agent: Audit"
|
|
|
24
24
|
# Shared: network policy only. This workflow owns its Safe Outputs and OpenCode configuration.
|
|
25
25
|
# permissions, engine, model and runs-on cannot be shared , see shared/platform-defaults.md.
|
|
26
26
|
imports:
|
|
27
|
+
- github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
|
|
27
28
|
- shared/platform-defaults.md
|
|
28
29
|
- shared/opencode-ci.md
|
|
29
30
|
|
|
@@ -28,6 +28,7 @@ name: "Agent: Implement Issue"
|
|
|
28
28
|
# Shared: network policy only. This workflow owns its Safe Outputs and OpenCode configuration.
|
|
29
29
|
# permissions, engine, model and runs-on cannot be shared , see shared/platform-defaults.md.
|
|
30
30
|
imports:
|
|
31
|
+
- github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
|
|
31
32
|
- shared/platform-defaults.md
|
|
32
33
|
- shared/opencode-ci.md
|
|
33
34
|
|
|
@@ -25,6 +25,7 @@ name: "Agent: Merge Gate"
|
|
|
25
25
|
# Router-only worker. The Work Router owns triggers, classification, and rung 1-2 checks.
|
|
26
26
|
# This workflow receives the classified inputs and runs rung 3+.
|
|
27
27
|
imports:
|
|
28
|
+
- github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
|
|
28
29
|
- shared/platform-defaults.md
|
|
29
30
|
- shared/opencode-ci.md
|
|
30
31
|
|
|
@@ -37,6 +37,7 @@ name: "Agent: Propose Feature"
|
|
|
37
37
|
# Shared: network policy only. This workflow owns its Safe Outputs and OpenCode configuration.
|
|
38
38
|
# permissions, engine, model and runs-on cannot be shared, see shared/platform-defaults.md.
|
|
39
39
|
imports:
|
|
40
|
+
- github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
|
|
40
41
|
- shared/platform-defaults.md
|
|
41
42
|
- shared/opencode-ci.md
|
|
42
43
|
|
|
@@ -54,6 +54,7 @@ on:
|
|
|
54
54
|
- cron: "17 1 * * 1"
|
|
55
55
|
- cron: "43 3 * * *"
|
|
56
56
|
- cron: "0 6 * * *"
|
|
57
|
+
- cron: "*/5 * * * *"
|
|
57
58
|
- cron: "0 */2 * * *"
|
|
58
59
|
- cron: "29 7 * * *"
|
|
59
60
|
|
|
@@ -73,6 +74,7 @@ on:
|
|
|
73
74
|
- propose
|
|
74
75
|
- audit-close
|
|
75
76
|
- cleanup-artifacts
|
|
77
|
+
- reconcile-bot-pr-runs
|
|
76
78
|
- stale-recovery
|
|
77
79
|
- validate
|
|
78
80
|
issue-number:
|
|
@@ -348,6 +350,84 @@ jobs:
|
|
|
348
350
|
gh api "repos/$REPO/actions/runs/$run_id/approve" --method POST
|
|
349
351
|
done
|
|
350
352
|
|
|
353
|
+
reconcile-bot-pr-runs:
|
|
354
|
+
needs: classify
|
|
355
|
+
if: needs.classify.outputs.route == 'reconcile-bot-pr-runs'
|
|
356
|
+
runs-on: ubuntu-latest
|
|
357
|
+
timeout-minutes: 5
|
|
358
|
+
permissions:
|
|
359
|
+
actions: write
|
|
360
|
+
pull-requests: read
|
|
361
|
+
steps:
|
|
362
|
+
# Runs only the default-branch workflow and never checks out pull-request code.
|
|
363
|
+
# External users cannot impersonate github-actions[bot].
|
|
364
|
+
- name: Create Platform bot token
|
|
365
|
+
id: app-token
|
|
366
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
367
|
+
with:
|
|
368
|
+
client-id: ${{ secrets.BOT_APP_ID }}
|
|
369
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
370
|
+
permission-actions: write
|
|
371
|
+
permission-pull-requests: read
|
|
372
|
+
- name: Approve checks and dispatch Merge Gate for trusted bot pull requests
|
|
373
|
+
env:
|
|
374
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
375
|
+
REPO: ${{ github.repository }}
|
|
376
|
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
377
|
+
run: |
|
|
378
|
+
set -euo pipefail
|
|
379
|
+
|
|
380
|
+
gh api "repos/$REPO/pulls?state=open&per_page=100" |
|
|
381
|
+
jq -r --arg repo "$REPO" '
|
|
382
|
+
.[]
|
|
383
|
+
| select(
|
|
384
|
+
.user.login == "github-actions[bot]"
|
|
385
|
+
and .head.repo.full_name == $repo
|
|
386
|
+
)
|
|
387
|
+
| [.number, .head.ref] | @tsv
|
|
388
|
+
' |
|
|
389
|
+
while IFS=$'\t' read -r pr_number branch; do
|
|
390
|
+
gh api "repos/$REPO/actions/runs?status=action_required&branch=$branch" \
|
|
391
|
+
--jq '.workflow_runs[]
|
|
392
|
+
| select(
|
|
393
|
+
.event == "pull_request"
|
|
394
|
+
and (.name == "App: CI" or .name == "Visual evidence")
|
|
395
|
+
)
|
|
396
|
+
| .id' |
|
|
397
|
+
while IFS= read -r run_id; do
|
|
398
|
+
echo "Approving trusted bot run $run_id"
|
|
399
|
+
gh api "repos/$REPO/actions/runs/$run_id/approve" --method POST
|
|
400
|
+
done
|
|
401
|
+
|
|
402
|
+
gate_recorded=$(gh api "repos/$REPO/issues/$pr_number/comments?per_page=100" \
|
|
403
|
+
--jq 'any(.[]; .body | contains("<!-- agent-merge-gate -->"))')
|
|
404
|
+
if [ "$gate_recorded" = "true" ]; then
|
|
405
|
+
continue
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
ci=$(gh api "repos/$REPO/actions/runs?branch=$branch&per_page=100" \
|
|
409
|
+
--jq '[.workflow_runs[]
|
|
410
|
+
| select(
|
|
411
|
+
.name == "App: CI"
|
|
412
|
+
and .event == "pull_request"
|
|
413
|
+
and .status == "completed"
|
|
414
|
+
and (.conclusion == "success" or .conclusion == "failure")
|
|
415
|
+
)
|
|
416
|
+
] | sort_by(.created_at) | last // empty | [.id, .conclusion] | @tsv')
|
|
417
|
+
if [ -z "$ci" ]; then
|
|
418
|
+
continue
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
IFS=$'\t' read -r ci_run_id ci_conclusion <<<"$ci"
|
|
422
|
+
echo "Dispatching Merge Gate for PR #$pr_number ($ci_conclusion, run $ci_run_id)"
|
|
423
|
+
gh api "repos/$REPO/actions/workflows/work-router.yml/dispatches" \
|
|
424
|
+
--method POST \
|
|
425
|
+
-f ref="$DEFAULT_BRANCH" \
|
|
426
|
+
-f "inputs[operation]=merge-gate" \
|
|
427
|
+
-f "inputs[pr-number]=$pr_number" \
|
|
428
|
+
-f "inputs[ci-conclusion]=$ci_conclusion" \
|
|
429
|
+
-f "inputs[ci-run-id]=$ci_run_id"
|
|
430
|
+
|
|
351
431
|
audit-close:
|
|
352
432
|
needs: classify
|
|
353
433
|
if: needs.classify.outputs.route == 'audit-close'
|