-
Notifications
You must be signed in to change notification settings - Fork 0
465 lines (444 loc) · 29.1 KB
/
Copy pathci.yml
File metadata and controls
465 lines (444 loc) · 29.1 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
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
name: ci
# The gate. Same steps a contributor runs locally with `bun run verify` — a check that lives only
# in CI is a check developers cannot run. Free GitHub-hosted runners only.
#
# Five jobs, and each answers a question no other job answers: does the framework ship (`verify`),
# do the tracked apps still gate (`reference-app-verify`), can a stranger scaffold an app that
# gates (`scaffold-smoke`), do the deployable artifacts BUILD and RENDER (`container`), and is each
# package green and above its coverage bar in isolation (`packages`). The last two each carry their
# own argument, above the job. There is deliberately no separate `lint`,
# `typecheck`, `boundaries` or `test` job — those are steps of `x verify`, and running them again
# beside the gate is the second path axiom 1 forbids. It also cost what it duplicated: the four
# jobs took ~2.2 runner-minutes and, because `verify` waited on them through `needs`, added their
# slowest (~1.6 min) to the critical path before the gate could start. `x verify` reports per step,
# so a red lint still names itself on its own line.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# A newer push to a PULL REQUEST supersedes the in-flight run for that branch: CI is a signal, not
# an archive. A push to `main` never supersedes anything, and the group is keyed by SHA there so it
# cannot.
#
# It was `group: …-${{ github.ref }}` with `cancel-in-progress: true` unconditionally, and every
# push to main shares one ref. Merge two PRs ninety seconds apart and the FIRST commit's run was
# cancelled by the second's — so its `reference-app-verify` concluded `cancelled`, and
# `deploy-social-demo.yml`'s verdict check (correctly) fails closed on any conclusion that is not
# `success`. The image for the first commit never shipped, and the log read like a red gate on a
# commit whose gate never ran.
#
# Keying pushes by SHA rather than setting `cancel-in-progress: false` for them: a shared group with
# no cancellation SERIALISES, so the second merge would wait out the first's full run before
# starting. Per-SHA groups let both run, and neither can cancel the other.
concurrency:
group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
# Deterministic tests: frozen clock, seeded RNG, sealed network (scripts/test-setup.ts).
ULTIMATE_TEST_SEED: '20260101'
# A browser IS present on `ubuntu-latest`, so an e2e suite that skips for want of one here is a
# green step over nothing — which is exactly what the first run of
# `packages/cli/e2e/cdp-browser.e2e.test.ts` was: 2,958ms on a step that takes 31s with a
# browser. Set, the suite refuses by name (`X_CDP_BROWSER_MISSING`) instead of skipping, so the
# day the runner image drops Chrome is a red job naming the paths it tried rather than silence.
E2E_BROWSER_REQUIRED: '1'
jobs:
# The whole gate, exactly as `x verify` runs it. Green here means shippable.
verify:
runs-on: ubuntu-latest
timeout-minutes: 12
# A real Postgres, not a fake driver — packages/testing/src/template-db.ts clones a database
# off it per worker, so client.ts/introspect.ts/branch.ts get exercised against a live server
# instead of a recording client. No admin URL means those tests skip, so `bun test` still
# passes on a laptop with nothing installed.
#
# pgvector/pgvector:pg17 rather than stock postgres:17: @ultimat3/ai's PgVectorStore is the
# only production vector path, and pg-vector.live.test.ts REFUSES to skip when the extension
# is missing. Asserting the statement text a store compiles to cannot catch a statement
# Postgres rejects — this image is what makes that suite a measurement instead of a skip.
services:
postgres:
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: ultimate
POSTGRES_PASSWORD: ultimate
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U ultimate"
--health-interval=10s
--health-timeout=5s
--health-retries=5
# The shared cache tier's two Lua scripts are the only thing in the framework a real Redis
# answers differently from a fake: `EVAL` enforces the KEYS declaration, and the invalidation
# script `DEL`d keys it never declared. Asserting the script TEXT cannot catch a script the
# server rejects, so without this container that path is proved by nothing.
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
TEST_DATABASE_URL: postgres://ultimate:ultimate@localhost:5432/postgres
TEST_REPLICATION_URL: postgres://ultimate:ultimate@localhost:5433/postgres
TEST_NATS_URL: nats://localhost:4222
# Absent on a laptop, so `packages/cache/src/redis.live.test.ts` skips there and measures here.
TEST_REDIS_URL: redis://localhost:6379
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: ./.github/actions/setup
# `wal_level = logical` is a server argument, and a service container takes none —
# so the replication server is started by hand. Without it the logical-replication
# feed's live tests skip, and a decoder nothing exercises is a decoder nobody trusts.
#
# JetStream is a server argument too, and it is what presence is built on — the KV bucket,
# its per-message TTL and its batch direct reads. Without a real one the bus client is only
# ever proved against the in-memory server in nats-fake.ts, which is the thing being checked.
#
# Both are started before either is waited on: they are independent servers, and starting
# them in sequence pays each one's readiness wait end to end for no reason.
- name: start the logical-replication postgres and the JetStream nats-server
run: |
docker run -d --name pg-logical \
-e POSTGRES_USER=ultimate -e POSTGRES_PASSWORD=ultimate -e POSTGRES_DB=postgres \
-p 5433:5432 postgres:17-alpine -c wal_level=logical
docker run -d --name nats-js -p 4222:4222 nats:2.11-alpine -js
# `-h 127.0.0.1`, not the default unix socket, and that is the whole point: the
# postgres entrypoint boots a TEMPORARY server to run initdb, and that one listens on
# the socket ONLY. A socket probe therefore answers "accepting connections" for a
# server that is about to shut down and be replaced — this step went red on main
# having logged "accepting connections" and "no response" two lines apart. TCP is up
# only once the real server is, so it cannot see the bootstrap one at all.
for _ in $(seq 1 30); do
docker exec pg-logical pg_isready -U ultimate -h 127.0.0.1 -p 5432 && break
sleep 2
done
docker exec pg-logical pg_isready -U ultimate -h 127.0.0.1 -p 5432 \
|| { echo 'postgres with wal_level=logical never became ready' >&2; exit 1; }
for _ in $(seq 1 30); do
docker logs nats-js 2>&1 | grep -q 'Server is ready' && break
sleep 2
done
docker logs nats-js 2>&1 | grep -q 'Server is ready' \
|| { echo 'nats-server never became ready' >&2; exit 1; }
- name: x verify
run: bun run scripts/verify.ts --json
# Both tracked apps — `examples/dummy` and the deployed `dummy/social-media-clone` — run the
# same 19-step gate a generated app would, and this job BLOCKS on both. Independent of `verify`:
# the framework gate and the app gate are two different contracts, checked separately — a red app
# doesn't hide a green framework or vice versa. One job, not two, because they answer the same
# question about two directories; the script names the app in every finding.
#
# Neither app is green yet, so blocking on a bare `x verify` would make red the resting state,
# which is how a gate stops being read. `scripts/reference-app-gate.ts` blocks on the ratchet
# instead: every step passing today must keep passing, every step pinned in that app's
# `expectedRed` table (scripts/lib/gated-apps.ts) must still be failing, and the moment an app's
# `typecheck` comes off the pin it has to join the root `tsc -b` solution. The pin can only ever
# shrink, and when it empties the job is asserting a plain 19 of 19. Human render, not `--json`:
# the whole step table, every failing step and every fix line belong in the log for the person
# reading a red run.
#
# Job id unchanged from when it gated one app: it is a required status check.
reference-app-verify:
runs-on: ubuntu-latest
# Covers BOTH apps, which the gate runs sequentially — and it is a runaway kill-switch, not a
# budget. Measured `As of 2026-08` over four runs of this job with both apps wired in: 53s, 29s,
# 28s, 24s wall-clock end to end, so 15 minutes is ~17× the worst one. A gate whose steps fail
# fast is dominated by install and boot, not by the second app; the number moves when a
# measurement moves it.
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: x verify (every tracked app), against the ratchet
run: bun run scripts/reference-app-gate.ts
# The end-to-end promise, proved for real and proved as the PLATFORM consumes it: a dz-runner box
# is an Ubuntu VM with bun and git, no docker and no services, and on a freshly scaffolded app it
# runs the app's own `bin/setup && bin/check`. This job runs those two scripts — the ones `x new`
# writes, spawned as a box spawns them — and holds the FIRST `bin/check` to green with no waiver
# and no repair round. A GitHub `ubuntu-latest` runner with bun, git and network is the closest
# thing in this repo to that VM; it is not the VM, and the timings the job prints say so.
#
# Distinct from scaffold-typecheck.ts (which compiles every generated file set in-process, in the
# workspace, against source packages, and tolerates a short list of pinned KNOWN_GAPS) — this job
# runs the real CLI-produced files through a real, separate `bun install` and a real gate
# subprocess, catching whatever only shows up once an app is actually assembled and installed (a
# missing package-shape file, a dependency range with no matching release, a preload path that
# only resolves inside this workspace). It runs outside the checkout so nothing here can quietly
# depend on this repo's own workspace linking.
#
# No `needs`: it was gated on lint/typecheck/boundaries to avoid scaffolding from a tree that
# could not compile, but those jobs are gone and waiting on the whole gate would serialise a
# 20-second job behind a two-minute one to save a runner-minute it usually does not save.
#
# THE TWO CI-ONLY LINES, and neither is doctoring the app under test:
#
# * `@ultimat3/*` isn't published to npm this early in the pipeline (that happens on release,
# from a tag — see PUBLISHING.md), so a plain `bun install` in the scaffolded app can't
# resolve it from the registry. `scripts/scaffold-smoke-overrides.ts` rewrites those ranges to
# `file:` overrides pointing at this repo's own `packages/*` — the same source a release would
# publish, installed the same way `bun install` installs anything.
# * an `[install]` section APPENDED to that app's own `bunfig.toml` pins it to the hoisted
# linker, and it is the override's own tax (appended, never written over: `x new` writes that
# file for `[test]`, and replacing it drops the preload that seals the network and freezes the
# clock for the gate's own `unit` step): Bun's
# default isolated linker does not resolve a `file:` dependency's own `@ultimat3/*` imports
# (reproduced: `x db migrate` dies on `Cannot find module '@ultimat3/time'` from
# `node_modules/.bun/@ultimat3+jobs@file.../src/task.ts`), and it races on identical `file:`
# targets shared by many dependents. It is a bunfig rather than the `bun install
# --linker=hoisted` this job used to run itself, because `bin/setup` now performs the install
# and a second `bun install` in the other layout relays out the whole tree.
#
# TWO scaffolds and two gates each, so the budget is two of everything below plus a second
# `bin/setup` and `bin/check` per scaffold. 20 minutes is the ceiling that covers it; the job is
# the slowest in this file and deliberately so — it is the only one that measures what a
# stranger's first ten minutes with the framework actually do.
scaffold-smoke:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: x new demoapp
run: bun run x -- new demoapp --dir "${{ runner.temp }}"
- name: point demoapp's @ultimat3/* ranges at this repo's own packages
run: |
bun run scripts/scaffold-smoke-overrides.ts "${{ runner.temp }}/demoapp"
printf '\n[install]\nlinker = "hoisted"\n' >> "${{ runner.temp }}/demoapp/bunfig.toml"
# THE CONTRACT, and the only step in this repo that runs it. `bin/setup` installs, generates
# and applies the first migration, SEEDS and writes the manifest; `bin/check` builds and then
# gates. Both are the scaffold's own scripts — no flag waives a step, and nothing runs a
# printed `fix:` and tries again, because a box gets one pass.
#
# `budgets` is asserted GREEN here rather than allowed to stay red, which it was until this
# change: `x verify` weighs `.x/build-stats.json` and the only writer is `x build --target
# static`, which is `bin/check`'s first line. Running the real script is what closed the
# waiver — not a change to the budgets step.
#
# It prints per-step wall time for both commands and for all 20 gate steps, so the run is its
# own measurement rather than something a PR body claims.
- name: bin/setup && bin/check (demoapp), first pass, no waiver
run: bun run scripts/scaffold-gate.ts "${{ runner.temp }}/demoapp"
# The generator sweep, over an app whose first run has already happened. All thirteen exit 0
# even when what they emit cannot resolve an import, so this step is not the catch: the gate
# below is, over the files they wrote — `TS2307: Cannot find module '../repo'` was invisible
# to every exit code in this job. The defect that bought it broke 4 generators of the 13,
# found by a human scaffolding an app — so the list is projected from the CLI's own
# `GENERATORS` and a fourteenth generator is covered the day it lands.
- name: every generator (demoapp)
run: bun run scripts/scaffold-first-run.ts "${{ runner.temp }}/demoapp"
# The same two scripts again, and the second run is not a repeat: `bin/setup` applies the
# migration the generators earned (which is also what makes its own documented "idempotent,
# safe to re-run" a measurement), and `bin/check` gates 13 generators' output with the same
# no-waiver rule.
- name: bin/setup && bin/check (demoapp), over every generator's output
run: bun run scripts/scaffold-gate.ts "${{ runner.temp }}/demoapp"
# The SECOND scaffold. `scaffold-gate.ts` takes one directory per run by design, so covering
# both shapes is two invocations rather than a flag. `--no-example` is an app with no islands
# and no example slice — 123 files against the default's 151, measured with
# `x new <name> --dry-run --json` and derived by `scripts/generator-counts.ts`, never counted
# by hand — which is precisely why it stayed green through a default scaffold whose fix chain
# was a loop: the steps that read emitted island and feature files had nothing to read.
- name: x new bareapp --no-example
run: bun run x -- new bareapp --dir "${{ runner.temp }}" --no-example
- name: point bareapp's @ultimat3/* ranges at this repo's own packages
run: |
bun run scripts/scaffold-smoke-overrides.ts "${{ runner.temp }}/bareapp"
printf '\n[install]\nlinker = "hoisted"\n' >> "${{ runner.temp }}/bareapp/bunfig.toml"
- name: bin/setup && bin/check (bareapp), first pass, no waiver
run: bun run scripts/scaffold-gate.ts "${{ runner.temp }}/bareapp"
- name: every generator (bareapp)
run: bun run scripts/scaffold-first-run.ts "${{ runner.temp }}/bareapp"
- name: bin/setup && bin/check (bareapp), over every generator's output
run: bun run scripts/scaffold-gate.ts "${{ runner.temp }}/bareapp"
# The deployable artifacts, BUILT and RENDERED — not merely present.
#
# The rule at the top of this file says a job beside the gate has to be argued. Here is the
# argument: the ONLY thing in `x verify` that looks at `docker/` is the `roadmap` step, and it
# asks whether `docker/Dockerfile`, `docker/docker-compose.prod.yml` and `docker/helm` EXIST
# (scripts/roadmap.ts, REQUIRED_ARTIFACTS[11]). Existence is not function, and nothing anywhere
# built the image or rendered the chart. That is why this set rotted unseen:
#
# - the image's default command was "dev --once" — the DEV SERVER, which ignores ROLE and is
# documented "boot, report, exit" — so every service in a compose file booted it, exited 0 and
# was restarted forever, and "run --rm migrate" applied no migration and reported success;
# - the chart declared serviceAccount.create and serviceAccount.name and read neither, so an
# operator configuring IRSA got the chart's own account and pods with no cloud identity.
#
# Neither is visible to a path check, and both are caught below. Every step is a command a
# developer can paste and run — the rule at the top of this file — which is also why the
# assertions are shell over docker and helm rather than a script this job alone knows how to run.
#
# No ./.github/actions/setup, and that is deliberate rather than forgotten: nothing here runs
# bun, so the composite's frozen install would be pure latency. docker, helm and jq all come from
# the runner image, and the first step refuses if one is missing.
#
# Cost: measured 20s wall clock for the full image build on a warm daemon — bun install
# --frozen-lockfile 1.1s, bun build --compile --minify 1.8s, because the framework has very few
# external dependencies. A cold runner pays the two base image pulls on top. That is comfortably
# inside the 5-minute target, so this builds the WHOLE image rather than a --target slice: the
# runtime stage is where three of the four recorded build bugs lived (musl-vs-glibc, the Debian
# skew, the missing --define), and a build stopping at "build" would have caught none of them.
container:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: docker, helm and jq, from the runner image
run: |
set -euo pipefail
for tool in docker helm jq; do
command -v "$tool" >/dev/null || {
echo "::error title=${tool} missing::The ubuntu-latest runner image no longer ships ${tool}, so this job cannot check docker/. fix: add a SHA-pinned setup action for ${tool} to this job — the pinning rule and its reason are in .github/actions/setup/action.yml — or install it inline in this step."
exit 1
}
done
docker version --format 'docker {{.Server.Version}}'
helm version --short
jq --version
# Builds every stage, ending in the runtime stage's own RUN ["/app/x", "--version"]. That exec
# is what turns a wrong libc, a Debian skew, a missing --define or a lost workspace manifest
# into a red build instead of a red pod — all four have happened.
- name: build the framework's CLI image
run: docker build -f docker/Dockerfile -t ultimate-cli:ci .
# The assertion the CMD defect needed. "x" with no arguments prints its catalogue and exits 0,
# so a default command that boots ANYTHING fails here twice over: "x dev --once" in a container
# with no app exits 1 with X_NOT_IN_APP and prints no catalogue. Verified against a mutant
# image built FROM this one with the old CMD restored — exit 1, no catalogue.
- name: the image is a CLI, and its default command is the catalogue
run: |
set -euo pipefail
out="$(docker run --rm ultimate-cli:ci)"
echo "$out"
case "$out" in
*"usage: x <command>"*) echo "ok: the default command is the CLI catalogue" ;;
*)
echo "::error title=Image default command is not the CLI catalogue::docker run ultimate-cli:ci with no arguments did not print the x command catalogue. This image contains /app/x and no app, so it can serve no ROLE; a CMD that boots something is an infinite restart loop under 'restart: unless-stopped', and a release phase that reports success having done nothing. fix: remove the CMD from docker/Dockerfile — an app image is what serves roles, and 'x new' writes one."
exit 1
;;
esac
# The --define ULTIMATE_FRAMEWORK_VERSION chain, checked on the SHIPPED artifact and against
# the source of truth rather than against itself. The runtime stage's RUN proves the binary
# answers --version at build time; this proves the answer is the right number and that the
# ENTRYPOINT wiring survived into the image config.
- name: the version define survived, and matches @ultimat3/core
run: |
set -euo pipefail
expected="$(jq -r .version packages/core/package.json)"
actual="$(docker run --rm ultimate-cli:ci --version --json | jq -r .data.version)"
echo "core=${expected} image=${actual}"
[ "$expected" = "$actual" ] || {
echo "::error title=Image version mismatch::The image answers --version '${actual}' but packages/core/package.json is '${expected}'. A single-file executable carries no package.json, so this number comes only from the --define in docker/Dockerfile's build stage. fix: check the VERSION_DEFINE line in docker/Dockerfile, then run 'bun run scripts/release.ts --check ${expected}' for a workspace that was not stamped."
exit 1
}
# Both compose files, parsed by compose itself. .env.production is a secrets file that is never
# committed and env_file: is required by design — a missing one must fail an operator loudly —
# so an empty stand-in is created for the syntax check and removed again.
- name: both compose files parse
run: |
set -euo pipefail
touch .env.production
trap 'rm -f .env.production' EXIT
docker compose -f docker/docker-compose.dev.yml config -q
docker compose -f docker/docker-compose.prod.yml config -q
echo "ok: both compose files parse"
- name: helm lint
run: helm lint docker/helm --strict
# Defaults plus every branch the defaults leave dark. serviceMonitor, ingress and the
# replicator role are all off by default, and scheduler/replicator carry no autoscaling block
# at all — that last one is what made the whole chart fail to render once, so the no-HPA path
# is rendered on purpose rather than left to a default that hides it.
- name: the chart renders, on defaults and on every optional branch
run: |
set -euo pipefail
helm template app docker/helm > /dev/null
helm template app docker/helm \
--set serviceMonitor.enabled=true \
--set ingress.enabled=true \
--set roles.replicator.enabled=true \
--set nameOverride=demo > /dev/null
helm template app docker/helm \
--set roles.web.autoscaling.enabled=false \
--set roles.sync.autoscaling.enabled=false \
--set roles.worker.autoscaling.enabled=false > /dev/null
helm template app docker/helm --set migrate.enabled=false > /dev/null
echo "ok: every branch renders"
# The assertion the dead-knob defect needed, and it cannot pass by accident: it reads the
# rendered serviceAccountName off the workloads, which is the field an operator's IRSA
# annotation has to land on. Before 2026-08 both branches rendered app-ultimate and the chart
# emitted its own ServiceAccount either way.
- name: serviceAccount.create and .name are honoured
run: |
set -euo pipefail
refs_of() { grep 'serviceAccountName:' "$1" | awk '{print $2}' | sort -u | paste -sd, -; }
count_sa() { grep -c '^kind: ServiceAccount$' "$1" || true; }
# create: false + a name — the IRSA case. The chart must create nothing, and every
# workload must name the operator's account.
helm template app docker/helm \
--set serviceAccount.create=false --set serviceAccount.name=app-irsa > /tmp/sa-off.yaml
refs="$(refs_of /tmp/sa-off.yaml)"; made="$(count_sa /tmp/sa-off.yaml)"
echo "create=false name=app-irsa -> refs=[${refs}] serviceAccounts=${made}"
if [ "$refs" != "app-irsa" ] || [ "$made" != "0" ]; then
echo "::error title=serviceAccount values not honoured::With serviceAccount.create=false and name=app-irsa the chart rendered serviceAccountName=[${refs}] and ${made} ServiceAccount object(s); it must render exactly [app-irsa] and 0. An operator configuring IRSA otherwise gets the chart's own account and pods with no cloud identity. fix: every workload must resolve the name through the 'ultimate.serviceAccountName' helper in docker/helm/templates/_helpers.tpl, and templates/service.yaml must gate the ServiceAccount on .Values.serviceAccount.create."
exit 1
fi
# create: true + a name — the chart owns the account, under the name it was given.
helm template app docker/helm --set serviceAccount.name=my-sa > /tmp/sa-on.yaml
refs="$(refs_of /tmp/sa-on.yaml)"; made="$(count_sa /tmp/sa-on.yaml)"
echo "create=true name=my-sa -> refs=[${refs}] serviceAccounts=${made}"
if [ "$refs" != "my-sa" ] || [ "$made" != "1" ]; then
echo "::error title=serviceAccount.name not honoured on create::With serviceAccount.create=true and name=my-sa the chart rendered serviceAccountName=[${refs}] and ${made} ServiceAccount object(s); it must render exactly [my-sa] and 1. fix: see the 'ultimate.serviceAccountName' helper in docker/helm/templates/_helpers.tpl."
exit 1
fi
# Every package's own tests and its own coverage bar, each in isolation. No lint — `verify`'s
# `biome check .` is a strict superset of any per-package lint, and a second one cannot fail alone.
#
# This does NOT duplicate `verify`, and the rule at the top of this file is why it has to be
# argued rather than assumed. `verify` answers "does the framework ship" over the whole tree at
# once; this answers two questions it structurally cannot:
#
# 1. WHICH package is below bar. `x verify`'s `unit` step runs every suite in one process, so a
# coverage number over that run is a number for the monorepo, not for a package — and a
# package's own sources read 20-50% there purely because every package it imports is loaded
# and mostly unexercised (`@ultimat3/cache` read 35% while its own `src/` was at 98.8%).
# 2. Whether a suite passes IN ISOLATION. Run together, a package whose tests only pass because
# another package's preload registered something first is invisible. Run alone, it is a
# failure — and `registerFrameworkCatalog`/`registerMailCatalog` were exactly that shape.
#
# ONE job, not a matrix of one per package, and isolation is not what was traded for it:
# `coverage-gate.ts --all` spawns a separate `bun test` process per package, which is the
# isolation, and runs as many at once as the runner has cores. The matrix was 32 jobs — a
# `package-list` job plus one per package — and GitHub bills each job rounded UP to a whole
# minute, so ~9s of checkout-and-install plus a 5s suite cost a minute 32 times per run: 33 of
# the ~40 runner-minutes one push to `main` cost, measured 2026-09-16, for about nine minutes of
# actual work. `--all` reports every package's findings, never only the first to break.
#
# No services, deliberately: `scripts/lib/coverage-pins.ts` explains that the pins are measured
# with no Postgres, no Redis and no NATS so the number is identical on a laptop and on a runner.
# The live paths are `verify`'s `live` step, which does have them.
packages:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: ./.github/actions/setup
- name: every package's tests + coverage, each in its own process, all cores
run: bun run scripts/coverage-gate.ts --all