@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.
@@ -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 = `${compileLine}\n${stageLine}\n${actionLockLine}\n`;
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
- if (content.includes(stageLine))
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}${stageLine}\n${actionLockLine}\n` };
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\ngit add -- .github/workflows/*.lock.yml\n[ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\n");
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\nnode scripts/compile-agent-workflows.mjs\ngit add -- .github/workflows/*.lock.yml\n[ ! -f .github/actions/actions-lock.json ] || git add -- .github/actions/actions-lock.json\n");
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 add -- .github/workflows/*.lock.yml");
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
 
@@ -25,6 +25,7 @@ description: |
25
25
  name: "Agent: Direct"
26
26
 
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
 
@@ -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
 
@@ -33,6 +33,7 @@ description: |
33
33
  name: "Agent: Refine Issue"
34
34
 
35
35
  imports:
36
+ - github/gh-aw/.github/workflows/shared/opencode.md@v0.86.2
36
37
  - shared/platform-defaults.md
37
38
  - shared/opencode-ci.md
38
39
 
@@ -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'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",