-
Notifications
You must be signed in to change notification settings - Fork 34
383 lines (359 loc) · 17.2 KB
/
Copy pathci.yml
File metadata and controls
383 lines (359 loc) · 17.2 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
name: PR gate
# THE LIGHT GATE, AND IT IS DELIBERATELY LIGHT. Work moves into `dev` many
# times a day, much of it written by agents, and a gate that takes fifteen
# minutes is a gate people learn to route around. So this one asks only the
# questions whose answer is always the same on every machine and whose failure
# always means somebody broke something: does the tree compile, is it
# formatted, does vet find a real bug, do the laws hold, and does the manual
# still know about the feature that just landed.
#
# THE LAWS ARE HERE BECAUSE THEY WERE NOT. Until 2026-09-02 this gate's one
# test step was `go test -run 'Manual'` over two packages — a filter nobody
# remembered — and the endings ratchet in internal/session went red on dev
# through two merged pull requests while every check here was green (#372).
# A structural test decides in under a second and the same on every machine;
# it belongs on the light gate by the light gate's own definition, and now
# `make test-laws` finds every one of them without anybody keeping a list.
#
# The whole suite is not here. The packages a change touched run in full in the
# concurrent touched legs below, on every pull request, and `check` — the one name a person
# and a ruleset look at — is green only when both are; everything else runs on
# the way into `staging`, and nightly against `dev` — see ci-full.yml.
# docs/rules/ci.md says why the line is drawn in that place.
#
# EVERY LINE WORK LANDS ON IS GATED, NOT ONLY THE TRUNK. `santos/dev2` is a
# line feature work merges into many times a day, and until 2026-09-21 no
# workflow named it: this gate listed `dev` alone and ci-full.yml lists
# `staging` and `main`, so a pull request based on `santos/dev2` was answered by
# the licence check and nothing else. Whether that line was green was a question
# only a person on a cluster could answer, and for a while the answer was no.
# That is the same failure the note above records for #372, one level up: not a
# filter nobody remembered, but a branch nobody listed.
on:
pull_request:
branches:
- dev
- santos/dev2
# Also on the push, because a merge of two individually-green branches can
# still be red, and the merge is exactly the moment nobody is watching.
push:
branches:
- dev
- santos/dev2
workflow_dispatch:
permissions:
contents: read
# A pull request's newer push cancels its older run; a push to `dev` never
# cancels another, because the group is the commit. Keyed on the ref, the
# second merge inside twenty-five minutes cancelled the first merge's
# `touched packages` run, and a cancelled run leaves no red — on exactly the
# push the trigger above exists for.
concurrency:
group: pr-gate-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
# THE LIGHT HALF: the questions that answer in a few minutes. It is not the
# required name — `check`, at the bottom, is — so that the light answer can
# arrive early while the touched packages are still running beside it.
light:
name: light gate
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
# The changelog check compares against the base of the pull request,
# which a shallow clone does not contain.
fetch-depth: 0
- uses: actions/setup-go@v5
id: go
with:
go-version-file: go.mod
cache: false
# UTC-day keys refresh each namespace on its first dev push of the day.
# PRs only restore; the undated prefix finds the newest compatible entry.
- name: Cache paths
id: cache-paths
run: |
echo "build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "modules=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
echo "today=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@v4
id: build-cache
with:
path: ${{ steps.cache-paths.outputs.build }}
key: codeaf-go-v1-light-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-${{ steps.cache-paths.outputs.today }}
restore-keys: |
codeaf-go-v1-light-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-
- uses: actions/cache/restore@v4
id: module-cache
with:
path: ${{ steps.cache-paths.outputs.modules }}
key: codeaf-go-v1-modules-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum') }}
restore-keys: |
codeaf-go-v1-modules-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-
# Several sessions work this tree at once and a half-finished file breaks
# the build for everyone. This is the cheapest way to find that out, and
# the one failure that blocks every other kind of work.
- name: Build
run: go build ./...
- name: Vet
run: go vet ./...
# A file gofmt would rewrite is a file the next editor's save rewrites,
# and that diff lands in somebody else's pull request. Two files had
# drifted by the time anybody looked (#372).
- name: gofmt
run: make fmt-check
# THE PACKED CORPORA ARE GENERATED FROM THE FOLDERS. The manuals are
# ignored build products because committing one shared binary made every
# manual-bearing branch conflict; exercise that exact shipped source mode.
# The remaining generated corpora are tracked, so a diff still means real
# drift between their folders and archives.
- name: The packed corpora build from their folders
run: |
set -euo pipefail
go generate ./internal/manual
go test -tags=codeaf_packed_manual ./internal/manual
if ! git diff --exit-code; then
echo
echo 'A tracked packed corpus disagrees with its source folder.'
echo 'Run `make embed` and commit the regenerated archive.'
exit 1
fi
# THE CHANGELOG CARRIES WHAT A DIFF CANNOT — which of the things somebody
# believes about this repository stopped being true. It is checked here
# because it is worth nothing written later: the only moment the author
# knows they contradicted a page is the moment they went and edited it.
#
# Two seconds, and the cost of complying is one file with three fields.
# `kind: internal` with just a title is a legitimate entry; the
# `no-changelog` label is the way out for a typo in a comment.
# docs/rules/changelog.md is the rule.
- name: The change is written down
if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'no-changelog')
env:
BASE: ${{ github.event.pull_request.base.sha }}
PRNUM: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
go run ./cmd/codeaf-changes check
if git diff --name-only --diff-filter=A "$BASE" HEAD \
-- 'docs/changes/unreleased/*.md' | grep -q .; then
echo "This pull request adds a change entry."
exit 0
fi
cat <<'MSG'
This pull request adds no entry in docs/changes/unreleased/.
The changelog here is written for whoever reads the repository next,
and increasingly that is a model with a fortnight-old memory of it. Its
job is to say what somebody now believes WRONGLY — the branch that
stopped existing, the default that moved, the refusal that became a
capability. No tool can derive that from a diff, which is why it is
asked for here.
MSG
# The number is spelled out so the command pastes unedited — a message
# that can be pasted unedited is a message that gets pasted.
echo " make changelog-new PR=${PRNUM} KIND=changed SLUG=<a-short-slug>"
cat <<'MSG'
Kinds: added changed renamed fixed removed internal. A one-line
`kind: internal` entry is perfectly legitimate and takes ten seconds.
If this really is a typo in a comment, add the `no-changelog` label.
docs/rules/changelog.md says what belongs in the fields.
MSG
exit 1
# THE MANUAL LAW, ENFORCED HERE BECAUSE IT IS THE LAW AGENTS BREAK MOST.
# A feature that ships without its page is a feature the running chat will
# deny having. These are the three gates CLAUDE.md names — every slash
# command and alias, every tool on the belt, every probe question still
# reaching the page that answers it — and they cost six seconds.
- name: The manual knows about the features
run: |
set -euo pipefail
go test ./internal/manual/
go test -run 'Manual' ./internal/tui3/ ./internal/session/
# THE LAWS HOLD. Every test that reads the tree itself — the endings
# ratchet, the guard, the taxonomy, the words the e2e suite waits for —
# found by what it does (scripts/laws.sh) and run as one target, about
# twenty seconds after the link. (The known-red ratchet was one of these
# until the ledger burned to zero in #1012.) This is the step that
# would have stopped #279 at the door.
#
# TMPDIR IS SET ON THIS STEP AND NOT ON THE JOB. A test that binds a unix
# socket under t.TempDir() overflows sun_path on the runner's default
# path, so the tests get a short one; but set on the job it reaches
# setup-go's own `go env`, which runs before any step has created the
# directory, and the go command fails making its work dir. Two runs of
# this very pull request died that way.
- name: The laws hold
env:
TMPDIR: /tmp/codeaf-ci
run: |
mkdir -p "$TMPDIR"
make test-laws
# THE CACHE BUDGET IS DAILY, NOT PER COMMIT. At most five build
# namespaces a day plus one module entry per go.sum cost about 2 GB a day
# at the measured ~385 MB. GitHub's least-recently-used eviction at the
# 10 GB repository limit removes old days without a pruning script.
# Only dev pushes that miss the exact key save; PRs never save.
- uses: actions/cache/save@v4
if: always() && github.event_name == 'push' && github.ref == 'refs/heads/dev' && steps.cache-paths.outcome == 'success' && steps.build-cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.cache-paths.outputs.build }}
key: ${{ steps.build-cache.outputs.cache-primary-key }}
- uses: actions/cache/save@v4
if: always() && github.event_name == 'push' && github.ref == 'refs/heads/dev' && steps.cache-paths.outcome == 'success' && steps.module-cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.cache-paths.outputs.modules }}
key: ${{ steps.module-cache.outputs.cache-primary-key }}
# The selector does no compilation unless module files changed. A docs-only
# change creates no test runner; the light gate and the aggregate still run.
select:
name: select touched packages
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.select.outputs.matrix }}
has-tests: ${{ steps.select.outputs.has-tests }}
base: ${{ steps.select.outputs.base }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Select and partition packages
id: select
env:
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
set -euo pipefail
case "${BASE:-}" in ''|0000000000000000000000000000000000000000) BASE="$(git rev-parse HEAD~1)";; esac
export BASE
echo "base=$BASE" >> "$GITHUB_OUTPUT"
./scripts/touched-packages.sh | python3 scripts/touched-matrix.py >> "$GITHUB_OUTPUT"
# THE LEGS START TOGETHER ON SEPARATE RUNNERS. A failure never cancels another
# leg's evidence. Five named failures at most receive one focused retry each
# and then a base probe; unnamed failures fail immediately. No test is skipped.
touched-leg:
name: touched ${{ matrix.leg }}
needs: select
if: needs.select.outputs.has-tests == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.select.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
id: go
with:
go-version-file: go.mod
cache: false
# UTC-day keys refresh each namespace on its first dev push of the day.
# PRs only restore; the undated prefix finds the newest compatible entry.
- name: Cache paths
id: cache-paths
run: |
echo "build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
echo "modules=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
echo "today=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@v4
id: build-cache
with:
path: ${{ steps.cache-paths.outputs.build }}
key: codeaf-go-v1-${{ matrix.leg }}-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-${{ steps.cache-paths.outputs.today }}
restore-keys: |
codeaf-go-v1-${{ matrix.leg }}-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-
codeaf-go-v1-light-build-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-
- uses: actions/cache/restore@v4
id: module-cache
with:
path: ${{ steps.cache-paths.outputs.modules }}
key: codeaf-go-v1-modules-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-${{ hashFiles('go.sum') }}
restore-keys: |
codeaf-go-v1-modules-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-
- name: Runner capacity
run: |
nproc
free -g
# Offline module-listing tests need the whole module cache; each leg compiles only part of the tree.
- name: Download modules
run: go mod download
- name: Test and attribute failures
env:
BASE: ${{ needs.select.outputs.base }}
PACKAGES: ${{ matrix.packages }}
LEG: ${{ matrix.leg }}
TMPDIR: /tmp/codeaf-ci
run: |
set -euo pipefail
mkdir -p "$TMPDIR"
# -p 2 permits concurrent compilation/testing in rest on the public
# 4-vCPU / 16-GB runner, leaving headroom for the linker. tui3 and
# session use four shards; codeaf runs its suite without sharding.
./scripts/touched-verdict.sh run --base "$BASE" --shards 4 --report "$TMPDIR/$LEG.json" $PACKAGES
- uses: actions/upload-artifact@v4
if: always()
with:
name: touched-verdict-${{ matrix.leg }}
path: /tmp/codeaf-ci/${{ matrix.leg }}.json
retention-days: 7
- uses: actions/cache/save@v4
if: always() && github.event_name == 'push' && github.ref == 'refs/heads/dev' && steps.cache-paths.outcome == 'success' && steps.build-cache.outputs.cache-hit != 'true'
with:
path: ${{ steps.cache-paths.outputs.build }}
key: ${{ steps.build-cache.outputs.cache-primary-key }}
# KEEP THIS NAME. Consumers of `touched packages` see the aggregate even when
# no leg was needed; selector failures and failed/cancelled legs remain red.
touched:
name: touched packages
needs: [select, touched-leg]
if: always()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
if: needs.select.outputs.has-tests == 'true'
continue-on-error: true
with:
pattern: touched-verdict-*
merge-multiple: true
path: /tmp/codeaf-verdicts
- name: Record flaky and inherited failures
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
TOUCHED_REPORT_ISSUE: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
run: ./scripts/touched-verdict.sh report-issues /tmp/codeaf-verdicts
- name: Every needed leg passed
if: always()
env:
SELECT: ${{ needs.select.result }}
HAS_TESTS: ${{ needs.select.outputs.has-tests }}
LEGS: ${{ needs.touched-leg.result }}
run: |
set -euo pipefail
echo "selection: $SELECT; needed: $HAS_TESTS; touched legs: $LEGS"
[ "$SELECT" = success ]
if [ "$HAS_TESTS" = true ]; then [ "$LEGS" = success ]; else [ "$LEGS" = skipped ]; fi
# The single required name stays check. Rulesets and landing scripts need
# only this result, which requires both the light gate and the aggregate.
check:
name: check
needs: [light, touched]
if: always()
runs-on: ubuntu-latest
steps:
- name: Both halves passed
run: |
set -euo pipefail
echo "light gate: ${{ needs.light.result }}; touched packages: ${{ needs.touched.result }}"
[ "${{ needs.light.result }}" = "success" ] && [ "${{ needs.touched.result }}" = "success" ]