@plainconceptsplatform/workflows 1.0.14 → 1.1.1

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.
@@ -35,7 +35,7 @@ source "${HERE}/../classify-route/classify-route.sh"
35
35
  # Every worker route the package knows. The ones with a worker file here are installed; the
36
36
  # others must have no job in this router. Plumbing routes are in every router.
37
37
  ALL_WORKER_ROUTES=(refine implement triage apply-review merge-gate audit release)
38
- PLUMBING_ROUTES=(bot-approve audit-close cleanup-artifacts reconcile-bot-pr-runs housekeeping validate)
38
+ PLUMBING_ROUTES=(bot-approve evidence audit-close cleanup-artifacts reconcile-bot-pr-runs housekeeping validate)
39
39
 
40
40
  worker_installed() {
41
41
  [ -f "${WORKFLOWS_DIR}/agent-$1.md" ]
@@ -78,7 +78,7 @@ route_field() {
78
78
  unset EVENT ACTION LABEL ISSUE_LABELS EVENT_ISSUE_NUMBER EVENT_PR_NUMBER \
79
79
  COMMENT_ON_PR COMMENT_SENDER_TYPE RUN_PR_NUMBER RUN_CONCLUSION RUN_ID \
80
80
  SCHEDULE OPERATION INPUT_ISSUE_NUMBER INPUT_PR_NUMBER INPUT_MODE \
81
- INPUT_CI_CONCLUSION INPUT_CI_RUN_ID INPUT_TRIGGER_KIND
81
+ INPUT_CI_CONCLUSION INPUT_CI_RUN_ID INPUT_TRIGGER_KIND PR_MERGED EVIDENCE_ENABLED
82
82
 
83
83
  for assignment in "${assignments[@]}"; do
84
84
  key="${assignment%%=*}"
@@ -187,6 +187,14 @@ assert_route "a submitted review routes to apply-review" apply-review \
187
187
  EVENT=pull_request_review EVENT_PR_NUMBER=7
188
188
  assert_route "a pull_request_target routes to bot-approve" bot-approve \
189
189
  EVENT=pull_request_target ACTION=opened
190
+ assert_route "a merged pull request routes to evidence" evidence \
191
+ EVENT=pull_request_target ACTION=closed PR_MERGED=true EVENT_PR_NUMBER=7
192
+ assert "evidence carries the merged pull request" 7 \
193
+ "$(route_field pr-number EVENT=pull_request_target ACTION=closed PR_MERGED=true EVENT_PR_NUMBER=7)"
194
+ assert_route "a pull request closed unmerged does not route to evidence" bot-approve \
195
+ EVENT=pull_request_target ACTION=closed PR_MERGED=false EVENT_PR_NUMBER=7
196
+ assert_route "EVIDENCE_ENABLED=false turns the merge route off" none \
197
+ EVENT=pull_request_target ACTION=closed PR_MERGED=true EVENT_PR_NUMBER=7 EVIDENCE_ENABLED=false
190
198
 
191
199
  echo "── CI completion ─────────────────────────────────────────────────────────"
192
200
  assert_route "a failed App CI run on a pull request routes to merge-gate" merge-gate \
@@ -250,6 +258,14 @@ assert "implement dispatch forwards the attempt count" 2 \
250
258
  "$(route_field attempts EVENT=workflow_dispatch OPERATION=implement INPUT_ISSUE_NUMBER=42 INPUT_ATTEMPTS_SO_FAR=2)"
251
259
  assert_route "release dispatch needs no numbers" release \
252
260
  EVENT=workflow_dispatch OPERATION=release
261
+ assert_route "evidence dispatch with a pull request routes to evidence" evidence \
262
+ EVENT=workflow_dispatch OPERATION=evidence INPUT_PR_NUMBER=7
263
+ assert_route "evidence dispatch without a pull request routes nowhere" none \
264
+ EVENT=workflow_dispatch OPERATION=evidence INPUT_PR_NUMBER=
265
+ assert_route "evidence dispatch runs even where the merge route is off" evidence \
266
+ EVENT=workflow_dispatch OPERATION=evidence INPUT_PR_NUMBER=7 EVIDENCE_ENABLED=false
267
+ assert_route "the retired visual-verify operation routes nowhere" none \
268
+ EVENT=workflow_dispatch OPERATION=visual-verify INPUT_PR_NUMBER=7
253
269
  assert_route "reconcile-bot-pr-runs dispatch needs no numbers" reconcile-bot-pr-runs \
254
270
  EVENT=workflow_dispatch OPERATION=reconcile-bot-pr-runs
255
271
  assert_route "an unknown operation routes nowhere" none \
@@ -1667,60 +1683,59 @@ if worker_installed merge-gate; then
1667
1683
  if [ "$GATE_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1668
1684
  fi
1669
1685
 
1670
- echo "── Visual verify wiring ───────────────────────────────────────────────────"
1686
+ echo "── Evidence wiring ───────────────────────────────────────────────────────"
1671
1687
 
1672
- # The merge-gate's conclude job dispatches agent-visual-verify before merging when the outcome
1673
- # is auto-merge and VISUAL_VERIFY_ENABLED is true. Each of these is one line an edit could drop
1674
- # with nothing going red: a missing dispatch step, a missing env var, or a missing worker file.
1675
- # The guard against the original bug comes first: the call is a dispatch, never a step doing
1676
- # `uses:` on a workflow file -- that resolves as a local action, dies with "Can't find
1677
- # action.yml", and under continue-on-error every auto-merge shipped with zero screenshots
1678
- # and nothing red.
1679
- if worker_installed merge-gate; then
1680
- VV_OK=1
1681
-
1682
- vv() {
1683
- grep -qE "$1" "$2" || { VV_OK=0; echo "FAIL: $3" >&2; }
1684
- }
1685
-
1686
- # The dispatch step exists and runs only on auto-merge with the feature flag on.
1687
- vv 'name: Dispatch visual verification' "$MERGE_GATE_WORKER_MD" 'merge-gate has no visual-verify dispatch step'
1688
- vv "outcome == 'auto-merge'.*VISUAL_VERIFY_ENABLED == 'true'" "$MERGE_GATE_WORKER_MD" 'visual-verify dispatch has wrong guard'
1689
-
1690
- # The shape that never worked: a step calling a reusable workflow file. Also reject the
1691
- # swallowed-failure form it shipped with.
1692
- vv 'operation=visual-verify' "$MERGE_GATE_WORKER_MD" 'the dispatch must target the router operation, not a nested file'
1693
- if grep -qE '^ - name:.*\n.*uses: \./\.github/workflows/agent-visual-verify' "$MERGE_GATE_WORKER_MD" 2>/dev/null \
1694
- || grep -A2 '^ - name:' "$MERGE_GATE_WORKER_MD" | grep -q 'uses: \./\.github/workflows/'; then
1695
- VV_OK=0
1696
- echo "FAIL: a step still uses: a workflow file; the runner resolves it as a local action and it cannot work" >&2
1697
- fi
1698
-
1699
- # The env vars the worker needs.
1700
- vv 'VISUAL_VERIFY_ENABLED:' "$MERGE_GATE_WORKER_MD" 'merge-gate missing VISUAL_VERIFY_ENABLED env var'
1701
- vv 'VISUAL_VERIFY_START_COMMAND:' "$MERGE_GATE_WORKER_MD" 'merge-gate missing VISUAL_VERIFY_START_COMMAND env var'
1702
-
1703
- # The worker gates its agent on its own configuration, so an unconfigured repository
1704
- # runs no model: the subject job exposes it, the top-level if: reads it.
1705
- vv 'enabled: ' "${WORKFLOWS_DIR}/agent-visual-verify.md" 'visual-verify subject does not expose the enabled output'
1706
- vv "outputs.enabled == 'true'" "${WORKFLOWS_DIR}/agent-visual-verify.md" 'visual-verify agent gate does not read the enabled output'
1707
-
1708
- # The worker file exists.
1709
- VV_WORKER_MD="${WORKFLOWS_DIR}/agent-visual-verify.md"
1710
- if [ ! -f "$VV_WORKER_MD" ]; then
1711
- VV_OK=0
1712
- echo "FAIL: agent-visual-verify.md does not exist" >&2
1713
- fi
1688
+ # A merged pull request's verification plan is replayed by evidence.yml, a plain workflow with no
1689
+ # model. The shape it replaced (agent-visual-verify) never produced one screenshot, and each check
1690
+ # below is one way it failed: dispatched from the merge gate it raced the merge, published with a
1691
+ # token `gh --attach` refuses, and would have run pull request code beside a write token.
1692
+ EVIDENCE_YML="${WORKFLOWS_DIR}/evidence.yml"
1693
+ EV_OK=1
1694
+ ev() {
1695
+ grep -qE "$1" "$2" || { EV_OK=0; echo "FAIL: $3" >&2; }
1696
+ }
1697
+ ev_absent() {
1698
+ if grep -qE "$1" "$2"; then EV_OK=0; echo "FAIL: $3" >&2; fi
1699
+ }
1714
1700
 
1715
- # The attach-screenshots action exists.
1716
- VV_ATTACH="${HERE}/../attach-screenshots/action.yml"
1717
- if [ ! -f "$VV_ATTACH" ]; then
1718
- VV_OK=0
1719
- echo "FAIL: attach-screenshots/action.yml does not exist" >&2
1720
- fi
1701
+ if [ ! -f "$EVIDENCE_YML" ]; then
1702
+ EV_OK=0
1703
+ echo "FAIL: evidence.yml does not exist" >&2
1704
+ else
1705
+ ev 'uses: \./\.github/workflows/evidence\.yml' "$ROUTER_YML" 'the router does not call evidence.yml'
1706
+ ev '^ EVIDENCE_ENABLED: ' "$ROUTER_YML" 'the router env does not declare EVIDENCE_ENABLED'
1707
+ ev 'uses: \./\.github/actions/capture-evidence' "$EVIDENCE_YML" 'evidence.yml does not use the capture-evidence action'
1708
+ if [ ! -f "${HERE}/../capture-evidence/action.yml" ] || [ ! -f "${HERE}/../capture-evidence/capture.mjs" ]; then
1709
+ EV_OK=0
1710
+ echo "FAIL: the capture-evidence action is incomplete" >&2
1711
+ fi
1712
+
1713
+ # The capture job runs the pull request's code, so it holds no secret and no write.
1714
+ capture_job="$(sed -n '/^ capture:/,/^ publish:/p' "$EVIDENCE_YML")"
1715
+ if grep -qE 'secrets\.|contents: write|pull-requests: write|issues: write' <<<"$capture_job"; then
1716
+ EV_OK=0
1717
+ echo "FAIL: evidence.yml's capture job runs pull request code and must hold no secret and no write" >&2
1718
+ fi
1719
+ # The publish job writes, so it runs no repository code: no checkout and no local action.
1720
+ publish_job="$(sed -n '/^ publish:/,$p' "$EVIDENCE_YML")"
1721
+ if grep -qE 'actions/checkout|uses: \./' <<<"$publish_job"; then
1722
+ EV_OK=0
1723
+ echo "FAIL: evidence.yml's publish job holds a write token and must not check out or run repository code" >&2
1724
+ fi
1725
+ ev_absent 'secrets\.' "$EVIDENCE_YML" 'evidence.yml must need no secret; GITHUB_TOKEN is enough'
1726
+ ev_absent 'gh (issue|pr) comment.*--attach' "$EVIDENCE_YML" 'gh --attach refuses installation tokens; publish through the evidence branch'
1727
+ fi
1721
1728
 
1722
- if [ "$VV_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1729
+ # The retired worker stays retired: nothing dispatches it and nothing waits for it.
1730
+ if worker_installed merge-gate; then
1731
+ ev_absent 'operation=visual-verify|VISUAL_VERIFY_' "$MERGE_GATE_WORKER_MD" 'merge-gate still carries the retired visual-verify wiring'
1723
1732
  fi
1733
+ if [ -f "${WORKFLOWS_DIR}/agent-visual-verify.md" ]; then
1734
+ EV_OK=0
1735
+ echo "FAIL: agent-visual-verify.md was retired by evidence.yml and must not come back" >&2
1736
+ fi
1737
+
1738
+ if [ "$EV_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
1724
1739
 
1725
1740
  echo "── Expression functions ──────────────────────────────────────────────────"
1726
1741
 
@@ -2275,13 +2290,7 @@ for worker_md in "${WORKFLOWS_DIR}"/agent-*.md; do
2275
2290
  case " $allowed_pools " in *" $p "*) ;; *) ok=0 ;; esac
2276
2291
  done
2277
2292
  if ! echo "$got" | grep -qw "$AGENT_POOL"; then
2278
- # visual-verify gets its agent runs-on from a shared import, not from the .md file,
2279
- # so the pool check only sees the subject gate job on RunnerLandingZone.
2280
- if [ "$worker_name" = "agent-visual-verify" ] && echo "$got" | grep -qw "$PLUMBING_POOL"; then
2281
- :
2282
- else
2283
- ok=0
2284
- fi
2293
+ ok=0
2285
2294
  fi
2286
2295
  if [ "$ok" -ne 1 ]; then
2287
2296
  POOL_OK=0
@@ -2289,10 +2298,18 @@ for worker_md in "${WORKFLOWS_DIR}"/agent-*.md; do
2289
2298
  fi
2290
2299
  done
2291
2300
 
2301
+ # The router's own jobs run on the agent fleet's per-run lease: `[agents-aw, "affinity-..."]`,
2302
+ # which pools_named reads as its first token. That is the shape the compile step rewrites every
2303
+ # worker lock to as well, so it is the router's pool, not a drift from agents-arc.
2304
+ AFFINITY_POOL="[agents-aw,"
2292
2305
  router_pools="$(pools_named "$ROUTER_YML" | tr '\n' ' ' | sed 's/ $//')"
2293
- if ! echo "$router_pools" | grep -qw "$AGENT_POOL"; then
2306
+ router_ok=1
2307
+ for p in $router_pools; do
2308
+ case " ${AGENT_POOL} ${PLUMBING_POOL} ${AFFINITY_POOL} " in *" $p "*) ;; *) router_ok=0 ;; esac
2309
+ done
2310
+ if [ -z "$router_pools" ] || [ "$router_ok" -ne 1 ]; then
2294
2311
  POOL_OK=0
2295
- echo "FAIL: the router names runner pool(s) '${router_pools}' but must name only '${AGENT_POOL}' and/or '${PLUMBING_POOL}' (with at least ${AGENT_POOL})" >&2
2312
+ echo "FAIL: the router names runner pool(s) '${router_pools}' but must name only '${AFFINITY_POOL} affinity]', '${AGENT_POOL}' and/or '${PLUMBING_POOL}'" >&2
2296
2313
  fi
2297
2314
 
2298
2315
  # And the pool must not be reintroduced as a per-consumer value: that is the mechanism that let
@@ -412,7 +412,8 @@ reads them next time.
412
412
 
413
413
  ## agent-release.md
414
414
 
415
- Manual dispatch only. The agent writes prose; a deterministic job does everything irreversible.
415
+ Manual dispatch only. The agent writes prose; deterministic jobs do everything irreversible, and
416
+ nothing reaches `main` except through a pull request.
416
417
 
417
418
  ```mermaid
418
419
  flowchart TD
@@ -420,26 +421,34 @@ flowchart TD
420
421
  relFacts("Facts (rung 3)<br/>Commit log since the last tag<br/>and the current version, to disk") --> relAgent
421
422
  relAgent("Agent (rung 5)<br/>Categorise by conventional-commit<br/>prefix, write release-notes.md.<br/>The only judgement in this route") --> relNoop
422
423
  relNoop("noop<br/>the agent writes a file,<br/>not a GitHub object") --> relConclude
423
- relConclude["conclude (rung 6)<br/>Bump: BREAKING is major,<br/>feat is minor, else patch"] --> relPush
424
- relPush("Commit, tag, push,<br/>create the GitHub Release") --> relDone
424
+ relConclude["conclude (rung 6)<br/>Bump: BREAKING is major,<br/>feat is minor, else patch"] --> relPr
425
+ relPr("App token: push release/vX.Y.Z,<br/>open chore(release) PR,<br/>enable squash auto-merge") --> relMerge
426
+ relMerge{"CI green and<br/>auto-merge allowed?"}
427
+ relMerge -->|yes| relTag
428
+ relMerge -.->|auto-merge disabled| relOpen
429
+ relTag("release-tag.yml on push to main<br/>GITHUB_TOKEN: tag vX.Y.Z,<br/>Release with the PR body as notes") --> relDone
425
430
  relConclude -.->|fails| relFail
426
- relDone(("Released<br/>tag and Release published"))
427
- relFail(("Failed<br/>no tag created, nothing partial"))
431
+ relOpen(("PR left open<br/>warning names the fix"))
432
+ relDone(("Released<br/>tag and Release published,<br/>no tag-triggered workflow fired"))
433
+ relFail(("Failed<br/>no branch, nothing partial"))
428
434
 
429
435
  classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
430
436
  classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
431
437
  classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
438
+ classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
432
439
  classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
433
440
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
434
441
  class relStart start
435
- class relFacts,relAgent,relNoop,relPush action
436
- class relConclude decision
442
+ class relFacts,relAgent,relNoop,relPr,relTag action
443
+ class relConclude,relMerge decision
437
444
  class relFail failure
445
+ class relOpen idle
438
446
  class relDone success
439
447
  ```
440
448
 
441
449
  The version bump, the tag and the Release are deterministic on purpose. The agent's only output is
442
- a file on disk, so a confused model cannot publish a release.
450
+ a file on disk, so a confused model cannot publish a release. The tag and the Release use
451
+ `GITHUB_TOKEN` so that a consumer deploying on `v*` tags does not deploy because a release was cut.
443
452
 
444
453
 
445
454
  ---
@@ -613,49 +622,41 @@ earlier versions of them passed against a deliberately broken guard and were rew
613
622
 
614
623
  ---
615
624
 
616
- ## agent-visual-verify.md
625
+ ## evidence.yml
617
626
 
618
- Runs after merge-gate decides to auto-merge, before the actual merge, and produces **evidence, not
619
- code changes**: it writes a verification plan, drives a real browser through the app, and attaches
620
- screenshots to the linked issue. It is called directly by merge-gate's `conclude` job, so it is not
621
- a router route. Failures never block the merge a browser that will not start or an app that never
622
- comes up emits a warning comment and the merge proceeds.
627
+ Archives what a merged pull request looks like. Not an agent: the plan was written by the agent that
628
+ implemented the change (`pc-repo-verify`), and this replays it. The router calls it when a pull
629
+ request merges, and `operation=evidence` runs it by hand. Nothing waits on it and nothing reads it.
630
+ The capture job runs the pull request's code with a read-only token; only the publish job writes,
631
+ and it runs no repository code.
623
632
 
624
633
  ```mermaid
625
634
  flowchart TD
626
- vvStart("merge-gate conclude<br/>calls agent-visual-verify") --> vvSubject
627
- vvSubject["subject (rung 4)<br/>PR open? linked issue found?"] --> vvConfig
628
- vvConfig{"config (rung 4)<br/>VISUAL_VERIFY_ENABLED and<br/>START_COMMAND set?"}
629
- vvConfig -.->|"no"| vvIdle
630
- vvConfig -->|"yes"| vvFacts
631
- vvFacts["Facts (rung 3)<br/>PR branch checked out,<br/>issue context loaded"] --> vvPlan
632
- vvPlan{"/repo-verify (rung 5)<br/>reachable UI surface?"}
633
- vvPlan -.->|"not-applicable"| vvNoUi
634
- vvPlan -->|"yes"| vvStartCmd
635
- vvStartCmd{"start command set?<br/>VISUAL_VERIFY_START_COMMAND"}
636
- vvStartCmd -.->|"empty"| vvNoStart
637
- vvStartCmd -->|"yes"| vvAgent
638
- vvAgent["Agent (rung 5)<br/>parse waypoints into plan.json,<br/>one add_comment on the issue"] --> vvCapture
639
- vvCapture["post-agent (rung 6)<br/>build + start the app,<br/>agent-browser captures per waypoint"] --> vvAttach
640
- vvAttach["attach-screenshots (rung 6)<br/>always(), subject found + enabled"] --> vvShutdown
641
- vvShutdown["shut down the app"] --> vvDone
642
- vvCapture -.->|"app never started,<br/>no plan, or all waypoints failed"| vvPartial
643
- vvDone(("Verified<br/>screenshots attached,<br/>merge proceeds"))
644
- vvPartial(("Attach what exists<br/>no-capture explanation,<br/>run stays green"))
645
- vvNoUi(("No UI surface<br/>summary comment, stop"))
646
- vvNoStart(("Not configured<br/>warning comment, stop"))
647
- vvIdle(("Idle<br/>unconfigured or PR not open"))
635
+ evStart("router: pull request merged<br/>or operation=evidence") --> evPlans
636
+ evPlans{"verification-plan.md<br/>in the pull request?"}
637
+ evPlans -.->|"no"| evIdle
638
+ evPlans -->|"yes"| evInspect
639
+ evInspect{"capture-evidence inspect<br/>a replayable journey?"}
640
+ evInspect -.->|"not-applicable or prose"| evIdle
641
+ evInspect -->|"yes"| evApp
642
+ evApp["capture (read-only)<br/>check out the head,<br/>install, start the app"] --> evUp
643
+ evUp{"app answers at<br/>the plan's url?"}
644
+ evUp -->|"yes"| evReplay
645
+ evUp -.->|"no, after the timeout"| evUnreached
646
+ evReplay["replay each waypoint<br/>in headless Chromium,<br/>one JPEG per waypoint"] --> evPublish
647
+ evUnreached["record why nothing<br/>was captured"] --> evPublish
648
+ evPublish["publish (GITHUB_TOKEN)<br/>commit to the orphan evidence branch,<br/>upsert one comment"] --> evDone
649
+ evDone(("Archived<br/>on the pull request"))
650
+ evIdle(("Nothing to capture"))
648
651
 
649
652
  classDef start fill:#ffffff,stroke:#172033,stroke-width:2px,color:#172033
650
653
  classDef action fill:#eef0ff,stroke:#554cff,stroke-width:2px,color:#172033
651
654
  classDef decision fill:#fff8e8,stroke:#c75b00,stroke-width:2px,color:#172033
652
- classDef idle fill:#202c40,stroke:#738198,stroke-width:2px,color:#ffffff
653
- classDef failure fill:#fff0f0,stroke:#ef2929,stroke-width:2px,color:#8b1a1a
655
+ classDef idle fill:#f4f4f6,stroke:#5b5b66,stroke-width:2px,color:#2b2b33
654
656
  classDef success fill:#e8f8ec,stroke:#18883c,stroke-width:2px,color:#145a32
655
- class vvStart start
656
- class vvSubject,vvFacts,vvCapture,vvAttach,vvShutdown action
657
- class vvConfig,vvPlan,vvStartCmd decision
658
- class vvIdle idle
659
- class vvNoUi,vvNoStart,vvPartial failure
660
- class vvDone success
657
+ class evStart start
658
+ class evApp,evReplay,evUnreached,evPublish action
659
+ class evPlans,evInspect,evUp decision
660
+ class evIdle idle
661
+ class evDone success
661
662
  ```
@@ -72,14 +72,6 @@ env:
72
72
  # one unusable report can keep every other bot pull request in the repository waiting.
73
73
  PARK_AT_UNUSABLE_OUTPUT: "2"
74
74
  ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/issue-context.json
75
- # Visual verification runs after auto-merge is decided and before the merge itself. The worker
76
- # runs /repo-verify, drives a browser via agent-browser, and attaches screenshots to the linked
77
- # issue. Failures never block the merge. Consumers opt out by setting this to "false".
78
- VISUAL_VERIFY_ENABLED: "true"
79
- VISUAL_VERIFY_BUILD_COMMAND: ""
80
- VISUAL_VERIFY_START_COMMAND: ""
81
- VISUAL_VERIFY_PORT: "3000"
82
- VISUAL_VERIFY_WAIT_SECONDS: "30"
83
75
  GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
84
76
  GIT_AUTHOR_NAME: "github-actions[bot]"
85
77
  GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
@@ -382,17 +374,6 @@ jobs:
382
374
  protected-hit: ${{ needs.protected_changes.outputs.requires_review }}
383
375
  owner-hit: ${{ needs.protected_changes.outputs.owner_hit }}
384
376
  confidence-threshold: ${{ env.CONFIDENCE_THRESHOLD }}
385
- # The pre-merge screenshot pass is dispatched rather than nested. A nested
386
- # workflow_call job cannot live downstream of this worker's agent: the compiler hoists
387
- # prompt-referenced jobs into the agent's needs, and any call job depending on
388
- # validate_output closes a cycle through agent (measured: the graph refuses to compile).
389
- # The first version of this wiring was a step doing `uses:` on the worker's lock file,
390
- # which is not a call at all -- the runner resolved it as a local action and died with
391
- # "Can't find action.yml" under continue-on-error, so every auto-merge since the feature
392
- # shipped merged with zero screenshots and nothing red. This dispatches the router's
393
- # existing visual-verify operation (the dispatch-triage pattern) before the merge below.
394
- # The merge pins the head SHA with --match-head-commit, and visual-verify re-reads the
395
- # pull request itself, so the called worker owns its own subject validation.
396
377
  conclude:
397
378
  needs: [activation, subject, protected_changes, agent, safe_outputs, validate_output]
398
379
  # `protected_changes.result == 'success'` is stated rather than relied on. GitHub skips a job
@@ -411,8 +392,6 @@ jobs:
411
392
  contents: write
412
393
  issues: write
413
394
  pull-requests: write
414
- # dispatching the visual-verify pass through the router's workflow_dispatch
415
- actions: write
416
395
  steps:
417
396
  - name: Create bot token
418
397
  id: app-token
@@ -474,30 +453,6 @@ jobs:
474
453
  ```
475
454
 
476
455
  Findings and verification on the linked issue: #${{ needs.subject.outputs.issue }}. [View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
477
- # Screenshots before the merge. The visual-verify pass is dispatched through the
478
- # router (see the visual_verify note above conclude) and cannot be awaited from here;
479
- # the merge below pins the head SHA with --match-head-commit, so a screenshot run on
480
- # the same head describes exactly what is being merged. GITHUB_TOKEN: a dispatch
481
- # raises one workflow_run-free event, and the called worker re-owns its subject.
482
- - name: Dispatch visual verification
483
- if: needs.validate_output.outputs.outcome == 'auto-merge' && env.VISUAL_VERIFY_ENABLED == 'true'
484
- env:
485
- GH_TOKEN: ${{ github.token }}
486
- REPO: ${{ github.repository }}
487
- REF: ${{ github.event.repository.default_branch }}
488
- PR: ${{ needs.subject.outputs.pr }}
489
- ISSUE: ${{ needs.subject.outputs.issue }}
490
- run: |
491
- set -euo pipefail
492
- if gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
493
- -f operation=visual-verify -f pr-number="$PR" -f issue-number="$ISSUE"; then
494
- echo "Visual verification dispatched for PR #$PR."
495
- else
496
- echo "::warning::could not dispatch visual verification for PR #$PR; merging without screenshots."
497
- fi
498
- # The visual verification pass is the visual_verify dispatch above. It runs as its own
499
- # worker because a step cannot call a reusable workflow, and the merge below may land
500
- # while the capture is still running -- the head SHA pin keeps the two honest.
501
456
  - name: Merge approved pull request
502
457
  if: needs.validate_output.outputs.outcome == 'auto-merge'
503
458
  env:
@@ -6,9 +6,10 @@ env:
6
6
  GIT_COMMITTER_NAME: "github-actions[bot]"
7
7
  GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
8
8
  description: |
9
- Generates release notes from the commit log and creates a tagged GitHub Release.
9
+ Generates release notes from the commit log and opens a release pull request.
10
10
  The agent writes human-readable release notes; every other step (version bump,
11
- commit, tag, push, release creation) is deterministic.
11
+ release branch, pull request, auto-merge) is deterministic. release-tag.yml tags
12
+ the merged commit and publishes the GitHub Release.
12
13
 
13
14
  Router-only worker: triggered exclusively via workflow_call from work-router.yml.
14
15
  Contract input: version-bump(patch|minor|major|auto).
@@ -92,6 +93,15 @@ safe-outputs:
92
93
  timeout-minutes: 30
93
94
 
94
95
  jobs:
96
+ # The release is a pull request, not a push. A repository whose `main` requires pull
97
+ # requests and status checks cannot accept a direct push, and a push with GITHUB_TOKEN would
98
+ # not start CI on the branch either, so the required checks would never report. The App token
99
+ # does both: its push and its pull request fire workflows, so CI runs and auto-merge can land
100
+ # the bump once it is green.
101
+ #
102
+ # No tag and no GitHub Release here. `release-tag.yml` creates both after the merge, with
103
+ # GITHUB_TOKEN on purpose: a consumer deploying to production on `v*` tags must not start
104
+ # that deploy from a release (see the comment at the top of release-tag.yml).
95
105
  conclude:
96
106
  needs: [activation, agent, safe_outputs]
97
107
  if: >
@@ -100,7 +110,7 @@ jobs:
100
110
  needs.safe_outputs.result == 'success'
101
111
  runs-on: agents-arc
102
112
  permissions:
103
- contents: write
113
+ contents: read
104
114
  steps:
105
115
  - name: Download agent output artifact
106
116
  id: download-agent-output
@@ -110,10 +120,16 @@ jobs:
110
120
  pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}"
111
121
  merge-multiple: true
112
122
  path: /tmp/gh-aw/
123
+ - name: Create bot token
124
+ id: app-token
125
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
126
+ with:
127
+ client-id: ${{ secrets.BOT_APP_ID }}
128
+ private-key: ${{ secrets.BOT_PRIVATE_KEY }}
113
129
  - name: Checkout repository
114
130
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
115
131
  with:
116
- token: ${{ github.token }}
132
+ token: ${{ steps.app-token.outputs.token }}
117
133
  fetch-depth: 0
118
134
  - name: Read release notes and determine version
119
135
  id: release
@@ -173,32 +189,72 @@ jobs:
173
189
  }
174
190
  }
175
191
  "
176
- - name: Commit, tag, and push
192
+ - name: Commit and push the release branch
193
+ id: branch
177
194
  env:
178
195
  NEW_VERSION: ${{ steps.release.outputs.new_version }}
179
- GH_TOKEN: ${{ github.token }}
180
196
  run: |
181
197
  set -euo pipefail
198
+ base=$(git rev-parse --abbrev-ref HEAD)
199
+ branch="release/v${NEW_VERSION}"
200
+ # A second dispatch before the first release PR merged computes the same version from
201
+ # the same package.json. Two PRs bumping to one version would tag it twice.
202
+ if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
203
+ echo "::error::Branch $branch already exists: a release PR for v${NEW_VERSION} is open or was abandoned. Merge or close it, delete the branch, then dispatch again."
204
+ exit 1
205
+ fi
182
206
  git config user.name "github-actions[bot]"
183
207
  git config user.email "github-actions[bot]@users.noreply.github.com"
208
+ git switch -c "$branch"
184
209
  git add package.json
185
210
  [ -f cli/package.json ] && git add cli/package.json
186
211
  git commit -m "chore(release): v${NEW_VERSION}"
187
- git tag "v${NEW_VERSION}"
188
- git push
189
- git push --tags
190
- echo "Tagged v${NEW_VERSION} and pushed."
191
- - name: Create GitHub Release
212
+ git push origin "$branch"
213
+ echo "base=$base" >> "$GITHUB_OUTPUT"
214
+ echo "branch=$branch" >> "$GITHUB_OUTPUT"
215
+ echo "Pushed $branch (base $base)."
216
+ # The body is what release-tag.yml publishes as the GitHub Release notes. The marker is how
217
+ # it tells this PR from any other PR associated with the merge commit.
218
+ - name: Open the release pull request
219
+ id: pr
192
220
  env:
193
221
  NEW_VERSION: ${{ steps.release.outputs.new_version }}
194
- GH_TOKEN: ${{ github.token }}
222
+ BASE: ${{ steps.branch.outputs.base }}
223
+ BRANCH: ${{ steps.branch.outputs.branch }}
224
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
195
225
  run: |
196
226
  set -euo pipefail
197
- gh release create "v${NEW_VERSION}" \
227
+ {
228
+ echo "<!-- release:v${NEW_VERSION} -->"
229
+ cat /tmp/release-notes.md
230
+ } > /tmp/release-pr-body.md
231
+ url=$(gh pr create \
198
232
  --repo "$GITHUB_REPOSITORY" \
199
- --title "v${NEW_VERSION}" \
200
- --notes-file /tmp/release-notes.md
201
- echo "GitHub Release v${NEW_VERSION} created."
233
+ --base "$BASE" \
234
+ --head "$BRANCH" \
235
+ --title "chore(release): v${NEW_VERSION}" \
236
+ --body-file /tmp/release-pr-body.md)
237
+ echo "url=$url" >> "$GITHUB_OUTPUT"
238
+ echo "Opened $url"
239
+ # Squash keeps the PR title as the commit subject, which is what release-tag.yml matches.
240
+ - name: Enable auto-merge
241
+ env:
242
+ NEW_VERSION: ${{ steps.release.outputs.new_version }}
243
+ PR_URL: ${{ steps.pr.outputs.url }}
244
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
245
+ run: |
246
+ set -euo pipefail
247
+ if gh pr merge "$PR_URL" --auto --squash --subject "chore(release): v${NEW_VERSION}" 2>/tmp/auto-merge.err; then
248
+ echo "Auto-merge enabled on $PR_URL; v${NEW_VERSION} is tagged once it merges."
249
+ exit 0
250
+ fi
251
+ cat /tmp/auto-merge.err >&2
252
+ allowed=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.allow_auto_merge' 2>/dev/null || echo "unknown")
253
+ if [ "$allowed" = "false" ]; then
254
+ echo "::warning::Auto-merge is disabled on $GITHUB_REPOSITORY, so the release PR $PR_URL stays open. Merge it by hand (squash), or enable it once: gh api -X PATCH repos/$GITHUB_REPOSITORY -F allow_auto_merge=true"
255
+ else
256
+ echo "::warning::Could not enable auto-merge on $PR_URL; it stays open. Merge it by hand (squash) to tag v${NEW_VERSION}."
257
+ fi
202
258
 
203
259
  incomplete:
204
260
  needs: [agent, safe_outputs]
@@ -241,4 +297,4 @@ jobs:
241
297
  - Keep it concise and factual. Do not invent changes that are not in the log.
242
298
 
243
299
  5. Call `noop` and stop. The deterministic `conclude` job reads the notes you wrote
244
- and handles version bumping, tagging, and release creation.
300
+ and handles the version bump and the release pull request.