@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.
- package/dist/catalog-installation.js +17 -7
- package/dist/route-processing.js +1 -1
- package/dist/workflow-catalog.d.ts +4 -1
- package/dist/workflow-catalog.js +10 -1
- package/loops/actions/capture-evidence/action.yml +117 -0
- package/loops/actions/capture-evidence/capture.mjs +617 -0
- package/loops/actions/classify-route/action.yml +1 -0
- package/loops/actions/classify-route/classify-route.sh +15 -3
- package/loops/actions/report-workflow-errors/action.yml +1 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +78 -61
- package/loops/docs/diagrams.md +45 -44
- package/loops/workflows/agent-merge-gate.md +0 -45
- package/loops/workflows/agent-release.md +73 -17
- package/loops/workflows/evidence.yml +352 -0
- package/loops/workflows/release-tag.yml +98 -0
- package/loops/workflows/shared/opencode-ci.md +5 -6
- package/loops/workflows/work-router.yml +29 -14
- package/package.json +1 -1
- package/loops/actions/attach-screenshots/action.yml +0 -93
- package/loops/workflows/agent-visual-verify.md +0 -350
|
@@ -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 "──
|
|
1686
|
+
echo "── Evidence wiring ───────────────────────────────────────────────────────"
|
|
1671
1687
|
|
|
1672
|
-
#
|
|
1673
|
-
#
|
|
1674
|
-
#
|
|
1675
|
-
#
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}'
|
|
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
|
package/loops/docs/diagrams.md
CHANGED
|
@@ -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;
|
|
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"] -->
|
|
424
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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,
|
|
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
|
-
##
|
|
625
|
+
## evidence.yml
|
|
617
626
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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:#
|
|
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
|
|
656
|
-
class
|
|
657
|
-
class
|
|
658
|
-
class
|
|
659
|
-
class
|
|
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
|
|
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
|
-
|
|
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:
|
|
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: ${{
|
|
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
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
echo "
|
|
191
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
--
|
|
200
|
-
--
|
|
201
|
-
|
|
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
|
|
300
|
+
and handles the version bump and the release pull request.
|