@plainconceptsplatform/workflows 1.0.13 → 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-implement.md +0 -66
- 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
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/evidence.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: "Agentics: Evidence"
|
|
3
|
+
|
|
4
|
+
# Archives what a merged pull request looks like. The agent that implemented the change wrote a
|
|
5
|
+
# verification plan with the harness skill pc-repo-verify; this replays that plan against the
|
|
6
|
+
# app, with no model, and keeps one screenshot per waypoint on the pull request. The router calls
|
|
7
|
+
# it when a pull request merges, and operation=evidence runs it by hand for any pull request.
|
|
8
|
+
# Nothing waits on it and nothing reads it: it is the record, not a gate.
|
|
9
|
+
#
|
|
10
|
+
# Two jobs, because one runs the pull request's code and the other writes. `capture` checks out
|
|
11
|
+
# the head, starts the app and drives Chromium with a read-only token and no secret. `publish`
|
|
12
|
+
# checks nothing out: it takes the images as an artifact, commits them to the orphan `evidence`
|
|
13
|
+
# branch and writes one comment. Both use GITHUB_TOKEN, so a repository configures nothing, and
|
|
14
|
+
# nothing either job writes raises another workflow event.
|
|
15
|
+
#
|
|
16
|
+
# This replaced agent-visual-verify, which never produced a screenshot: it ran only on pull
|
|
17
|
+
# requests nobody would review, raced the merge it was dispatched before, re-planned with a model
|
|
18
|
+
# what implement had already planned, and published through `gh --attach`, which refuses the
|
|
19
|
+
# installation tokens a workflow has.
|
|
20
|
+
|
|
21
|
+
on:
|
|
22
|
+
workflow_call:
|
|
23
|
+
inputs:
|
|
24
|
+
pr-number:
|
|
25
|
+
description: Pull request whose verification plans are replayed.
|
|
26
|
+
required: true
|
|
27
|
+
type: string
|
|
28
|
+
|
|
29
|
+
env:
|
|
30
|
+
# The command that brings the whole stack up with mock auth. Every Platform repository has it at
|
|
31
|
+
# the root; a plan that needs something else names it in its own `start:`.
|
|
32
|
+
EVIDENCE_START_COMMAND: "pnpm run dev"
|
|
33
|
+
# How long the app may take to answer before the run records that it never did. A cold .NET
|
|
34
|
+
# build plus SQL Server in a container is most of it.
|
|
35
|
+
EVIDENCE_START_TIMEOUT_SECONDS: "900"
|
|
36
|
+
# The SDK when a repository has .NET projects but no global.json to say which.
|
|
37
|
+
EVIDENCE_DOTNET_VERSION: "10.0.x"
|
|
38
|
+
EVIDENCE_BRANCH: evidence
|
|
39
|
+
EVIDENCE_MARKER: "<!-- agentic-evidence -->"
|
|
40
|
+
# The capture runs in mcr.microsoft.com/playwright at this tag, pre-pulled by the runner image
|
|
41
|
+
# (runners/versions.env), so the two move together.
|
|
42
|
+
PLAYWRIGHT_VERSION: "1.62.1"
|
|
43
|
+
|
|
44
|
+
jobs:
|
|
45
|
+
capture:
|
|
46
|
+
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
47
|
+
timeout-minutes: 45
|
|
48
|
+
permissions:
|
|
49
|
+
contents: read
|
|
50
|
+
pull-requests: read
|
|
51
|
+
outputs:
|
|
52
|
+
has-evidence: ${{ steps.done.outputs.has-evidence }}
|
|
53
|
+
steps:
|
|
54
|
+
- name: Checkout workflow actions
|
|
55
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
56
|
+
with:
|
|
57
|
+
persist-credentials: false
|
|
58
|
+
|
|
59
|
+
- name: Find the pull request's verification plans
|
|
60
|
+
id: plans
|
|
61
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
62
|
+
env:
|
|
63
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
64
|
+
with:
|
|
65
|
+
script: |
|
|
66
|
+
const pull_number = Number(process.env.PR_NUMBER);
|
|
67
|
+
const { data: pr } = await github.rest.pulls.get({ ...context.repo, pull_number });
|
|
68
|
+
const files = await github.paginate(github.rest.pulls.listFiles, { ...context.repo, pull_number, per_page: 100 });
|
|
69
|
+
const plans = files
|
|
70
|
+
.filter((file) => file.status !== 'removed' && /(^|\/)verification-plan\.md$/.test(file.filename))
|
|
71
|
+
.map((file) => file.filename);
|
|
72
|
+
core.setOutput('sha', pr.head.sha);
|
|
73
|
+
core.setOutput('plans', plans.join('\n'));
|
|
74
|
+
core.setOutput('found', plans.length > 0 ? 'true' : 'false');
|
|
75
|
+
core.info(plans.length > 0
|
|
76
|
+
? `Verification plans in #${pull_number}: ${plans.join(', ')}`
|
|
77
|
+
: `#${pull_number} carries no verification plan; there is nothing to capture.`);
|
|
78
|
+
|
|
79
|
+
- name: Check out the pull request head
|
|
80
|
+
if: steps.plans.outputs.found == 'true'
|
|
81
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
82
|
+
with:
|
|
83
|
+
ref: ${{ steps.plans.outputs.sha }}
|
|
84
|
+
path: pr
|
|
85
|
+
persist-credentials: false
|
|
86
|
+
|
|
87
|
+
# A plan that only says "no UI surface", or that is prose with no journey, starts nothing and
|
|
88
|
+
# writes nothing: a comment on every backend pull request saying so would be noise.
|
|
89
|
+
- name: Read the plans
|
|
90
|
+
id: inspect
|
|
91
|
+
if: steps.plans.outputs.found == 'true'
|
|
92
|
+
uses: ./.github/actions/capture-evidence
|
|
93
|
+
with:
|
|
94
|
+
mode: inspect
|
|
95
|
+
root: pr
|
|
96
|
+
plans: ${{ steps.plans.outputs.plans }}
|
|
97
|
+
playwright-version: ${{ env.PLAYWRIGHT_VERSION }}
|
|
98
|
+
|
|
99
|
+
- name: Detect the toolchain the app needs
|
|
100
|
+
id: toolchain
|
|
101
|
+
if: steps.inspect.outputs.runnable == 'true'
|
|
102
|
+
working-directory: pr
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
global_json="$(find . -maxdepth 3 -name global.json -not -path '*/node_modules/*' | head -n 1)"
|
|
106
|
+
dotnet_project="$(find . -maxdepth 3 \( -name '*.slnx' -o -name '*.sln' -o -name '*.csproj' \) -not -path '*/node_modules/*' | head -n 1)"
|
|
107
|
+
if [ -n "$global_json" ] || [ -n "$dotnet_project" ]; then
|
|
108
|
+
echo "dotnet=true" >> "$GITHUB_OUTPUT"
|
|
109
|
+
else
|
|
110
|
+
echo "dotnet=false" >> "$GITHUB_OUTPUT"
|
|
111
|
+
fi
|
|
112
|
+
echo "global-json=${global_json#./}" >> "$GITHUB_OUTPUT"
|
|
113
|
+
|
|
114
|
+
- name: Set up .NET
|
|
115
|
+
if: steps.toolchain.outputs.dotnet == 'true'
|
|
116
|
+
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
|
117
|
+
with:
|
|
118
|
+
global-json-file: ${{ steps.toolchain.outputs.global-json != '' && format('pr/{0}', steps.toolchain.outputs.global-json) || '' }}
|
|
119
|
+
dotnet-version: ${{ steps.toolchain.outputs.global-json == '' && env.EVIDENCE_DOTNET_VERSION || '' }}
|
|
120
|
+
env:
|
|
121
|
+
# the default /usr/share/dotnet needs root; the tool cache belongs to the runner user
|
|
122
|
+
DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
|
|
123
|
+
|
|
124
|
+
- name: Install the app's dependencies
|
|
125
|
+
if: steps.inspect.outputs.runnable == 'true'
|
|
126
|
+
working-directory: pr
|
|
127
|
+
run: |
|
|
128
|
+
set -euo pipefail
|
|
129
|
+
# Local development reads .env; the example is what a new clone is told to copy.
|
|
130
|
+
if [ ! -f .env ] && [ -f .env.example ]; then cp .env.example .env; fi
|
|
131
|
+
if [ -f pnpm-lock.yaml ]; then
|
|
132
|
+
corepack enable
|
|
133
|
+
corepack prepare --activate
|
|
134
|
+
pnpm install --frozen-lockfile
|
|
135
|
+
elif [ -f package-lock.json ]; then
|
|
136
|
+
npm ci
|
|
137
|
+
elif [ -f package.json ]; then
|
|
138
|
+
npm install --no-audit --no-fund
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
- name: Start the app
|
|
142
|
+
id: app
|
|
143
|
+
if: steps.inspect.outputs.runnable == 'true'
|
|
144
|
+
working-directory: pr
|
|
145
|
+
env:
|
|
146
|
+
START: ${{ steps.inspect.outputs.start || env.EVIDENCE_START_COMMAND }}
|
|
147
|
+
APP_URL: ${{ steps.inspect.outputs.url }}
|
|
148
|
+
TIMEOUT: ${{ env.EVIDENCE_START_TIMEOUT_SECONDS }}
|
|
149
|
+
run: |
|
|
150
|
+
set -euo pipefail
|
|
151
|
+
log="$RUNNER_TEMP/evidence-app.log"
|
|
152
|
+
# Its own session, so the whole tree it spawns stops with one signal at the end.
|
|
153
|
+
setsid bash -c "$START" > "$log" 2>&1 < /dev/null &
|
|
154
|
+
pid=$!
|
|
155
|
+
{
|
|
156
|
+
echo "pid=$pid"
|
|
157
|
+
echo "start=$START"
|
|
158
|
+
} >> "$GITHUB_OUTPUT"
|
|
159
|
+
|
|
160
|
+
waited=0
|
|
161
|
+
up=false
|
|
162
|
+
# Any HTTP answer counts, a 500 from a dev server still compiling included: the first
|
|
163
|
+
# waypoint's navigation waits for the page itself.
|
|
164
|
+
while [ "$waited" -lt "$TIMEOUT" ]; do
|
|
165
|
+
if curl -s -o /dev/null --max-time 5 "$APP_URL"; then
|
|
166
|
+
up=true
|
|
167
|
+
break
|
|
168
|
+
fi
|
|
169
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
170
|
+
echo "::warning::'$START' exited before the app answered at $APP_URL"
|
|
171
|
+
break
|
|
172
|
+
fi
|
|
173
|
+
sleep 5
|
|
174
|
+
waited=$((waited + 5))
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
echo "up=$up"
|
|
179
|
+
echo "waited=$waited"
|
|
180
|
+
} >> "$GITHUB_OUTPUT"
|
|
181
|
+
if [ "$up" = "true" ]; then
|
|
182
|
+
echo "The app answered at $APP_URL after ${waited}s."
|
|
183
|
+
else
|
|
184
|
+
echo "::warning::the app did not answer at $APP_URL after ${waited}s of '$START'; recording that instead of screenshots"
|
|
185
|
+
echo "::group::Last lines of the app's output"
|
|
186
|
+
tail -n 80 "$log" || true
|
|
187
|
+
echo "::endgroup::"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
- name: Replay the plans
|
|
191
|
+
if: steps.app.outputs.up == 'true'
|
|
192
|
+
uses: ./.github/actions/capture-evidence
|
|
193
|
+
env:
|
|
194
|
+
EVIDENCE_SHA: ${{ steps.plans.outputs.sha }}
|
|
195
|
+
EVIDENCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
196
|
+
with:
|
|
197
|
+
mode: capture
|
|
198
|
+
root: pr
|
|
199
|
+
plans: ${{ steps.plans.outputs.plans }}
|
|
200
|
+
out-dir: ${{ runner.temp }}/evidence
|
|
201
|
+
playwright-version: ${{ env.PLAYWRIGHT_VERSION }}
|
|
202
|
+
|
|
203
|
+
- name: Record that the app never answered
|
|
204
|
+
if: steps.app.outputs.up == 'false'
|
|
205
|
+
uses: ./.github/actions/capture-evidence
|
|
206
|
+
env:
|
|
207
|
+
EVIDENCE_SHA: ${{ steps.plans.outputs.sha }}
|
|
208
|
+
EVIDENCE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
209
|
+
EVIDENCE_APP_URL: ${{ steps.inspect.outputs.url }}
|
|
210
|
+
EVIDENCE_START: ${{ steps.app.outputs.start }}
|
|
211
|
+
EVIDENCE_WAITED_SECONDS: ${{ steps.app.outputs.waited }}
|
|
212
|
+
with:
|
|
213
|
+
mode: unreached
|
|
214
|
+
root: pr
|
|
215
|
+
plans: ${{ steps.plans.outputs.plans }}
|
|
216
|
+
out-dir: ${{ runner.temp }}/evidence
|
|
217
|
+
|
|
218
|
+
- name: Stop the app
|
|
219
|
+
if: always() && steps.app.outputs.pid != ''
|
|
220
|
+
env:
|
|
221
|
+
PID: ${{ steps.app.outputs.pid }}
|
|
222
|
+
run: |
|
|
223
|
+
kill -TERM -- "-$PID" 2>/dev/null || true
|
|
224
|
+
sleep 3
|
|
225
|
+
kill -KILL -- "-$PID" 2>/dev/null || true
|
|
226
|
+
|
|
227
|
+
- name: Hand the evidence to the publish job
|
|
228
|
+
id: done
|
|
229
|
+
if: always() && steps.plans.outputs.found == 'true'
|
|
230
|
+
run: |
|
|
231
|
+
if [ -f "$RUNNER_TEMP/evidence/comment.md" ]; then
|
|
232
|
+
echo "has-evidence=true" >> "$GITHUB_OUTPUT"
|
|
233
|
+
else
|
|
234
|
+
echo "has-evidence=false" >> "$GITHUB_OUTPUT"
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
- name: Upload the evidence
|
|
238
|
+
if: always() && steps.done.outputs.has-evidence == 'true'
|
|
239
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
240
|
+
with:
|
|
241
|
+
name: evidence-pr-${{ inputs.pr-number }}
|
|
242
|
+
path: ${{ runner.temp }}/evidence
|
|
243
|
+
retention-days: 3
|
|
244
|
+
if-no-files-found: error
|
|
245
|
+
|
|
246
|
+
publish:
|
|
247
|
+
needs: capture
|
|
248
|
+
if: always() && needs.capture.outputs.has-evidence == 'true'
|
|
249
|
+
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
250
|
+
timeout-minutes: 10
|
|
251
|
+
permissions:
|
|
252
|
+
contents: write
|
|
253
|
+
pull-requests: write
|
|
254
|
+
issues: write
|
|
255
|
+
steps:
|
|
256
|
+
- name: Download the evidence
|
|
257
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
258
|
+
with:
|
|
259
|
+
name: evidence-pr-${{ inputs.pr-number }}
|
|
260
|
+
path: ${{ runner.temp }}/evidence
|
|
261
|
+
|
|
262
|
+
# An orphan branch rather than the default branch: the images stay out of every build, every
|
|
263
|
+
# diff and every review, and a clone that only wants the code can leave the branch behind.
|
|
264
|
+
# One folder per pull request, so two publishes never touch the same file and a lost push
|
|
265
|
+
# race is a retry, never a conflict.
|
|
266
|
+
- name: Commit the screenshots to the evidence branch
|
|
267
|
+
id: branch
|
|
268
|
+
env:
|
|
269
|
+
TOKEN: ${{ github.token }}
|
|
270
|
+
REPO: ${{ github.repository }}
|
|
271
|
+
SERVER: ${{ github.server_url }}
|
|
272
|
+
BRANCH: ${{ env.EVIDENCE_BRANCH }}
|
|
273
|
+
PR: ${{ inputs.pr-number }}
|
|
274
|
+
run: |
|
|
275
|
+
set -euo pipefail
|
|
276
|
+
src="$RUNNER_TEMP/evidence"
|
|
277
|
+
if ! compgen -G "$src/*.jpg" > /dev/null; then
|
|
278
|
+
echo "No screenshots to keep; the comment records why."
|
|
279
|
+
echo "base=" >> "$GITHUB_OUTPUT"
|
|
280
|
+
exit 0
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
work="$RUNNER_TEMP/evidence-branch"
|
|
284
|
+
rm -rf "$work"
|
|
285
|
+
mkdir -p "$work"
|
|
286
|
+
cd "$work"
|
|
287
|
+
git init -q
|
|
288
|
+
git config user.name "github-actions[bot]"
|
|
289
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
290
|
+
git config "http.${SERVER}/.extraheader" "AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 -w0)"
|
|
291
|
+
git remote add origin "${SERVER}/${REPO}.git"
|
|
292
|
+
|
|
293
|
+
pushed=false
|
|
294
|
+
for attempt in 1 2 3 4 5; do
|
|
295
|
+
if git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; then
|
|
296
|
+
git fetch -q --depth=1 origin "$BRANCH"
|
|
297
|
+
git checkout -q -f -B "$BRANCH" FETCH_HEAD
|
|
298
|
+
else
|
|
299
|
+
git checkout -q -f --orphan "$BRANCH"
|
|
300
|
+
git rm -rqf --cached . > /dev/null 2>&1 || true
|
|
301
|
+
printf '%s\n' "# Evidence" "" \
|
|
302
|
+
"Screenshots the evidence workflow replayed from each merged pull request's verification plan, one folder per pull request." \
|
|
303
|
+
"Written by a workflow, never by hand. Nothing here is built, deployed or merged." > README.md
|
|
304
|
+
git add README.md
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
rm -rf "pr-$PR"
|
|
308
|
+
mkdir -p "pr-$PR"
|
|
309
|
+
cp "$src"/*.jpg "$src/manifest.json" "pr-$PR/"
|
|
310
|
+
git add -A "pr-$PR"
|
|
311
|
+
if git diff --cached --quiet; then
|
|
312
|
+
pushed=true
|
|
313
|
+
break
|
|
314
|
+
fi
|
|
315
|
+
git commit -q -m "evidence: pull request #$PR"
|
|
316
|
+
if git push -q origin "HEAD:refs/heads/$BRANCH"; then
|
|
317
|
+
pushed=true
|
|
318
|
+
break
|
|
319
|
+
fi
|
|
320
|
+
echo "Another publish moved $BRANCH first (attempt $attempt); retrying on its new tip."
|
|
321
|
+
sleep $((attempt * 3))
|
|
322
|
+
done
|
|
323
|
+
[ "$pushed" = "true" ] || { echo "::error::could not push to $BRANCH after 5 attempts"; exit 1; }
|
|
324
|
+
|
|
325
|
+
sha="$(git rev-parse HEAD)"
|
|
326
|
+
echo "base=${SERVER}/${REPO}/raw/${sha}/pr-${PR}" >> "$GITHUB_OUTPUT"
|
|
327
|
+
echo "Screenshots kept at ${SERVER}/${REPO}/tree/${sha}/pr-${PR}"
|
|
328
|
+
|
|
329
|
+
# GITHUB_TOKEN on purpose: its comment raises no workflow event, so the router does not wake
|
|
330
|
+
# up to read its own record. One comment per pull request, rewritten on a re-run.
|
|
331
|
+
- name: Write the evidence comment
|
|
332
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
333
|
+
env:
|
|
334
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
335
|
+
MARKER: ${{ env.EVIDENCE_MARKER }}
|
|
336
|
+
IMAGE_BASE: ${{ steps.branch.outputs.base }}
|
|
337
|
+
with:
|
|
338
|
+
script: |
|
|
339
|
+
const fs = require('node:fs');
|
|
340
|
+
const path = require('node:path');
|
|
341
|
+
const issue_number = Number(process.env.PR_NUMBER);
|
|
342
|
+
const source = fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'evidence', 'comment.md'), 'utf8');
|
|
343
|
+
const body = source.split('__EVIDENCE_BASE__').join(process.env.IMAGE_BASE);
|
|
344
|
+
const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number, per_page: 100 });
|
|
345
|
+
const existing = comments.find((comment) => comment.user?.type === 'Bot' && (comment.body ?? '').includes(process.env.MARKER));
|
|
346
|
+
if (existing) {
|
|
347
|
+
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body });
|
|
348
|
+
core.info(`Updated ${existing.html_url}`);
|
|
349
|
+
} else {
|
|
350
|
+
const { data } = await github.rest.issues.createComment({ ...context.repo, issue_number, body });
|
|
351
|
+
core.info(`Created ${data.html_url}`);
|
|
352
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/release-tag.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Release Tag
|
|
3
|
+
|
|
4
|
+
# The second half of the release route. agent-release.md opens a `chore(release): vX.Y.Z` pull
|
|
5
|
+
# request instead of pushing to `main`, so `main` can require pull requests. Once that PR is
|
|
6
|
+
# squash-merged, this tags the merge commit and publishes the GitHub Release with the notes the
|
|
7
|
+
# agent wrote, which the PR body carries. No model is involved.
|
|
8
|
+
#
|
|
9
|
+
# The tag and the Release are created with GITHUB_TOKEN, never the App token, and that is
|
|
10
|
+
# load-bearing. GITHUB_TOKEN writes start no workflows, so a consumer whose deploy triggers on
|
|
11
|
+
# `v*` tags (Numa's app-deploy.yml deploys to production on one) does not ship to production
|
|
12
|
+
# because a release was cut. That is exactly how the release behaved when it pushed the tag
|
|
13
|
+
# itself; moving the bump into a pull request must not change it.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
push:
|
|
17
|
+
branches: [main]
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: write
|
|
21
|
+
pull-requests: read
|
|
22
|
+
|
|
23
|
+
concurrency:
|
|
24
|
+
group: release-tag
|
|
25
|
+
cancel-in-progress: false
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
tag:
|
|
29
|
+
name: Tag and publish the release
|
|
30
|
+
if: >-
|
|
31
|
+
startsWith(github.event.head_commit.message, 'chore(release): v')
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 10
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
36
|
+
with:
|
|
37
|
+
persist-credentials: false
|
|
38
|
+
- name: Tag the release commit and publish the GitHub Release
|
|
39
|
+
env:
|
|
40
|
+
GH_TOKEN: ${{ github.token }}
|
|
41
|
+
REPO: ${{ github.repository }}
|
|
42
|
+
SHA: ${{ github.sha }}
|
|
43
|
+
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
|
|
44
|
+
shell: bash
|
|
45
|
+
run: |
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
# Squash keeps the PR title as the subject and appends ` (#123)`.
|
|
49
|
+
subject="${HEAD_MESSAGE%%$'\n'*}"
|
|
50
|
+
if ! [[ "$subject" =~ ^chore\(release\):\ v([0-9]+\.[0-9]+\.[0-9]+)([[:space:]]|$) ]]; then
|
|
51
|
+
echo "Head commit is not a release commit: $subject"
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
version="${BASH_REMATCH[1]}"
|
|
55
|
+
tag="v${version}"
|
|
56
|
+
|
|
57
|
+
if [ -f package.json ]; then
|
|
58
|
+
manifest=$(node -p "require('./package.json').version")
|
|
59
|
+
if [ "$manifest" != "$version" ]; then
|
|
60
|
+
echo "::error::The commit says $tag but package.json is at $manifest; not tagging."
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Idempotent: a re-run, or a second push of the same commit, does nothing.
|
|
66
|
+
if gh release view "$tag" --repo "$REPO" >/dev/null 2>&1; then
|
|
67
|
+
echo "Release $tag already exists; nothing to do."
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
if existing=$(gh api "repos/$REPO/git/ref/tags/$tag" --jq '.object.sha' 2>/dev/null); then
|
|
71
|
+
if [ "$existing" != "$SHA" ]; then
|
|
72
|
+
echo "::warning::Tag $tag already exists on $existing, not on $SHA; leaving it alone."
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# The marker tells the release PR apart from any other PR associated with this commit.
|
|
78
|
+
marker="<!-- release:${tag} -->"
|
|
79
|
+
gh api "repos/$REPO/commits/$SHA/pulls" > /tmp/pulls.json
|
|
80
|
+
MARKER="$marker" node -e '
|
|
81
|
+
const pulls = JSON.parse(require("fs").readFileSync("/tmp/pulls.json", "utf8"));
|
|
82
|
+
const pr = pulls.find((p) => p.merged_at && (p.body || "").includes(process.env.MARKER));
|
|
83
|
+
if (!pr) process.exit(3);
|
|
84
|
+
const notes = pr.body.split(/\r?\n/).filter((line) => line.trim() !== process.env.MARKER).join("\n").trim();
|
|
85
|
+
require("fs").writeFileSync("/tmp/release-notes.md", notes + "\n");
|
|
86
|
+
console.log(`Notes from #${pr.number}`);
|
|
87
|
+
' || status=$?
|
|
88
|
+
|
|
89
|
+
if [ "${status:-0}" -eq 3 ]; then
|
|
90
|
+
echo "::warning::No merged pull request carrying $marker is associated with $SHA; publishing generated notes instead."
|
|
91
|
+
gh release create "$tag" --repo "$REPO" --target "$SHA" --title "$tag" --generate-notes
|
|
92
|
+
elif [ "${status:-0}" -ne 0 ]; then
|
|
93
|
+
echo "::error::Could not read the release notes from the pull request."
|
|
94
|
+
exit 1
|
|
95
|
+
else
|
|
96
|
+
gh release create "$tag" --repo "$REPO" --target "$SHA" --title "$tag" --notes-file /tmp/release-notes.md
|
|
97
|
+
fi
|
|
98
|
+
echo "Tagged $SHA as $tag and published the GitHub Release."
|
|
@@ -139,12 +139,11 @@ pre-agent-steps:
|
|
|
139
139
|
npm install -g "@fission-ai/openspec@1.8.0"
|
|
140
140
|
openspec --version
|
|
141
141
|
|
|
142
|
-
# NOTE:
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
# add them as consumer-specific steps after the shared baseline.
|
|
142
|
+
# NOTE: no browser and no SQL Server in the shared CI baseline. An agent cannot reach a
|
|
143
|
+
# browser from inside the awf sandbox, so it only writes the verification plan
|
|
144
|
+
# (/repo-verify); evidence.yml replays that plan outside the sandbox once the pull request
|
|
145
|
+
# merges and archives the screenshots on it. If you need SQL Server inside the agent sandbox
|
|
146
|
+
# for other reasons, add it as a consumer-specific step after the shared baseline.
|
|
148
147
|
|
|
149
148
|
- name: Setup .NET
|
|
150
149
|
# Ephemeral VMs carry no SDK; the old host had it baked in, and GitHub-hosted images made
|
|
@@ -67,8 +67,8 @@ run-name: >-
|
|
|
67
67
|
&& inputs.operation == 'merge-gate'
|
|
68
68
|
&& format('Agent: Merge Gate - PR #{0}', inputs.pr-number)
|
|
69
69
|
|| github.event_name == 'workflow_dispatch'
|
|
70
|
-
&& inputs.operation == '
|
|
71
|
-
&& format('
|
|
70
|
+
&& inputs.operation == 'evidence'
|
|
71
|
+
&& format('Agentics: Evidence - PR #{0}', inputs.pr-number)
|
|
72
72
|
|| github.event_name == 'workflow_dispatch'
|
|
73
73
|
&& inputs.operation == 'batch'
|
|
74
74
|
&& format('Working: Batch - #{0}', inputs.issue-number)
|
|
@@ -144,6 +144,11 @@ env:
|
|
|
144
144
|
# merge-gate worker carried its own MAX_ATTEMPTS; the route matrix now asserts the two
|
|
145
145
|
# agree, because a belt that gives up later than the worker says burns whole runs.
|
|
146
146
|
MAX_GATE_ATTEMPTS: "6"
|
|
147
|
+
# Whether a merged pull request's verification plan is replayed and its screenshots archived on
|
|
148
|
+
# the pull request (evidence.yml). It needs nothing configured: the app starts with the root
|
|
149
|
+
# `pnpm run dev`, and a pull request with no plan costs one API call. Set it to "false" for a
|
|
150
|
+
# repository whose app cannot start on a runner; operation=evidence still works by hand.
|
|
151
|
+
EVIDENCE_ENABLED: "true"
|
|
147
152
|
|
|
148
153
|
on:
|
|
149
154
|
issues:
|
|
@@ -158,7 +163,8 @@ on:
|
|
|
158
163
|
pull_request_target:
|
|
159
164
|
# opened / reopened / synchronize feed conflict detection (detect-pr-conflicts) and
|
|
160
165
|
# bot-approve. closed feeds detect-pr-conflicts: when a PR merges, the target branch
|
|
161
|
-
# moved and previously-mergeable sibling bot PRs may now conflict.
|
|
166
|
+
# moved and previously-mergeable sibling bot PRs may now conflict. A merge also routes to
|
|
167
|
+
# evidence, which replays the pull request's verification plan.
|
|
162
168
|
types: [opened, synchronize, reopened, labeled, closed]
|
|
163
169
|
|
|
164
170
|
# CI completion on a pull request. The classifier admits only conclusion == failure with an
|
|
@@ -199,7 +205,7 @@ on:
|
|
|
199
205
|
- triage
|
|
200
206
|
- apply-review
|
|
201
207
|
- merge-gate
|
|
202
|
-
-
|
|
208
|
+
- evidence
|
|
203
209
|
- audit
|
|
204
210
|
- audit-close
|
|
205
211
|
- cleanup-artifacts
|
|
@@ -212,7 +218,7 @@ on:
|
|
|
212
218
|
required: false
|
|
213
219
|
type: string
|
|
214
220
|
pr-number:
|
|
215
|
-
description: "Pull request number (required for apply-review / merge-gate)"
|
|
221
|
+
description: "Pull request number (required for apply-review / merge-gate / evidence)"
|
|
216
222
|
required: false
|
|
217
223
|
type: string
|
|
218
224
|
mode:
|
|
@@ -623,18 +629,24 @@ jobs:
|
|
|
623
629
|
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
624
630
|
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
625
631
|
|
|
626
|
-
|
|
632
|
+
# Screenshots of a merged pull request, replayed from the verification plan its implementation
|
|
633
|
+
# wrote and archived on the pull request. Plain YAML with no model, so no secret is passed and the
|
|
634
|
+
# grant is exactly what the publish job writes: the evidence branch and one comment. Its capture
|
|
635
|
+
# job runs the pull request's code with read-only access; only publish holds these writes, and it
|
|
636
|
+
# runs no repository code.
|
|
637
|
+
call-evidence:
|
|
627
638
|
needs: classify
|
|
628
|
-
if: needs.classify.outputs.route == '
|
|
629
|
-
uses: ./.github/workflows/
|
|
630
|
-
|
|
639
|
+
if: needs.classify.outputs.route == 'evidence'
|
|
640
|
+
uses: ./.github/workflows/evidence.yml
|
|
641
|
+
concurrency:
|
|
642
|
+
group: evidence-${{ needs.classify.outputs.pr-number }}
|
|
643
|
+
cancel-in-progress: false
|
|
644
|
+
permissions:
|
|
645
|
+
contents: write
|
|
646
|
+
pull-requests: write
|
|
647
|
+
issues: write
|
|
631
648
|
with:
|
|
632
649
|
pr-number: ${{ needs.classify.outputs.pr-number }}
|
|
633
|
-
linked-issue: ${{ needs.classify.outputs.linked-issue }}
|
|
634
|
-
secrets:
|
|
635
|
-
OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
636
|
-
CODEX_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
637
|
-
COPILOT_GITHUB_TOKEN: ${{ secrets.FORGE_API_KEY }}
|
|
638
650
|
|
|
639
651
|
call-audit:
|
|
640
652
|
needs: classify
|
|
@@ -663,6 +675,9 @@ jobs:
|
|
|
663
675
|
version-bump: ${{ github.event.inputs.version-bump || 'auto' }}
|
|
664
676
|
secrets:
|
|
665
677
|
OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
678
|
+
# The release PR is pushed and opened with the App token so its CI runs; see conclude.
|
|
679
|
+
BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
|
|
680
|
+
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
666
681
|
|
|
667
682
|
bot-approve:
|
|
668
683
|
needs: classify
|
package/package.json
CHANGED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/attach-screenshots/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
name: Attach screenshots
|
|
3
|
-
description: Post screenshots captured by agent-browser to the linked issue as a single comment.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token with issues:write access.
|
|
7
|
-
required: true
|
|
8
|
-
issue-number:
|
|
9
|
-
description: Issue number to comment on.
|
|
10
|
-
required: true
|
|
11
|
-
pr-number:
|
|
12
|
-
description: Pull request number being verified.
|
|
13
|
-
required: true
|
|
14
|
-
screenshots-dir:
|
|
15
|
-
description: Directory containing screenshots and plan.json.
|
|
16
|
-
required: true
|
|
17
|
-
screenshot-count:
|
|
18
|
-
description: Number of screenshots captured (0 if none).
|
|
19
|
-
required: false
|
|
20
|
-
default: '0'
|
|
21
|
-
app-started:
|
|
22
|
-
description: Whether the app started successfully.
|
|
23
|
-
required: false
|
|
24
|
-
default: 'false'
|
|
25
|
-
enabled:
|
|
26
|
-
description: Whether visual verification was enabled.
|
|
27
|
-
required: false
|
|
28
|
-
default: 'true'
|
|
29
|
-
runs:
|
|
30
|
-
using: composite
|
|
31
|
-
steps:
|
|
32
|
-
- name: Attach screenshots to issue
|
|
33
|
-
shell: bash
|
|
34
|
-
env:
|
|
35
|
-
GH_TOKEN: ${{ inputs.token }}
|
|
36
|
-
REPO: ${{ github.repository }}
|
|
37
|
-
ISSUE: ${{ inputs.issue-number }}
|
|
38
|
-
PR: ${{ inputs.pr-number }}
|
|
39
|
-
SCREENSHOTS_DIR: ${{ inputs.screenshots-dir }}
|
|
40
|
-
SCREENSHOT_COUNT: ${{ inputs.screenshot-count }}
|
|
41
|
-
APP_STARTED: ${{ inputs.app-started }}
|
|
42
|
-
ENABLED: ${{ inputs.enabled }}
|
|
43
|
-
run: |
|
|
44
|
-
set -euo pipefail
|
|
45
|
-
|
|
46
|
-
if [ "$ENABLED" != "true" ]; then
|
|
47
|
-
echo "Visual verification disabled; nothing to attach."
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
if [ -f "$SCREENSHOTS_DIR/plan.json" ] && [ "$APP_STARTED" = "true" ] && [ "$SCREENSHOT_COUNT" -gt 0 ]; then
|
|
52
|
-
# Build the comment body from the plan's descriptions.
|
|
53
|
-
body=$(jq -r --arg pr "$PR" '
|
|
54
|
-
"<!-- agent-visual-verify -->\n## Visual verification for PR #" + $pr + "\n",
|
|
55
|
-
(.waypoints | to_entries[] |
|
|
56
|
-
"**\(.key + 1). " + (.value.description // .value.expect // "waypoint") + "**\n" +
|
|
57
|
-
"Selector: `" + (.value.selector // "n/a") + "`\n"
|
|
58
|
-
)
|
|
59
|
-
' "$SCREENSHOTS_DIR/plan.json")
|
|
60
|
-
|
|
61
|
-
# Collect all screenshot files.
|
|
62
|
-
attaches=()
|
|
63
|
-
for png in "$SCREENSHOTS_DIR"/*.png; do
|
|
64
|
-
[ -f "$png" ] || continue
|
|
65
|
-
attaches+=("$png")
|
|
66
|
-
done
|
|
67
|
-
|
|
68
|
-
if [ ${#attaches[@]} -gt 0 ]; then
|
|
69
|
-
# --body-file with --attach is not supported by every gh version; --body with a
|
|
70
|
-
# here-string is, and one comment per screenshot loses the narrative. Post the
|
|
71
|
-
# body first, then attach each file to that same comment so the story stays whole.
|
|
72
|
-
comment_url=$(gh issue comment "$ISSUE" --repo "$REPO" --body "$body" --json url --jq '.url')
|
|
73
|
-
for png in "${attaches[@]}"; do
|
|
74
|
-
gh issue comment "$ISSUE" --repo "$REPO" --attach "$png" --body "" 2>/dev/null || true
|
|
75
|
-
done
|
|
76
|
-
echo "Attached ${#attaches[@]} screenshots to issue #$ISSUE ($comment_url)"
|
|
77
|
-
else
|
|
78
|
-
gh issue comment "$ISSUE" --repo "$REPO" --body "$body"
|
|
79
|
-
echo "No screenshots found; posted plan summary only"
|
|
80
|
-
fi
|
|
81
|
-
else
|
|
82
|
-
# App didn't start or no plan was produced. Post a warning.
|
|
83
|
-
reason="visual verification could not produce screenshots"
|
|
84
|
-
if [ "$APP_STARTED" != "true" ]; then
|
|
85
|
-
reason="$reason (app did not start)"
|
|
86
|
-
elif [ ! -f "$SCREENSHOTS_DIR/plan.json" ]; then
|
|
87
|
-
reason="$reason (no verification plan was written)"
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
warning_body=$(printf '<!-- agent-visual-verify -->## Visual verification for PR #%s\n\n%s' "$PR" "$reason. The merge proceeds without visual evidence.")
|
|
91
|
-
gh issue comment "$ISSUE" --repo "$REPO" --body "$warning_body"
|
|
92
|
-
echo "Posted warning: $reason"
|
|
93
|
-
fi
|