-
Notifications
You must be signed in to change notification settings - Fork 1
640 lines (618 loc) · 36.1 KB
/
Copy pathci.yml
File metadata and controls
640 lines (618 loc) · 36.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
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
name: ci
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
# Least privilege: the whole workflow is read-only. No job widens this scope.
permissions:
contents: read
# Cancel superseded runs for the same ref so only the latest commit is verified.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Third-party actions are pinned to a full commit SHA (an immutable supply-chain
# anchor — a moved tag cannot inject code); the trailing `# vX.Y.Z` is the
# human-readable version, kept current by Dependabot's github-actions updates.
# The one exception is the org-owned reusable `bymaxone/.github` workflow below,
# pinned to the moving major `@v1` by deliberate convention so a vetted CI fix
# propagates to every repo at once (org-internal, not a third-party dependency).
jobs:
# ─────────────────────────────────────────────────────────────────────────────
# Universal Cargo core — format, clippy, build & test, 100% llvm-cov coverage,
# the MSRV floor build, and the source-gated cargo-mutants run — all via the
# org-wide reusable (bymaxone/.github → rust-ci.yml). Mutation runs only on a push
# to main / manual dispatch, never on PRs. Every bespoke gate below stays here.
# ─────────────────────────────────────────────────────────────────────────────
core:
uses: bymaxone/.github/.github/workflows/rust-ci.yml@v1
with:
run-coverage: true
coverage-fail-under: 100
run-msrv: true
msrv-version: '1.90'
run-mutation: true
# No --all-features here: .cargo/mutants.toml already carries it in
# additional_cargo_args, and cargo rejects the flag twice.
mutation-command: cargo mutants --in-place
# Match .cargo/mutants.toml examine_globs (logic crates only; bindings/** is
# excluded there), so a bindings-only change never triggers the gate for
# code cargo-mutants would not mutate.
mutation-source-globs: '^(crates/)'
# ─────────────────────────────────────────────────────────────────────────────
# Feature matrix — each feature is verified in isolation to catch accidental
# cross-feature dependencies or features that only compile when combined.
# bymax-auth-crypto and the bymax-auth facade both mandate at least one hasher
# (scrypt or argon2) via compile_error!, so they cannot be run feature-by-feature
# (an isolated non-hasher feature would fail the guard); they are checked
# separately with valid combinations, and the rest use cargo-hack --each-feature.
# ─────────────────────────────────────────────────────────────────────────────
feature-matrix:
name: feature matrix (cargo-hack)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: cargo-hack
- name: Hasher-gated crates — valid combinations
run: |
for features in scrypt argon2 'scrypt,argon2' 'scrypt,mfa' 'argon2,mfa' 'scrypt,argon2,mfa'; do
echo "checking bymax-auth-crypto --features $features"
cargo check -p bymax-auth-crypto --no-default-features --features "$features" --locked
done
for features in scrypt argon2 'scrypt,argon2' full; do
echo "checking bymax-auth --features $features"
cargo check -p bymax-auth --no-default-features --features "$features" --locked
done
- name: Workspace feature matrix (each feature in isolation)
run: |
cargo hack check \
--workspace \
--exclude bymax-auth-crypto \
--exclude bymax-auth \
--each-feature \
--locked
# ─────────────────────────────────────────────────────────────────────────────
# Rustdoc — RUSTDOCFLAGS=-D warnings turns broken intra-doc links, malformed
# attributes, and missing backticks into hard errors. --no-deps restricts the
# gate to workspace crates only, not third-party documentation.
# ─────────────────────────────────────────────────────────────────────────────
doc:
name: rustdoc
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps --locked
# ─────────────────────────────────────────────────────────────────────────────
# ts-rs staleness — regenerates the ./shared TypeScript types from the Rust
# source and fails if the committed output has drifted. Skipped automatically
# while bymax-auth-types does not yet declare the ts-export feature.
# ─────────────────────────────────────────────────────────────────────────────
ts-rs-drift:
name: ts-rs staleness
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- name: Check for ts-rs drift
run: |
set -euo pipefail
# Resolve whether bymax-auth-types declares the ts-export feature.
# While that feature is absent, the gate is a no-op.
if cargo metadata --no-deps --format-version 1 2>/dev/null | \
python3 -c "
import sys, json
pkgs = json.load(sys.stdin)['packages']
pkg = next((p for p in pkgs if p['name'] == 'bymax-auth-types'), None)
exit(0 if pkg and 'ts-export' in (pkg.get('features') or {}) else 1)
" 2>/dev/null; then
cargo test -p bymax-auth-types --features ts-export --locked
git diff --exit-code packages/rust-auth/src/shared/
else
echo 'note: ts-export feature not yet present in bymax-auth-types; staleness check skipped.'
fi
# ─────────────────────────────────────────────────────────────────────────────
# WASM purity — the three foundation crates (types, crypto, jwt) must compile on
# wasm32-unknown-unknown, ensuring no tokio / std::net / C FFI leaks onto any
# path. crypto and jwt pull `getrandom`, which refuses to compile on wasm until a
# backend is selected, so they are built with their `wasm-js` (Web Crypto)
# feature. This catches purity regressions before the edge binding is built.
# ─────────────────────────────────────────────────────────────────────────────
wasm-purity:
name: wasm purity (foundation crates)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo build -p bymax-auth-types --target wasm32-unknown-unknown --locked
- run: cargo build -p bymax-auth-crypto --target wasm32-unknown-unknown --features wasm-js --locked
- run: cargo build -p bymax-auth-jwt --target wasm32-unknown-unknown --features wasm-js --locked
# ─────────────────────────────────────────────────────────────────────────────
# WASM binding — the edge binding must compile cleanly as a cdylib for the
# wasm32 target. A successful build here proves the complete wasm-safe stack:
# types → crypto → jwt → wasm binding.
# ─────────────────────────────────────────────────────────────────────────────
wasm-binding:
name: wasm binding build
runs-on: ubuntu-latest
timeout-minutes: 15
needs: wasm-purity
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- run: cargo build -p bymax-auth-wasm --target wasm32-unknown-unknown --locked
# ─────────────────────────────────────────────────────────────────────────────
# wasm-pack — the artifact the npm package actually ships. Builds the JWT-only
# edge binding for the `bundler` target, enforces the gzipped *_bg.wasm size
# budget (≤ 350 KiB — a real, fixed gate, not a placeholder), and runs the wasm
# smoke tests under Node (no browser: the JWT surface is pure compute). The
# exhaustive decision coverage lives in the host-side unit tests gated by the
# coverage job; this proves the emitted wasm artifact works end to end.
# ─────────────────────────────────────────────────────────────────────────────
wasm-pack:
name: wasm-pack (bundler build + node test + size gate)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: wasm-purity
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: wasm-pack
- name: Build the edge wasm (npm bundler target, JWT-only)
run: wasm-pack build bindings/bymax-auth-wasm --target bundler --release --out-dir pkg
- name: Enforce the gzipped wasm size budget
run: bash bindings/bymax-auth-wasm/check-wasm-size.sh
- name: Run the wasm smoke tests under Node
run: wasm-pack test --node bindings/bymax-auth-wasm
# ─────────────────────────────────────────────────────────────────────────────
# Supply chain — three-layer gate:
# cargo-deny : ban-list, license allow-list, RustSec advisories, source policy
# cargo-audit : cross-check Cargo.lock against the RustSec advisory database
# cargo-vet : every dependency audited or trust-imported from vetted orgs
# cargo-vet 0.10.2 is installed from source; the latest release binary (0.10.0
# shipped by taiki-e/install-action) cannot parse the current audit schema.
# ─────────────────────────────────────────────────────────────────────────────
supply-chain:
name: supply-chain (deny / audit / vet)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: cargo-deny,cargo-audit
- run: cargo install --locked cargo-vet@0.10.2
- run: cargo deny check
- run: cargo audit
# `--locked` on purpose: without it `cargo vet` refetches the import sets on every run,
# so the gate answers a question about what upstream happened to publish this minute
# rather than about what this repository recorded and reviewed. That also makes the
# result differ between a networked and an offline run. Refresh deliberately, in a PR
# that shows the diff, rather than implicitly on every CI run.
- run: cargo vet --locked
- name: Dependency budget (per-crate transitive-graph cap)
run: bash scripts/dependency-budget.sh
# ─────────────────────────────────────────────────────────────────────────────
# Security invariants — the § 24 contract expressed as automatable structural
# checks (HS256 pinned, no token in a query string, RustCrypto-only / no ring,
# forbid(unsafe_code) everywhere but the wasm glue, no `.` npm root, the wasm
# binding never a crate, no secret interpolated into a log). A change that weakens
# one fails the build. The dynamic gates (coverage, mutation, property, fuzz)
# protect the same contract at runtime.
# ─────────────────────────────────────────────────────────────────────────────
invariants:
name: security invariants (§24 gate)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: bash scripts/check-invariants.sh
# ─────────────────────────────────────────────────────────────────────────────
# Release readiness — the eight crates pin each other exactly and ship one
# version together with the npm package. Checked on every pull request rather
# than only when a tag is cut: at tag time the only fix is a new tag, because
# crates.io and npm are both append-only.
# ─────────────────────────────────────────────────────────────────────────────
versions:
name: release versions
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
- run: bash scripts/check-release-versions.sh
# ─────────────────────────────────────────────────────────────────────────────
# Public API surface — `cargo public-api` snapshots the exported surface and
# `cargo-semver-checks` gates SemVer compatibility, so a breaking change to the
# published API cannot ship unnoticed. `cargo public-api` builds rustdoc JSON,
# which only the nightly toolchain emits; nightly is installed for this job and
# selected per invocation with `cargo +nightly` (the tool has no toolchain flag —
# `+toolchain` is the only supported selector).
# ─────────────────────────────────────────────────────────────────────────────
public-api:
name: public API (public-api + semver-checks)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
- name: Install the nightly toolchain (rustdoc JSON for public-api)
run: rustup toolchain install nightly --profile minimal --no-self-update
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: cargo-public-api,cargo-semver-checks
- name: Snapshot the public API of every published crate
# An informational snapshot of the exported surface — it must not fail on
# ordinary output. RUSTFLAGS is cleared for this step: the job-wide
# `-D warnings` (set by setup-rust-toolchain) would promote benign nightly
# rustdoc-JSON build lints to hard errors and break the metadata build.
env:
RUSTFLAGS: ""
run: |
set -euo pipefail
for crate in bymax-auth-types bymax-auth-jwt bymax-auth-crypto \
bymax-auth-redis bymax-auth-client bymax-auth-core \
bymax-auth-axum bymax-auth; do
echo "::group::public-api $crate"
cargo +nightly public-api -p "$crate" --all-features
echo "::endgroup::"
done
- name: SemVer compatibility check
# Non-blocking until the first publish: cargo-semver-checks diffs the current
# API against a baseline from crates.io, and nothing is published yet (the
# release pipeline is deferred — see docs/RELEASE.md), so there is no baseline
# to diff against. We still build the rustdoc and surface any findings as an
# informational log. Once the first version is published this becomes a hard
# gate: drop `continue-on-error` so a SemVer-breaking change fails the build.
continue-on-error: true
env:
RUSTFLAGS: ""
run: cargo semver-checks --workspace --all-features
# ─────────────────────────────────────────────────────────────────────────────
# Fuzz smoke — a deterministic COMPILE-ONLY smoke over the trust-boundary parsers
# (JWT decode, Base32 TOTP secret, PHC password string). `cargo fuzz build`
# compiles all three libFuzzer targets against the real crate APIs but never runs
# them, which proves the harnesses stay valid as the surface evolves without the
# flakiness of hosting libFuzzer on a shared runner. The exhaustive "no panic on
# adversarial input" guarantees are covered by the property tests in the coverage
# job; this gate keeps the fuzz targets buildable. cargo-fuzz needs nightly.
#
# The target is pinned to the host gnu triple: on some nightlies `cargo fuzz`
# defaults to `x86_64-unknown-linux-musl`, which is statically linked (the ASan
# instrumentation rejects a static libc) and is not installed on the runner.
# Building for `x86_64-unknown-linux-gnu` sidesteps both problems deterministically.
# ─────────────────────────────────────────────────────────────────────────────
fuzz-smoke:
name: fuzz smoke (time-boxed)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install the nightly toolchain (libFuzzer instrumentation)
run: rustup toolchain install nightly --profile minimal --no-self-update
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: "fuzz -> fuzz/target"
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: cargo-fuzz
- name: Compile every fuzz target (build-only smoke)
run: cargo +nightly fuzz build --target x86_64-unknown-linux-gnu
# ─────────────────────────────────────────────────────────────────────────────
# npm package — the @bymax-one/rust-auth frontend package. Rebuilds the edge wasm
# from source (the wasm/ output is gitignored), then builds the dual ESM + CJS
# bundles, typechecks every subpath, and runs the Vitest suite (which verifies a
# real backend-signed HS256 token against the same wasm the package ships).
# ─────────────────────────────────────────────────────────────────────────────
npm:
name: npm package (build + typecheck + test)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: wasm-pack
- name: Install npm dependencies
working-directory: packages/rust-auth
run: npm ci
- name: Build the edge wasm (bundler target)
working-directory: packages/rust-auth
run: npm run build:wasm
- name: Build the dual ESM + CJS bundles
working-directory: packages/rust-auth
run: npm run build
- name: Typecheck every subpath
working-directory: packages/rust-auth
run: npx tsc --noEmit
- name: Type resolution across module systems
working-directory: packages/rust-auth
# attw resolves every subpath the way each module system does, against the
# packed tarball rather than the source tree. The `tsc --noEmit` above compiles
# `src` and never reads the `exports` map, so it cannot see a CommonJS consumer
# being handed the ESM declarations.
run: npm run check:exports
- name: Lint the kept TypeScript layers
working-directory: packages/rust-auth
run: npm run lint
- name: Validate the TypeDoc API surface renders
working-directory: packages/rust-auth
run: npx typedoc --emit none
- name: Run the Vitest suite under the coverage ratchet
working-directory: packages/rust-auth
# Thresholds live in vitest.config.ts and are pinned just under what the suite
# currently reaches, so this layer's coverage can only go up. The Rust crates are
# gated at 100% separately by the reusable workflow above.
run: npm run test:cov
# ─────────────────────────────────────────────────────────────────────────────
# Examples — the official examples form their OWN Cargo workspace (and their own
# npm packages), so they are never measured by the coverage gate. They are built
# and linted here so a contract change that breaks an example fails CI. The
# frontend examples consume the npm package by `file:` path, so the package's
# dist/ + wasm/ are built first. The examples never contact external services.
# ─────────────────────────────────────────────────────────────────────────────
examples:
name: examples (build + lint)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
components: clippy
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: ". -> target\nexamples -> examples/target"
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: wasm-pack
# Rust examples — their own workspace; build + clippy-as-error over all targets.
- name: Build the Rust examples
working-directory: examples
run: cargo build --locked
- name: Lint the Rust examples
working-directory: examples
run: cargo clippy --all-targets --locked -- -D warnings
# The npm package the frontend examples consume must exist on disk first.
- name: Build the npm package (dist + wasm)
working-directory: packages/rust-auth
run: |
npm ci
npm run build:wasm
npm run build
# react-vite — typecheck + production build against the built package.
- name: Build the react-vite example
working-directory: examples/react-vite
run: |
npm ci --no-audit --no-fund
npm run build
# nextjs — `next build` type-checks and compiles every route + the middleware.
- name: Build the nextjs example
working-directory: examples/nextjs
env:
AUTH_ACCESS_TOKEN_SECRET: an-edge-ci-secret-key-0123456789abcdef
AUTH_BACKEND_URL: http://127.0.0.1:8080
run: |
npm ci --no-audit --no-fund
npm run build
# ─────────────────────────────────────────────────────────────────────────────
# Dogfood smokes — non-publishing validation of the to-be-shipped surface.
# crate smoke: boots the Axum router over a real testcontainers Redis and drives
# register -> login -> /me -> refresh -> logout via the native client.
# npm smoke : verifies a backend-signed HS256 token at the edge through the
# shipped wasm (server/edge parity) against the built package.
# The GitHub-hosted Ubuntu runner ships Docker, so the crate smoke runs a real Redis.
# ─────────────────────────────────────────────────────────────────────────────
dogfood:
name: dogfood smokes (crate + npm)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: ". -> target\nexamples -> examples/target"
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: wasm-pack
# Crate smoke — boots the router over a real Redis (testcontainers) and asserts
# the happy path end to end.
- name: Crate smoke (Axum + real Redis)
working-directory: examples
run: cargo test -p smoke-crate --test smoke --locked -- --nocapture
# npm smoke — build the package, then verify edge HS256 parity against the wasm.
- name: Build the npm package (dist + wasm)
working-directory: packages/rust-auth
run: |
npm ci
npm run build:wasm
npm run build
- name: npm smoke (edge HS256 parity)
working-directory: examples/smoke-npm
run: |
npm ci --no-audit --no-fund
npm run typecheck
npm run smoke
# ─────────────────────────────────────────────────────────────────────────────
# Browser E2E (Playwright) — drives a real Chromium (Playwright manages its own
# headless browser, no system browser needed) through login -> protected request ->
# silent refresh -> logout against the Next.js example served in front of the Rust
# backend (Axum over a testcontainers Redis). It asserts the Next middleware
# edge-verifies (via WASM) a token the backend signed — server/edge JWT parity in a
# real browser. The harness starts Redis + the backend; Playwright's webServer
# starts the built Next.js app.
# ─────────────────────────────────────────────────────────────────────────────
e2e-browser:
name: browser e2e (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 40
env:
AUTH_E2E_SECRET: an-e2e-edge-hs256-secret-key-0123456789ab
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: ". -> target\nexamples -> examples/target"
- uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2.85.7
with:
tool: wasm-pack
# Build the backend binary up front so the harness spawn does not pay for a cold
# compile inside the test timeout.
- name: Pre-build the e2e backend
working-directory: examples
run: cargo build -p e2e-backend --locked
# Build the npm package the Next.js example consumes.
- name: Build the npm package (dist + wasm)
working-directory: packages/rust-auth
run: |
npm ci
npm run build:wasm
npm run build
# Build the Next.js example (production server is what the E2E drives).
- name: Build the nextjs example
working-directory: examples/nextjs
env:
AUTH_ACCESS_TOKEN_SECRET: ${{ env.AUTH_E2E_SECRET }}
AUTH_BACKEND_URL: http://127.0.0.1:8090
run: |
npm ci --no-audit --no-fund
npm run build
# Install Playwright's own chromium (with OS deps) and run the suite. The harness
# starts Redis (testcontainers) + the backend; Playwright starts the Next server.
- name: Install Playwright chromium
working-directory: examples/e2e
run: |
npm ci --no-audit --no-fund
npx playwright install --with-deps chromium
- name: Run the browser E2E
working-directory: examples/e2e
run: npm test
- name: Upload the Playwright report on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: |
examples/e2e/playwright-report/
examples/e2e/test-results/
retention-days: 7
# Single check the org ruleset can require, in every repo, under one name.
#
# A required status check is matched by literal name, and this repo's jobs are
# named nothing like the next repo's — so there is no list of contexts that
# works org-wide. One aggregate per repo, always spelled `CI passed`, is what
# makes a single rule at the organisation level possible, and what makes every
# repository created from now on inherit the gate without being configured.
#
# `always()` is what makes this a gate rather than decoration: without it the
# job is skipped the moment a dependency fails, and a skipped check reports
# neutral — the pull request would look unblocked precisely when it is broken.
# `cancelled` is named explicitly for the same reason; a cancelled job is not
# a passing one, and a queue timeout produces exactly that.
#
# A skipped dependency is accepted on purpose: jobs here are conditional on the
# event, and failing on skip would break every push that legitimately runs a
# subset.
ci-pass:
name: CI passed
if: always()
needs:
- core
- feature-matrix
- doc
- ts-rs-drift
- wasm-purity
- wasm-binding
- wasm-pack
- supply-chain
- invariants
- versions
- public-api
- fuzz-smoke
- npm
- examples
- dogfood
- e2e-browser
runs-on: ubuntu-latest
# Once this is the required check, a hung runner holds every merge behind it
# for GitHub's default of six hours. The job is one comparison, so a short
# ceiling costs nothing and bounds that.
timeout-minutes: 5
steps:
# Printed on success as well as failure. Without it the check reports a bare
# red X and the reader has to open each dependency to find which one broke.
- name: Report the result of every dependency
env:
NEEDS: ${{ toJSON(needs) }}
run: |
{
echo "| Job | Result |"
echo "| --- | --- |"
echo "$NEEDS" | jq -r 'to_entries[] | "| \(.key) | \(.value.result) |"' | sort
} | tee -a "$GITHUB_STEP_SUMMARY"
# Named for what the condition does: `skipped` is deliberately not a failure
# here, so "did not succeed" would describe a stricter gate than this is.
- name: Fail when any dependency failed or was cancelled
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1