Repository navigation
415 lines (406 loc) · 21 KB
/
Copy pathci.yml
File metadata and controls
415 lines (406 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
name: CRW CI
on:
# CRW-966: the repository integrates by one integrator's verified fast-forward to dev, so this
# workflow no longer starts on a push or a pull request. A manual dispatch runs every job as a
# developer's full remote check; it is not integration or release evidence.
workflow_dispatch:
permissions:
contents: read
concurrency:
# A title or body edit that keeps the base joins the pull request's own group, so it waits
# behind a running run instead of cancelling it, and a later push cancels it in turn. A
# retarget and every other event still replace obsolete runs.
group: workflow-skills-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ !(github.event.action == 'edited' && !github.event.changes.base) }}
jobs:
validate:
runs-on: ubuntu-24.04
timeout-minutes: 5
# The mirror reads this repository's runs and jobs with the workflow's own token.
permissions:
contents: read
actions: read
steps:
# A body-only edit needs only the mirror script, and the script has to exist before the
# mirror decides, so it is fetched sparsely first. Every other run skips this step.
- name: Check out scripts/ci for the body-only edit mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
sparse-checkout: scripts/ci
# On a body-only edit, a job this head already ran successfully stands down. Every later
# step of this job carries the answer, so no step is skipped at job level, which GitHub
# would report as a success and a skipped dev-gate could hide an earlier red run with.
- name: Mirror the jobs this head already ran
id: mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
run: bash scripts/ci/edit_mirror.sh
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
JOB_NAME: validate
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
if: steps.mirror.outputs.mirrored != 'true'
with:
# The large-blob check judges every blob a pull request or a push brings into the history.
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
if: steps.mirror.outputs.mirrored != 'true'
with:
go-version-file: go.mod
# The CI checks live in the development binary, which release archives never carry.
- run: go build -tags dev -o "$RUNNER_TEMP/crw-dev" ./cmd/crw-dev
if: steps.mirror.outputs.mirrored != 'true'
# A pull request run judges the commits it adds to its base, a push to dev the commits it adds;
# a run with neither (a manual dispatch) judges every commit.
- run: '"$RUNNER_TEMP/crw-dev" ci validate'
env:
BLOB_RANGE_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
if: steps.mirror.outputs.mirrored != 'true'
- run: '"$RUNNER_TEMP/crw-dev" ci plugin'
if: steps.mirror.outputs.mirrored != 'true'
- run: '"$RUNNER_TEMP/crw-dev" ci contracts'
if: steps.mirror.outputs.mirrored != 'true'
secrets:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
actions: read
steps:
- name: Check out scripts/ci for the body-only edit mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
sparse-checkout: scripts/ci
- name: Mirror the jobs this head already ran
id: mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
run: bash scripts/ci/edit_mirror.sh
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
JOB_NAME: secrets
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
if: steps.mirror.outputs.mirrored != 'true'
with:
fetch-depth: 0
persist-credentials: false
# A pull request run scans only the commits it adds; any other event scans every ref.
- run: bash scripts/ci/secrets.sh
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
if: steps.mirror.outputs.mirrored != 'true'
# The staged skills ship Node helper scripts, and their tests travel with them (G5). The job
# runs only when a staged skill path changed: a change that does not touch them ends the job
# before any runner setup, and a skipped run counts as success. Node is needed here and when an
# agent runs a skill script, never on a hook path or in the crw binary.
skill-scripts-node:
runs-on: ubuntu-24.04
timeout-minutes: 10
# The mirror reads this repository's runs and jobs with the workflow's own token.
permissions:
contents: read
actions: read
env:
# The skills root, named once: the changed-path filter and the test run both read it. The
# ported skills and their tests live in the plugin since the activation move (CRW-392).
SKILLS_ROOT: plugins/crw/skills
steps:
# A body-only edit needs only the mirror script, and the script has to exist before the
# mirror decides, so it is fetched sparsely first. Every other run skips this step.
- name: Check out scripts/ci for the body-only edit mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
sparse-checkout: scripts/ci
# On a body-only edit, a job this head already ran successfully stands down. Every later
# step of this job carries the answer, so no step is skipped at job level, which GitHub
# would report as a success and a skipped dev-gate could hide an earlier red run with.
- name: Mirror the jobs this head already ran
id: mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
run: bash scripts/ci/edit_mirror.sh
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
JOB_NAME: skill-scripts-node
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
if: steps.mirror.outputs.mirrored != 'true'
with:
# The changed-path decision below compares a pull request from its merge base, so the
# history the two sides share has to be here, not only the checked-out merge commit.
fetch-depth: 0
persist-credentials: false
- name: Decide from the changed files whether the staged skills changed
id: paths
if: steps.mirror.outputs.mirrored != 'true'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: |
set -euo pipefail
# A pull request compares from its merge base: the commits the branch adds to its base,
# never the base tip's own changes. A push to dev compares the previous commit with this
# one, where the range is already the pushed commits. A manual dispatch has neither, so
# it runs the tests.
if [[ -n "${PR_BASE_SHA}" ]]; then
range="${PR_BASE_SHA}...${PR_HEAD_SHA}"
elif [[ -n "${PUSH_BEFORE_SHA}" && "${PUSH_BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]]; then
range="${PUSH_BEFORE_SHA}..${GITHUB_SHA}"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo 'no base to compare with; the staged skill-script tests run'
exit 0
fi
changed="$(git diff --name-only "${range}" -- "${SKILLS_ROOT}")"
if [[ -n "${changed}" ]]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
printf 'staged skill paths changed:\n%s\n' "${changed}"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo 'no staged skill path changed; the job ends without installing Node'
fi
- if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
# An exact release, not the floating major: the moved tests are timing- and
# runtime-sensitive, so the same commit must meet the same Node.
node-version: '24.20.0'
- name: Run the staged skill-script tests
if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
run: |
set -euo pipefail
shopt -s nullglob
files=("${SKILLS_ROOT}"/*/tests/*.test.mjs)
if [[ ${#files[@]} -eq 0 ]]; then
echo 'no staged skill-script test file'
exit 0
fi
node --test "${files[@]}"
gui:
# The screens are built into the crw binary (internal/gui/assets, `//go:embed all:assets`), so
# the committed tree must be a fresh build of web/. This job always exists and dev-gate waits
# on it; it runs the full verification (Node 24.20.0, npm ci, the screen tests, a build and the
# byte-for-byte drift check) when a watched path changed, when the changed list could not be
# read, or on a manual dispatch, and otherwise ends without installing Node. Light mode never
# reaches it: the job reads no light condition at all, so an unlabeled light pull request runs
# the screens in full. The gui Makefile target runs the same three commands.
runs-on: ubuntu-24.04
timeout-minutes: 15
# The mirror reads this repository's runs and jobs with the workflow's own token.
permissions:
contents: read
actions: read
steps:
# A body-only edit needs only the mirror script, and the script has to exist before the
# mirror decides, so it is fetched sparsely first. Every other run skips this step.
- name: Check out scripts/ci for the body-only edit mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
sparse-checkout: scripts/ci
- name: Mirror the jobs this head already ran
id: mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base }}
run: bash scripts/ci/edit_mirror.sh
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
JOB_NAME: gui
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
if: steps.mirror.outputs.mirrored != 'true'
with:
# The drift check reads the committed tree with git, so the whole history has to be here.
fetch-depth: 0
persist-credentials: false
# The decision runs before Node is installed, so an unrelated change ends the job here. The
# script selects the full run for anything it cannot read, and it is unit-tested.
- name: Decide from the changed files whether the screens changed
id: paths
if: steps.mirror.outputs.mirrored != 'true'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE_SHA: ${{ github.event.before }}
run: bash scripts/ci/gui_paths.sh
- if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
# The same exact release the staged skill-script tests use: the built asset hashes are
# part of the committed tree, so the same commit must meet the same Node.
node-version: '24.20.0'
- name: Install the screen dependencies from the committed lockfile
if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
working-directory: web
run: npm ci
- name: Run the screen tests
if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
working-directory: web
run: npm test
# The build goes to a temporary tree, never to the committed internal/gui/assets, so the
# drift check compares the commit against a real rebuild rather than against itself.
- name: Build the screens into a fresh tree
if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
working-directory: web
run: npm run build -- --outDir "$RUNNER_TEMP/gui-built" --emptyOutDir
- name: Refuse a committed tree that is not a fresh build
if: steps.paths.outputs.changed == 'true' && steps.mirror.outputs.mirrored != 'true'
run: go run -tags dev ./cmd/crw-dev ci gui-drift --built "$RUNNER_TEMP/gui-built"
go-product:
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: read
# CRW-790: while the repository variable CRW_CI_MODE is light, a pull request the merge lane
# has not labeled crw-lane skips the test legs' work, and the labeled event -- and every
# later push while the label stays -- runs them in full. One definition, read by the steps
# below, so lint and dist can never be light and neither can a dev push or a manual dispatch.
# A skipped job's check still reads success, so a light leg skips its steps and never the job.
env:
CRW_LIGHT_LEG: ${{ github.event_name == 'pull_request' && vars.CRW_CI_MODE == 'light' && !contains(github.event.pull_request.labels.*.name, 'crw-lane') && startsWith(matrix.part, 'test-') }}
strategy:
fail-fast: false
matrix:
# One runner each: lint; the Go suite as `make test-part` parts, which together are
# `make test` (the contract corpus included); the static release binaries.
part: [lint, test-1, test-2, test-3, test-4, test-5, test-rest, test-dev, dist]
steps:
# A light leg says so in its summary and does none of the leg's work. The condition is a
# step condition, never a job-level if: GitHub reports a skipped job's check as success, so
# a skipped leg could hide an earlier red run behind a green dev-gate.
- name: Light mode notice
if: env.CRW_LIGHT_LEG == 'true'
run: |
printf '%s\n' "light mode: this leg's tests run in full when the merge lane labels the pull request crw-lane" >> "$GITHUB_STEP_SUMMARY"
- name: Check out scripts/ci for the body-only edit mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base && env.CRW_LIGHT_LEG != 'true' }}
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 1
persist-credentials: false
sparse-checkout: scripts/ci
- name: Mirror the jobs this head already ran
id: mirror
if: ${{ github.event.action == 'edited' && !github.event.changes.base && env.CRW_LIGHT_LEG != 'true' }}
run: bash scripts/ci/edit_mirror.sh
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
RUN_ID: ${{ github.run_id }}
PR_NUMBER: ${{ github.event.pull_request.number }}
JOB_NAME: go-product (${{ matrix.part }})
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
if: steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
with:
persist-credentials: false
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
if: steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
with:
go-version-file: go.mod
- name: Lint
if: matrix.part == 'lint' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
run: make lint
- name: Test and replay the contract corpus (${{ matrix.part }})
if: startsWith(matrix.part, 'test-') && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
env:
PART: ${{ matrix.part }}
run: make test-part TEST_PART="${PART#test-}"
- name: Build the static binaries for every published target
if: matrix.part == 'dist' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
run: |
for target in linux/amd64 linux/arm64 darwin/arm64; do
os=${target%/*}
arch=${target#*/}
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" make dist BINARY="dist/crw_${os}_${arch}/crw"
done
(cd dist && sha256sum crw_*/crw > SHA256SUMS && cat SHA256SUMS)
# IS-1, IS-7 and IS-8: the linux/amd64 binary just built is installed into an isolated
# home and the plugin's declared wiring runs against it. CGO_ENABLED=0 and -trimpath are
# the dist build's, so the test reuses its compiled packages and only relinks.
- name: Install and wire the release binary in an isolated home
if: matrix.part == 'dist' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
env:
CGO_ENABLED: '0'
run: CRW_TEST_BINARY="$PWD/dist/crw_linux_amd64/crw" go test -trimpath -tags integration -count=1 ./internal/runtime/integration/...
- if: matrix.part == 'dist' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: crw_linux_amd64
path: dist/crw_linux_amd64/crw
if-no-files-found: error
- if: matrix.part == 'dist' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: crw_linux_arm64
path: dist/crw_linux_arm64/crw
if-no-files-found: error
- if: matrix.part == 'dist' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: crw_darwin_arm64
path: dist/crw_darwin_arm64/crw
if-no-files-found: error
- if: matrix.part == 'dist' && steps.mirror.outputs.mirrored != 'true' && env.CRW_LIGHT_LEG != 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SHA256SUMS
path: dist/SHA256SUMS
if-no-files-found: error
# The one required check. It runs after every other job, even a failed or cancelled one, and
# passes only when every job it needs succeeded. The results are written into a file by the
# step's own script, never passed in the environment, whose size the runner bounds.
dev-gate:
if: always()
needs: [validate, secrets, skill-scripts-node, gui, go-product]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require every prerequisite to succeed
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
if [[ "$EVENT_NAME" == pull_request && "$BASE_REF" == main ]]; then
echo '::error::PRs must target a development branch; main is a release mirror.'
exit 1
fi
results="$RUNNER_TEMP/needs.json"
cat > "$results" <<'NEEDS'
${{ toJSON(needs) }}
NEEDS
jq -r 'to_entries[] | "\(.key): \(.value.result)"' "$results" || true
if ! jq -e 'type == "object" and length > 0 and all(.[]; type == "object" and .result == "success")' "$results" > /dev/null; then
echo '::error::Every prerequisite job must succeed.'
exit 1
fi
echo 'Every prerequisite job succeeded.'