-
Notifications
You must be signed in to change notification settings - Fork 69
440 lines (427 loc) · 20.8 KB
/
Copy pathci.yml
File metadata and controls
440 lines (427 loc) · 20.8 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
name: CI
# Runs the full test pyramid on every PR into main and on every push to
# main. The three layers run as separate jobs so a failure names the
# layer that broke. Mark all three as required status checks in the
# branch-protection rule for main so a PR can only merge when unit,
# browser, AND e2e are green (not just unit). See the repo's branch
# protection settings, or run scripts/protect-main.sh.
#
# Free on public repos (ubuntu-latest has unlimited Actions minutes).
on:
pull_request:
branches: [main]
push:
branches: [main]
# A newer push to the same branch cancels the older in-flight run.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
conventions:
name: Conventions (webjs check)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
# Dogfood the framework's own correctness checks on the in-repo apps.
- name: webjs check (blog, website, docs, ui website)
run: |
for app in examples/blog website docs packages/ui/packages/website; do
echo "::group::webjs check $app"
( cd "$app" && node "$GITHUB_WORKSPACE/packages/cli/bin/webjs.js" check )
echo "::endgroup::"
done
- name: Framework runtime packages are buildless (no .ts source)
run: |
# Invariant: packages/{core,server,cli} and packages/editors/* are
# plain .js + JSDoc. .ts is allowed in examples/docs/website and in
# scaffold templates / the ui registry (those ship to user apps).
hits=$(git ls-files 'packages/core/**/*.ts' 'packages/server/**/*.ts' 'packages/cli/**/*.ts' 'packages/editors/**/*.ts' \
| grep -vE '\.d\.ts$|/templates/' || true)
if [ -n "$hits" ]; then
echo "::error::TypeScript source in a buildless framework package (invariant violated):"; echo "$hits"; exit 1
fi
- name: No em-dash in source (invariant 11)
run: |
# U+2014 is banned repo-wide for webjs-AUTHORED prose. changelog/ is
# generated from commit history (pre-rule entries) so it is excluded,
# and .claude/skills/ holds vendored agent skills (e.g. the
# Anthropic-authored use-railway), which are external content not
# subject to our prose style, the same as node_modules.
hits=$(git grep -lP "\x{2014}" -- '*.js' '*.ts' '*.md' ':!changelog/' ':!**/node_modules/**' ':!.claude/skills/**' ':!.agents/skills/**' || true)
if [ -n "$hits" ]; then
echo "::error::em-dash (U+2014) found; replace per AGENTS.md invariant 11:"; echo "$hits"; exit 1
fi
unit:
name: Unit + integration (node --test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
# Some integration tests boot the blog example in-process (e.g. the
# differential elision test renders its mixed `/` page), which calls
# listPosts() and needs a migrated SQLite DB, the same setup the e2e job
# does.
- name: Prepare the blog example database
working-directory: examples/blog
run: |
cp .env.example .env
npm run db:migrate
npm run db:seed
- run: npm test
bun:
name: Bun runtime smoke + test matrix (#508, #509, #511)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
# npm ci installs the workspace + the `amaro` optionalDependency that the
# Bun stripper backend needs.
- run: npm ci
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Boot a webjs app under Bun: SSR + TypeScript strip (via amaro, since Bun
# has no built-in stripper) + a server-action RPC round-trip.
- name: webjs runtime smoke on Bun
run: bun test/bun/smoke.mjs
# Boot the full LISTENER on the Bun.serve shell (#511): SSR + a route.ts GET
# + the SSE live-reload stream + a WebSocket WS-export echo over a real
# socket. The same assertions run on the node:http shell under `npm test`,
# so this is the cross-shell parity proof.
- name: webjs listener parity on Bun
run: bun test/bun/listener.mjs
# Listener overhead reductions on Bun (#756): the out-of-band IP stamp (no
# Request clone), the buffered sync-compression fast path, the streamed-head
# non-blocking classifier, and the basePath rebuild spoof guard. Run as the
# plain `.mjs` script (not via `bun test`) so it is not subject to bun test's
# per-test timeout while it boots two servers + does a deliberate 400ms stall.
- name: webjs listener overhead on Bun
run: bun test/bun/listener-overhead.mjs
# FileStore streaming on Bun (#509): put/get round-trip + the
# no-orphan-on-mid-stream-error invariant (the Readable.fromWeb->reader-loop
# fix, which Readable.fromWeb hangs on under Bun).
- name: webjs FileStore streaming on Bun
run: bun test/bun/file-storage.mjs
# Compression on the Bun.serve shell (#517): brotli served via node:zlib
# (the web CompressionStream had no brotli), plus the no-hang-on-mid-stream-
# error guard (the body is fed through a reader-loop + pipeline, not
# Readable.fromWeb, which hangs on Bun).
- name: webjs compression on Bun
run: bun test/bun/compression.mjs
# Dev hot reload on Bun (#514): start `webjs dev` under Bun, edit a
# re-imported route module, and assert the response updates with NO manual
# restart. The CLI re-execs under `bun --hot` on Bun (vs `node --watch` on
# Node); without it Bun ignores the dev `?t=` cache-bust and the edit stays
# stale. The same script proves no Node regression under `npm test`.
- name: webjs dev hot reload on Bun
run: bun test/bun/dev-hot-reload.mjs
# Dev live-reload of a webjs.dev.watch dir OUTSIDE the appDir (#894) and
# the SSE retry hint on the Bun.serve shell (#893). Plain scripts (no
# per-test timeout) since each boots the real CLI + polls for readiness;
# the node:test integration versions are node:http-listener-specific and
# denylisted in run-bun-tests.js.
- name: webjs dev extra-watch + reload-retry on Bun
run: |
bun test/bun/dev-extra-watch.mjs
bun test/bun/dev-reload-retry.mjs
# The app-source deploy signal (#899) is derived from an fs source walk +
# a node:crypto digest, so it must be byte-identical on the Bun.serve path.
- name: App-source deploy signal on Bun
run: bun test/bun/app-source-signal.mjs
# Boot website (which serves /docs) + ui-website on Bun and GET real
# routes (#542). Every in-repo app deploys on Bun, but only examples/blog
# had a Bun boot in CI, so a per-route break only on Bun (the #526
# ui-website 500) could reach production. The script runs each app's
# webjs.start.before presteps (the ui-website registry copy is the #526
# root cause) and probes an ui-website component detail page. Node runs it
# too in the "In-repo app tests" job.
- name: App boot-check on Bun (website incl. /docs + ui-website)
run: bun test/bun/app-boot.mjs
# SSR action-result seeding on Bun (#529): seeding rode Node's
# module.registerHooks, which Bun lacks; it now installs via a Bun.plugin
# onLoad, so a shipping async component seeds during SSR (the __webjs-seeds
# block) and does NOT re-fetch on hydration. The same script proves the Node
# install under `npm test`.
- name: webjs action seeding on Bun
run: bun test/bun/seed.mjs
# Blog Drizzle DB round-trip on Bun (#551/#563): the connection picks
# bun:sqlite + drizzle-orm/bun-sqlite on Bun (vs node:sqlite on Node),
# so prove the blog's real schema round-trips an insert().returning(), a
# timestamp_ms Date column, and a relational read on the Bun driver. The
# same script proves the Node path under `npm test`.
- name: webjs blog DB round-trip on Bun
run: bun test/bun/blog-db.mjs
# # path-alias resolution on Bun (#555): the alias is Node's native
# package.json "imports" field; Bun must resolve the same
# `#*` catch-all key (a `#/`-prefixed key does NOT resolve on Bun, which is
# why the scaffold ships the slash-free `#*` form). The same script proves the Node path
# under `npm test`.
- name: "webjs # path alias on Bun"
run: bun test/bun/path-alias.mjs
# Server timeout wiring on Bun (#663): the node:http requestTimeout /
# headersTimeout / keepAliveTimeout map onto Bun.serve's single idleTimeout
# (#511); this asserts startBunListener feeds the mapped value into
# Bun.serve. The pure mapping is also unit-tested under the matrix below.
- name: webjs server timeouts on Bun
run: bun test/bun/timeouts.mjs
# Template binding-prefix dispatch on Bun (#784): the renderers now read
# the prefix set from core's single-sourced BINDING_PREFIXES and dispatch
# on kind; this asserts @event drops and .prop / ?bool round-trip the same
# on the Bun SSR path (buffered and streamed) as on Node.
- name: webjs binding-prefix dispatch on Bun
run: bun test/bun/binding-prefixes.mjs
# Reverse-proxy forwarded headers on Bun (#1090): the node shell corrects
# the request url via urlFromRequest before building its Request, but the
# Bun shell handed Bun.serve's request straight through, so behind a
# TLS-terminating proxy every absolute URL an app derived came out http://
# (webjs.dev served an http:// og:image). This asserts a page's ctx.url AND
# a route handler's raw req.url both carry the forwarded scheme + host.
- name: Forwarded proto/host on Bun
run: bun test/bun/forwarded-proto.mjs
# Root middleware resolution on Bun (#1098). A root `middleware.ts` was
# never loaded at all: the lookup was the single literal `middleware.js`,
# with no error to notice. The proof is a module-LOAD path (a bare
# import() of a .ts file), and the TS strip differs per runtime, so it
# needs its own step rather than riding the Node suite.
- name: Root middleware resolution on Bun
run: bun test/bun/root-middleware.mjs
# Light-DOM slot SSR projection on Bun (#1021): slot substitution
# (injectDSD / substituteSlotsInRender) is on the SSR hot path, so the
# projection must be byte-consistent across runtimes: authored children
# land in their named + default slots, an unmatched slot falls back, and
# the data-webjs-light + data-projection markers are emitted. The same
# script proves the Node path under `npm test`.
- name: Light-DOM slot SSR projection on Bun
run: bun test/bun/slot-ssr-parity.mjs
# SQLite busy_timeout on Bun (#673): bun:sqlite (like node:sqlite) defaults
# busy_timeout to 0, so a contended write throws `database is locked`; the
# connection tune sets busy_timeout=5000 + WAL. This asserts the bug
# condition and the fix on the Bun driver. The same script proves the Node
# path under `npm test`.
- name: webjs SQLite busy_timeout on Bun
run: bun test/bun/sqlite-busy-timeout.mjs
# The Bun test MATRIX (#509): run the runtime-sensitive node:test suite
# (core + server + cross-package test/) under Bun, file by file, classifying
# each result. Documented Node-only files + Bun-test-runner-quirk files are
# skipped with a reason; genuine Bun failures fail the job. This is what
# catches the long tail of cross-runtime incompatibilities.
- name: webjs Bun test matrix
run: node scripts/run-bun-tests.js
db-postgres:
name: Postgres prod-engine round-trip (#563)
runs-on: ubuntu-latest
# The cross-database Drizzle abstraction (#563) makes the schema, queries,
# and actions portable, but migrations + runtime behavior are per-dialect.
# The rest of CI runs on SQLite, so this job proves the SAME unified schema
# round-trips on a REAL Postgres engine (a service container), the
# dev-SQLite / prod-Postgres workflow the abstraction promises.
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: webjs_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
# The `pg` driver is installed only in THIS job (not a root devDependency),
# so it stays out of the shared package-lock. --no-save keeps the lock
# untouched; this job does not run the type fixtures, so the extra install
# cannot affect them.
- run: npm install --no-save pg@^8.13.0
- name: Postgres round-trip (unified schema on the prod engine)
env:
WEBJS_PG_URL: postgres://postgres:postgres@localhost:5432/webjs_test
run: node --test test/pg/pg-roundtrip.test.mjs
browser:
name: Browser (web-test-runner / Playwright)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
- name: Install Playwright browsers (Chromium, Firefox, WebKit)
run: npx playwright install --with-deps chromium firefox webkit
- run: npm run test:browser
e2e:
name: E2E (Puppeteer against the blog example)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
- name: Install Chromium for Puppeteer
run: npx playwright install --with-deps chromium
- name: Resolve the Chromium binary path
run: echo "CHROMIUM_PATH=$(node -e "console.log(require('playwright-core').chromium.executablePath())")" >> "$GITHUB_ENV"
- name: Prepare the blog example database
working-directory: examples/blog
run: |
cp .env.example .env
npm run db:migrate
npm run db:seed
- name: Run e2e
env:
WEBJS_E2E: '1'
run: node --test test/e2e/e2e.test.mjs
# Browser-test harness (#806): spawns real wtr with the shipped scaffold
# config against a fixture app whose browser test imports a real .ts
# component that imports a 'use server' action, and asserts it loads in
# Chromium. Verifies webjs test --browser end to end.
- name: Run browser-harness e2e (#806)
env:
WEBJS_E2E: '1'
run: node --test test/e2e/browser-harness.test.mjs
# Touch-emulation e2e for interactive Tier-2 ui components (#745/#747):
# boots the ui-website and taps hover-card / dropdown-submenu / sonner
# under a Chromium iPhone context (faithful touch events, no real device).
- name: Run ui touch e2e
run: npm run test:e2e:touch --workspace=@webjsdev/ui
# Cross-runtime e2e (#523), split into its OWN job (#774) so it runs in
# PARALLEL with the Node-served e2e above instead of as a trailing step
# (which serialized the two and ~doubled the e2e critical path). Re-runs the
# SAME suite under node --test (its node:test hook lifecycle does not survive
# `bun test`) but with the blog SERVED on Bun (WEBJS_E2E_RUNTIME=bun spawns
# the blog under the bun binary), proving the Bun.serve shell + Drizzle-on-Bun
# in a real browser. The few node-only assertions (SSR seeding, #472/#488) and
# the #528-blocked abort test skip themselves on Bun.
#
# NOTE: the required-status-check gate on `main` is the Node job above
# ("E2E (Puppeteer against the blog example)"); this Bun job is an additional
# parallel signal. To also gate merges on it, add its name to
# branches/main/protection required_status_checks (admin op, see
# scripts/protect-main.sh).
e2e-bun:
name: E2E (blog served on Bun)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
- name: Install Chromium for Puppeteer
run: npx playwright install --with-deps chromium
- name: Resolve the Chromium binary path
run: echo "CHROMIUM_PATH=$(node -e "console.log(require('playwright-core').chromium.executablePath())")" >> "$GITHUB_ENV"
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Prepare the blog example database
working-directory: examples/blog
run: |
cp .env.example .env
npm run db:migrate
npm run db:seed
- name: Run e2e with the blog served on Bun
env:
WEBJS_E2E: '1'
WEBJS_E2E_RUNTIME: 'bun'
run: node --test test/e2e/e2e.test.mjs
dist:
name: Build (@webjsdev/core dist)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
# The published @webjsdev/core ships pre-built dist/ bundles (built
# by the prepare hook at publish). Build them in CI so a bundling
# break is caught on the PR, not at release time.
- run: npm run build:dist --workspace=@webjsdev/core
apps:
name: In-repo app tests (all four apps)
runs-on: ubuntu-latest
# The framework jobs above cover packages/* and the root cross-package
# suite. This job runs each IN-REPO app's OWN test suite (its `webjs test`
# script), which the root runners do not discover, so a regression in an
# app's tests gates the merge (issue #342). The website's `test` runs both
# its node + browser suites (hence Playwright), and covers the docs too
# since they are its own /docs routes; the blog is node-only and touches
# its SQLite DB (the same setup the unit + e2e jobs do). The ui-website
# ships no `webjs test` suite, so it is covered by the
# `node test/bun/app-boot.mjs` boot-check step below (#627), which boots it
# via createRequestHandler and asserts it serves real routes with no
# broken modulepreload; the same script runs on Bun in the `bun` job (#542).
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: npm
- run: npm ci
- name: Install Playwright Chromium (for the website browser tests)
run: npx playwright install --with-deps chromium
- name: Prepare the blog example database
working-directory: examples/blog
run: |
cp .env.example .env
npm run db:migrate
npm run db:seed
- name: website tests (node + browser)
run: npm test --workspace=@webjsdev/website
- name: blog tests (node)
run: npm test --workspace=@webjsdev/example-blog
# The ui-website ships no `webjs test` suite (#627): boot the non-blog
# apps on Node and assert each serves real routes with no broken
# modulepreload, including the website's /docs routes. Runs the apps'
# `webjs.start.before` presteps first (the ui-website registry copy). The
# blog is covered by the e2e job. The docs.webjs.dev redirect host is not
# here on purpose: every route on it is an empty 301, which would pass
# vacuously; it is covered by test/docs/docs-host-redirect.test.mjs.
- name: App boot-check on Node (website incl. /docs + ui-website)
run: node test/bun/app-boot.mjs
docker:
name: Docker image build (the deploy artifact)
runs-on: ubuntu-latest
# The jobs above run the apps in-process via createRequestHandler; NONE of
# them build the Docker image that every Railway service actually deploys.
# That gap let #404's package reorg ship a stale `COPY packages/ts-plugin`
# path that broke all four live deploys for a day (#409): a COPY of a
# missing source is a hard Docker error, invisible to every in-process
# check. Building the image here catches that class (stale COPY paths, a
# broken npm install / dist / tailwind / db-migrate step in the image) on the
# PR instead of at deploy time. Build only, no push.
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v3
- name: Build the monorepo image
uses: docker/build-push-action@v6
with:
context: .
push: false
cache-from: type=gha
cache-to: type=gha,mode=max