Repository navigation
698 lines (681 loc) · 37 KB
/
Copy pathci.yml
File metadata and controls
698 lines (681 loc) · 37 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
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
name: CI
on:
push:
branches: [main]
pull_request:
# Manual full run. The maxplayer-trade jobs always run on push and dispatch; only pull requests
# are filtered (see trade-changes).
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
maxplayer-chat:
name: maxplayer-chat sidecar
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Test maxplayer-chat (in-process relay only)
run: cargo test -p maxplayer-chat --locked
# Default-feature builds + the canonical core test suite (AGENTS.md "Build & test").
build-default:
name: Build & test (default features)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build workspace (default features)
run: cargo build --workspace --locked
# The release workflow is the only other caller of this script, and it runs on a tag — so
# until now the script itself was unverified code on every PR that changed what it checks,
# and a broken release verifier reached main invisibly (#278, #262). The binary argument is
# the load-bearing part: the artifact leg is the only one that inspects a built executable
# rather than the tree, and it is the leg that actually failed. A tree-only invocation here
# would be a green row blind to exactly the class of failure this guards.
- name: Verify the release version check agrees with the built binary
run: |
VERSION="$(cargo metadata --no-deps --format-version 1 \
| node -e 'const m=JSON.parse(require("fs").readFileSync(0,"utf8"));const p=m.packages.find(p=>p.name==="maxplayer");if(!p){process.stderr.write("no maxplayer package in cargo metadata\n");process.exit(1)}process.stdout.write(p.version)')"
./scripts/verify-release-version.sh "$VERSION" target/debug/maxplayer
# One half of a pair — this build has `acp` off, and the acp job below runs the other verifier
# on a build that has it on. Since #510 this is the ONLY place the racer verifier runs: a
# release ships one binary with `acp` in, so nothing else exercises the buyer-only feature set.
# It stays because that feature set is still supported from source and must keep compiling and
# keep being acp-free, and because a surface verifier that has never run is not a guard.
- name: Verify the racer surface (default features)
run: ./scripts/verify-racer-surface.sh target/debug/maxplayer
- name: Test maxplayer-core (default features)
run: cargo test -p maxplayer-core --locked
# The CLI crate's tests (cli unit tests, cli_e2e.rs, and the #126/#127/#131 daemon self-spawn
# acceptance in mcp_daemon.rs, which drives the real built binary) run nowhere else: every
# other test invocation in this file is -p maxplayer-core (#264). mcp_daemon.rs is gated on the
# wallet feature, which is a default feature, so the default-feature job is the one that
# satisfies its cfg gate.
- name: Test maxplayer (CLI crate, default features)
run: cargo test -p maxplayer --locked
# Same gap, other crate (#264): the relay write-policy plugin's tests (the #372 config-flip
# red-prove, #397 NIP-17 admission) are built by the workspace build but named by no test
# invocation. The crate has no feature gates, so it runs here too.
- name: Test maxplayer-relay-write-policy
run: cargo test -p maxplayer-relay-write-policy --locked
# The acp feature gates the seller-execute path and is off by default, so it rots silently unless
# CI compiles it explicitly (issue #108). Build BOTH crates with acp on — maxplayer-core with the full
# shipped feature combo — then run the acp-gated tests.
build-acp:
name: Build & test (acp)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build maxplayer (acp + wallet, release)
run: cargo build -p maxplayer --release --features acp,wallet --locked
- name: Build maxplayer-core (full shipped feature combo)
run: cargo build -p maxplayer-core --features acp,gateway,git-delivery,wallet --locked
# The acp-gated tests (the ACP driver units + the #223 seller-concurrency regression:
# N jobs must OVERLAP on one LocalSet thread and the loop must stay live) only exist under
# the acp feature — the default-feature job above compiles them out, so run them here.
- name: Test maxplayer-core (acp)
run: cargo test -p maxplayer-core --features acp --locked
# The CLI crate's acp-gated tests run NOWHERE else: the default-feature job tests -p maxplayer
# WITHOUT acp (so `#[cfg(feature = "acp")]` tests compile out), and the acp steps here only
# BUILD -p maxplayer or TEST -p maxplayer-core (#264, same gap). Without this the seller-surface
# CLI locks are a false green — they compile but never execute (#549): the `seller --help`
# regression, the #542 no-alias foil, and the #533 doctor-wording pin. acp_smoke is `#[ignore]`d
# (env-gated real agent), so it is reported ignored here, not run.
- name: Test maxplayer (CLI crate, acp)
run: cargo test -p maxplayer --features acp,wallet --locked
# Paired with the racer check in the default-feature job: the two scripts must reach opposite
# verdicts on the two builds, which is what shows each one moves with the feature rather than
# with something incidental to the binary it happens to be handed.
- name: Verify the seller surface (acp)
run: ./scripts/verify-seller-surface.sh target/release/maxplayer
# `acp` and `wallet` are never both on in any `cargo test` in this file, so a test gated on BOTH is
# run by nothing. Every other row moves in ONE direction from that intersection: the acp job above
# has `acp` WITHOUT `wallet`, and money-path has `wallet` (which composes `gateway` and
# `git-delivery`) WITHOUT `acp`. build-acp already declares this exact combo as "the full shipped
# feature combo" — but as `cargo build`, and `cargo build` compiles no test target. So the combo was
# type-checked and never executed, and nothing in the check list could say so: a compiled-out test
# and a passing test produce the same green.
#
# The intersection is not an odd corner. `wallet` gates `seller_exec`, `acp` gates its
# agent-execution half, and what lives there is the credential-containment surface.
#
# MEASURED at 271186bd with `cargo test … -- --list`, which enumerates test names and executes no
# test body. This row lists 1053 names; the three existing maxplayer-core rows list 295 (acp), 276
# (default) and 1035 (money-path), 1054 together. FOUR names are in this row and in no existing row:
# seller_exec::tests::contained_launch_keeps_every_real_credential_out_of_the_container_view
# seller_exec::tests::todays_forwarding_leaks_every_real_credential_into_the_container_view
# seller_exec::tests::docker_launch_without_containment_opens_no_pinhole
# seller_exec::tests::response_timeout_is_classified_by_its_typed_timer_source
# The other rows cannot go RED on those four. The tests are ABSENT from their binaries, not passing
# in them — which is a stronger statement than "they happen to be green".
#
# RED-PROVED before this job was written, which is the only thing that makes it a guard rather than
# a row that has never failed. Invert the substitution guard in `seller_exec::contain_env_values` so
# a real credential is never replaced — a semantic defect, not a compile error, so no `#[cfg]` can
# mask it:
# this row rc=101, 1041 passed / 1 failed
# (contained_launch_keeps_every_real_credential_out_of_…)
# money-path's features, same tree rc=0, 1021 passed / 0 failed
# The control is the point: the broken function is `acp`-gated, so money-path does not compile it
# and cannot report it. One row red, one row green, same defect.
#
# This row is NOT a superset of money-path and must not replace it. FIVE names run today that this
# row does not carry: the remaining `live-mints` test, and
# `seller_exec::tests::agent_run_fail_closed_without_acp_feature`, which exists only under
# `not(feature = "acp")`. The two rows are complementary by construction, in both directions.
#
# `live-mints` is deliberately absent here. It stays exclusive to money-path (#720), the only row
# permitted to reach a live third-party mint, so this row adds NO network money reach.
#
# Release, so the git-delivery suite runs the way money-path runs it: that job documents a
# debug-only transport-lifecycle bug (#152) as its reason for release-only, and this row inherits
# the constraint rather than re-litigating it.
#
# Its OWN job, for the reason money-path has its own: this row carries money teeth, because
# `wallet` composes `git-delivery`. A step queued behind a failed step is reported `skipped` inside
# a job whose conclusion is `failure`, which cannot distinguish a suite that failed from a suite
# that never ran — the one ambiguity a row like this must not have.
#
# build-acp's build step stays. It compiles this combo in DEBUG and this row is release, so neither
# covers the other's profile.
feature-union:
name: Test the full shipped feature combo (acp + wallet)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Test maxplayer-core (acp + gateway + git-delivery + wallet)
run: cargo test -p maxplayer-core --release --features acp,gateway,git-delivery,wallet --locked
# The bare feature set — `wallet` off, so `acp` and the seller surface are off with it. NO other
# job compiles it: build-default builds `--workspace` with default features (wallet ON) and
# build-acp turns MORE features on, never fewer. Every job in this file moves in one direction
# from the default, so the floor of the feature graph was never built.
#
# It regressed the moment it was left unwatched. #203 closed on the reasoning that the build was
# fine — correct when it was made — and 86 minutes later ef3ff86 (#658) merged an ungated
# `use maxplayer_core::job_lifecycle` into mcp.rs, reaching a module gated on `wallet`. That is a
# compile error in this feature set and a no-op in every other, so it reached main and sat there.
# The defect was never the import: it was that no row in this file could go red on it.
#
# RED-PROVED before this job was written, which is the only thing that makes it a guard rather
# than a row that has never failed: `cargo build -p maxplayer --no-default-features --locked`
# gives E0432 at ae31bf85 and exits 0 at 48b83d7. Do not pipe the command — a pipe would report
# the exit status of the last stage and this job would pass on a failed build.
build-no-default:
name: Build (no default features)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build maxplayer (no default features)
run: cargo build -p maxplayer --no-default-features --locked
# web/app and web/network each ship a Node test suite ("node --test test/") that no CI
# job ran before #483 — a web-only PR could merge with its JS suite unverified while
# Rust CI stayed green. web/app is TypeScript bundled with esbuild, so it installs its
# devDependencies (lockfile-pinned) before testing; web/network still declares zero npm
# dependencies, so `npm test` runs directly. Kept as one job for both directories
# since neither takes meaningfully long; split them if that changes.
web-tests:
name: JS test suites (web/app, web/network)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
# 22+, matching web/app's declared engines: the deploy-time snapshot
# bake needs a global WebSocket, which Node 20 does not have. Testing
# on 20 while deploying on 22 proves nothing about the bake.
node-version: 22.23.4 # exact patch: >= Oct 21 2026 security release; a bare 22 resolved to a cached 22.23.3
- name: Test web/app
working-directory: web/app
run: npm ci && npm test
- name: Test web/network
working-directory: web/network
run: npm test
# The money path runs in its OWN job, on its own runner, with no `needs:` — the isolation IS the
# feature here, not an accident of layout (#271). Sharing a job means sharing fate: when the acp
# concurrency test dies, every step still queued behind it is marked `skipped` while the job as a
# whole reports `failure`, so the check list shows a single red row that cannot distinguish "the
# money suite failed" from "the money suite never ran" — the one ambiguity a money gate must not
# have. Job isolation is what empirically survives this fault: on run 30487009981 attempt 2 both
# sibling jobs passed while that job died. `if: always()` is the cheaper edit but a weaker
# guarantee — it keeps the suite on the same runner as the fault it is meant to outlive, and still
# buries its result inside another job's conclusion. A separate runner also gives the suite its own
# resource budget, which matters while the acp test's subprocess peak is the leading suspect.
# Co-locating them bought nothing to trade away, either: this suite compiles its own feature set
# (no acp, release) and so reuses none of the acp job's build output. The default-feature job
# carries zero money teeth (it compiles the gateway/git-delivery/wallet tests out entirely), which
# makes this the only job in CI that runs them.
money-path:
name: Money-path tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# Release build: the money-path suite is run in release (as the local money gate is), which
# also sidesteps a debug-only latent bug in the git-delivery HTTP transport
# (git_transport::client_short builds a nested tokio runtime inside the async collect path and
# panics on drop under debug). Interim: release-only until that transport-lifecycle bug is
# fixed (#152); the money assertions here (tip-match refusal, zero-burn, no
# journal/materialization) hold in release.
# Keep the live job_lifecycle pre-publish fee-floor check enabled here (#720).
# The payment_wallet worker-send tests now run offline with plain `wallet`;
# their keyset fixture no longer depends on testnut's HTTP availability.
- name: Test maxplayer-core (money-path suite)
run: cargo test -p maxplayer-core --release --no-default-features --features gateway,git-delivery,wallet,live-mints --locked
# The seller-credits mint sidecar (docs/specs/seller-credits.md stage 2). It is its OWN cargo
# workspace with its own Cargo.lock, so cdk's `mint` feature, and the `protoc` its signatory's
# build script needs, never reach the root build above. This is the only job that installs protoc.
# The guard step fails if cdk's `mint` feature ever leaks into the default `maxplayer` build.
maxplayer-mint:
name: maxplayer-mint sidecar
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates/maxplayer-mint -> target
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Test maxplayer-mint
run: cargo test --manifest-path crates/maxplayer-mint/Cargo.toml --locked
- name: Default maxplayer build carries no cdk mint code
# pipefail + a captured tree: a failing `cargo tree` must turn this step red, not green.
run: |
set -euo pipefail
tree="$(cargo tree -p maxplayer --locked -e features -i cdk)"
if grep -q 'cdk feature "mint"' <<<"$tree"; then
echo "cdk's mint feature reached the default maxplayer build" >&2
exit 1
fi
maxplayer-challenges:
name: maxplayer-challenges sidecar (maxplayer-swarm)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates/maxplayer-challenges -> target
- run: cargo fmt --manifest-path crates/maxplayer-challenges/Cargo.toml --check
- run: cargo clippy --manifest-path crates/maxplayer-challenges/Cargo.toml --locked --all-targets --no-deps -- -D warnings
- run: cargo test --manifest-path crates/maxplayer-challenges/Cargo.toml --locked
# The crate keeps its package name; the installed command is `maxplayer-swarm` ([[bin]]).
- name: Binary is named maxplayer-swarm
run: |
set -euo pipefail
cargo build --manifest-path crates/maxplayer-challenges/Cargo.toml --locked --bins
crates/maxplayer-challenges/target/debug/maxplayer-swarm --help | grep -q '^Usage: maxplayer-swarm <command>'
# scripts/challenges Python helpers (stdlib unittest, no deps; ubuntu-latest ships python3).
# The e2e harness itself needs a relay/Postgres/Redis and stays manual (run-e2e.sh).
- run: python3 -m unittest discover -s scripts/challenges -p 'test_*.py' -v
# The email registration service (crates/maxplayer-registration; spec: email signup v2.12). Its
# OWN cargo workspace with its own Cargo.lock, like the mint and challenges sidecars, so axum/sqlx
# never reach the root build. Integration tests need a real Postgres: each test creates a fresh
# database owned by a fresh NOSUPERUSER role from this admin URL, and talks to in-process fake
# relay + fake Resend servers. Nothing here reaches a real relay, Resend or any email address.
maxplayer-registration:
name: maxplayer-registration service
runs-on: ubuntu-latest
permissions:
contents: read
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 20
env:
MAXPLAYER_REGISTRATION_TEST_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates/maxplayer-registration -> target
- run: cargo fmt --manifest-path crates/maxplayer-registration/Cargo.toml --check
- run: cargo clippy --manifest-path crates/maxplayer-registration/Cargo.toml --locked --all-targets --no-deps -- -D warnings
- run: cargo test --manifest-path crates/maxplayer-registration/Cargo.toml --locked
- name: Binary builds and prints its usage
run: |
set -euo pipefail
cargo build --manifest-path crates/maxplayer-registration/Cargo.toml --locked --bins
crates/maxplayer-registration/target/debug/maxplayer-registration --help | grep -q '^Usage: maxplayer-registration '
- name: Root build carries no sqlx
run: |
set -euo pipefail
if grep -qE '^name = "sqlx' Cargo.lock; then
echo "sqlx reached the root Cargo.lock; maxplayer-registration must stay its own workspace" >&2
exit 1
fi
# The standalone Cashu trade CLI. It is its OWN cargo workspace (`[workspace]` in its manifest)
# with its own Cargo.lock, so no root-workspace job above builds or tests it: before this job a
# green CI said nothing about crates/maxplayer-trade. Its integration dev-dependencies start
# in-process CDK fake-wallet mints (cdk `mint` -> cdk-signatory), so the tests need protoc even
# though production trading is wallet-only. No test here reaches a real mint, wallet or relay:
# every mint is a loopback fixture, and `lab` timing controls refuse non-loopback mints.
#
# It used to be one job, ~63 minutes (default suite ~13 min + lab suite ~49 min, both
# --test-threads=1). It is now four jobs:
# trade-changes decides whether trading code changed (PRs only; push/dispatch always run)
# maxplayer-trade-lint fmt, the matrix guard, clippy (default + lab), and the shared cache build
# maxplayer-trade-test the default and lab suites, one matrix entry per test group
# maxplayer-trade aggregate that keeps the old check name, so anything keyed on it holds
#
# The trade jobs are skipped with a job-level `if:` rather than a workflow `paths:` filter: a job
# skipped by `if:` reports success, so a required check on it can never leave a PR waiting.
trade-changes:
name: maxplayer-trade change filter
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
run: ${{ steps.filter.outputs.run }}
env:
# maxplayer-trade's local dependencies, followed recursively from the `path =` entries:
# crates/maxplayer-trade -> maxplayer-core (dependency), maxplayer-mint (dev-dependency)
# crates/maxplayer-core -> crates/buzz/crates/maxplayer-private-protocol; it also inherits
# version/edition/dependencies from the root Cargo.toml
# crates/maxplayer-mint -> maxplayer-core (already listed)
# maxplayer-private-protocol -> nothing; a member of the crates/buzz workspace
# tests/auth_gated.rs includes crates/maxplayer-core/tests/support via #[path]; covered above.
# The trade Cargo.lock is crates/maxplayer-trade/Cargo.lock. There is no rust-toolchain file or
# .cargo/ config today; they are listed so adding one re-runs trading.
# `dir/` matches a prefix, anything else one exact path. scripts/verify-trade-test-matrix.py
# (run by the lint job) fails if a local package or its workspace manifest is not covered here.
TRADE_PATHS: >-
crates/maxplayer-trade/
crates/maxplayer-core/
crates/maxplayer-mint/
crates/buzz/crates/maxplayer-private-protocol/
Cargo.toml
crates/buzz/Cargo.toml
rust-toolchain
rust-toolchain.toml
.cargo/
.github/workflows/ci.yml
scripts/verify-trade-test-matrix.py
steps:
# A pull_request run checks out the merge commit refs/pull/N/merge: its first parent is the
# base branch tip and its diff against that parent is exactly what merging the PR changes.
- uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
fetch-depth: 2
- name: Did trading code change?
id: filter
env:
EVENT: ${{ github.event_name }}
run: |
set -euo pipefail
decide() {
echo "run=$1" >>"$GITHUB_OUTPUT"
echo "maxplayer-trade run=$1: $2" | tee -a "$GITHUB_STEP_SUMMARY"
}
if [ "$EVENT" != pull_request ]; then
decide true "$EVENT event, the trade jobs always run"
exit 0
fi
if ! git rev-parse -q --verify 'HEAD^2' >/dev/null; then
echo "::warning::HEAD is not a PR merge commit; running the trade jobs"
decide true "no merge commit to diff"
exit 0
fi
covered() {
local p
for p in $TRADE_PATHS; do
if [[ "$p" == */ ]]; then
if [[ "$1" == "$p"* ]]; then return 0; fi
elif [[ "$1" == "$p" ]]; then
return 0
fi
done
return 1
}
changed="$(git diff --no-renames --name-only 'HEAD^1' HEAD)"
hits=()
while IFS= read -r f; do
if [ -n "$f" ] && covered "$f"; then hits+=("$f"); fi
done <<<"$changed"
echo "Changed files: $(grep -c . <<<"$changed" || true); trading paths among them: ${#hits[@]}"
if [ "${#hits[@]}" -gt 0 ]; then
printf ' %s\n' "${hits[@]}" | tee -a "$GITHUB_STEP_SUMMARY"
decide true "${#hits[@]} trading path(s) changed"
else
decide false "no trading path changed; the trade jobs are skipped"
fi
# Clippy runs with -D warnings. The -A list names the lint classes that were already present
# when this job was added (collapsible `if`, wide money-path signatures, one no-op struct update,
# fixture slice clones, one constant assertion); allowing them by name keeps every OTHER lint a
# hard failure without restyling reviewed money code in the same change.
#
# Cache: every trade job uses shared-key `maxplayer-trade` (the key the single job used to save
# under), so they all restore one dependency build. Only this job saves it, after it has built
# both the clippy and the test artifacts; the test jobs only restore. Otherwise whichever job
# finished first would save, and a clippy-only cache would make every test job rebuild its deps.
# Each test job still compiles the trade crate and its own test binaries once.
maxplayer-trade-lint:
name: maxplayer-trade fmt, clippy, matrix guard
needs: trade-changes
if: needs.trade-changes.outputs.run == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
env:
TRADE_MANIFEST: crates/maxplayer-trade/Cargo.toml
TRADE_CLIPPY_ALLOW: >-
-A clippy::collapsible_if -A clippy::too_many_arguments -A clippy::needless_update
-A clippy::cloned_ref_to_slice_refs -A clippy::assertions_on_constants
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: maxplayer-trade
workspaces: crates/maxplayer-trade -> target
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: cargo fmt --check
run: cargo fmt --manifest-path "$TRADE_MANIFEST" --check
# Every test target, plus the lib/bin/doc tests, runs exactly once per suite in the matrix
# below, and every local dependency is covered by trade-changes. A new tests/*.rs file that
# is not added to the matrix turns this red instead of silently never running.
- name: Guard the test matrix and the change filter
run: python3 scripts/verify-trade-test-matrix.py
- name: Clippy (default features)
run: cargo clippy --manifest-path "$TRADE_MANIFEST" --locked --all-targets --no-deps -- -D warnings $TRADE_CLIPPY_ALLOW
- name: Clippy (lab)
run: cargo clippy --manifest-path "$TRADE_MANIFEST" --locked --all-targets --features lab --no-deps -- -D warnings $TRADE_CLIPPY_ALLOW
# Builds the test dependencies into the cache this job saves. `lab` adds no dependency, so
# one default build covers both suites.
- name: Build the tests (cache warm-up)
run: cargo test --manifest-path "$TRADE_MANIFEST" --locked --no-run
# The lab suite (`--features lab`, TRADE_LAB_SECONDS=1 short deadlines) is where refund/claim
# recovery, slow/dead-mint and deadline regressions live; the default suite compiles them out.
#
# Groups are balanced on per-test times from a green main run (job 117865014532), aiming at
# ~7 minutes of tests per job. Tests stay --test-threads=1 inside each job. A binary too slow for
# one job is sharded: `only:` entries run named tests (--exact), and exactly one `skip:` entry
# per binary runs everything else, so a test added to that binary later runs by default. The
# guard in the lint job checks that coverage; the step below fails if a named test is missing.
# Measured test seconds per group are noted on each entry.
maxplayer-trade-test:
name: maxplayer-trade test (${{ matrix.suite }}, ${{ matrix.group }})
needs: trade-changes
if: needs.trade-changes.outputs.run == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
# default suite (~799s total)
- suite: default
group: nostr # ~360s
targets: --test nostr
- suite: default
group: lib + other binaries # ~360s
targets: >-
--lib --bins --test auth_gated --test doctor_members_relay --test backoff --test cli
--test e2e --test lifecycle
--test money --test pinned_quote_recovery --test receive --test relays
--test review_regressions --test round5 --test round7 --test send
--test serve_receive --test serve_review_r2 --test serve_send_fund
--test serve_socket_uid --test ux
doc: true
# lab suite (~2870s total)
- suite: lab
group: nostr 1 # ~400s
targets: --test nostr
only: >-
nostr_abandoned_maker_lock_expires_on_slow_relay_and_frees_the_taker
nostr_taker_timed_refund
nostr_claim_past_window_stays_ambiguous_then_restores
- suite: lab
group: nostr 2 # ~383s
targets: --test nostr
only: >-
nostr_slow_relay_near_deadline_claim_stays_safe
nostr_nostr_trade_completes_through_lost_swap_replies
nostr_counterparty_unreachable_refund_still_lands
redelivered_request_is_refused_after_cancel_or_expiry
- suite: lab
group: nostr rest # ~310s
targets: --test nostr
skip: >-
nostr_abandoned_maker_lock_expires_on_slow_relay_and_frees_the_taker
nostr_taker_timed_refund
nostr_claim_past_window_stays_ambiguous_then_restores
nostr_slow_relay_near_deadline_claim_stays_safe
nostr_nostr_trade_completes_through_lost_swap_replies
nostr_counterparty_unreachable_refund_still_lands
redelivered_request_is_refused_after_cancel_or_expiry
- suite: lab
group: review_regressions 1 # ~357s
targets: --test review_regressions
only: >-
h2_failed_claim_abandoned_then_taker_refunds
probe_owned_change_spent_does_not_block_refund
n3_taker_invalid_dleq_refunds
n2_late_lock_after_abandonment_is_restored_and_refunded
probe_taker_persistent_invalid_dleq
probe_taker_invalid_lock_valid_refund
- suite: lab
group: review_regressions 2 # ~360s
targets: --test review_regressions
only: >-
n3_taker_missing_dleq_refunds
n1_maker_refund_before_taker_observes_abandonment
h2_landed_claim_lost_reply_never_refunds
r4_all_relays_down_after_both_locks_still_refunds
n2_late_claim_after_abandonment_restore_outage_never_refunds
m3_claimed_terminal_without_refund
h1_maker_offline_past_long_still_claims
- suite: lab
group: review_regressions rest # ~247s
targets: --test review_regressions
skip: >-
h2_failed_claim_abandoned_then_taker_refunds
probe_owned_change_spent_does_not_block_refund
n3_taker_invalid_dleq_refunds
n2_late_lock_after_abandonment_is_restored_and_refunded
probe_taker_persistent_invalid_dleq
probe_taker_invalid_lock_valid_refund
n3_taker_missing_dleq_refunds
n1_maker_refund_before_taker_observes_abandonment
h2_landed_claim_lost_reply_never_refunds
r4_all_relays_down_after_both_locks_still_refunds
n2_late_claim_after_abandonment_restore_outage_never_refunds
m3_claimed_terminal_without_refund
h1_maker_offline_past_long_still_claims
- suite: lab
group: lib + round5, e2e # ~400s
targets: >-
--lib --bins --test round5 --test e2e --test money --test auth_gated
--test doctor_members_relay
--test lifecycle --test cli --test pinned_quote_recovery
doc: true
- suite: lab
group: serve, backoff, other binaries # ~412s
targets: >-
--test serve_receive --test backoff --test ux --test send --test receive
--test serve_send_fund --test round7 --test relays --test serve_review_r2
--test serve_socket_uid
env:
TRADE_MANIFEST: crates/maxplayer-trade/Cargo.toml
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: maxplayer-trade
workspaces: crates/maxplayer-trade -> target
save-if: false
- name: Install protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Test (${{ matrix.suite }}, ${{ matrix.group }})
env:
SUITE: ${{ matrix.suite }}
TARGETS: ${{ matrix.targets }}
ONLY: ${{ matrix.only }}
SKIP: ${{ matrix.skip }}
DOC: ${{ matrix.doc && 'true' || 'false' }}
run: |
set -euo pipefail
args=(--manifest-path "$TRADE_MANIFEST" --locked)
if [ "$SUITE" = lab ]; then
args+=(--features lab)
# Set only for lab: the code checks presence, so even an empty value would count.
export TRADE_LAB_SECONDS=1
fi
read -ra target_args <<<"$TARGETS"
read -ra only_names <<<"$ONLY"
read -ra skip_names <<<"$SKIP"
filter=()
if [ $((${#only_names[@]} + ${#skip_names[@]})) -gt 0 ]; then
# A shard names tests; a renamed or deleted one must fail here, not shrink the shard.
listed="$(cargo test "${args[@]}" "${target_args[@]}" -- --list --format terse)"
for name in "${only_names[@]}" "${skip_names[@]}"; do
if ! grep -qxF "$name: test" <<<"$listed"; then
echo "::error::$name is not a test in ${target_args[*]} ($SUITE); update the ci.yml shard"
exit 1
fi
done
filter=(--exact "${only_names[@]}")
for name in "${skip_names[@]}"; do filter+=(--skip "$name"); done
fi
cargo test "${args[@]}" "${target_args[@]}" -- --test-threads=1 "${filter[@]}"
if [ "$DOC" = true ]; then
cargo test "${args[@]}" --doc -- --test-threads=1
fi
# Keeps the exact name of the old single job, so a required-check rule or anything else keyed on
# it keeps working without an admin change. Fails if any trade job failed or was cancelled.
# Skipped is accepted only when trade-changes found no trading change; if it said run, every
# trade job must have succeeded.
maxplayer-trade:
name: maxplayer-trade (fmt, clippy, default + lab tests)
needs: [trade-changes, maxplayer-trade-lint, maxplayer-trade-test]
if: always()
runs-on: ubuntu-latest
permissions: {}
steps:
- name: All maxplayer-trade jobs passed or were skipped
env:
NEEDS: ${{ toJSON(needs) }}
RUN: ${{ needs.trade-changes.outputs.run }}
run: |
set -euo pipefail
jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<<"$NEEDS"
bad="$(jq -r --arg run "$RUN" '[to_entries[]
| select(.value.result != "success"
and (.value.result != "skipped" or $run == "true" or .key == "trade-changes"))
| "\(.key)=\(.value.result)"] | join(", ")' <<<"$NEEDS")"
if [ -n "$bad" ]; then
echo "::error::maxplayer-trade jobs did not pass: $bad"
exit 1
fi
if [ "$RUN" != true ]; then
echo "::notice::maxplayer-trade jobs skipped: no trading path changed"
fi
# The release workflow triggers only on a tag, so no pull request ever exercises it. What CI can
# check is that the gates keeping it from publishing are still in place — otherwise the property
# holds only until somebody edits the file, and nothing would say so.
release-gates:
name: Release workflow gates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/verify-install-version.sh
- run: ./scripts/verify-release-workflow.sh
# The tree-side platform surfaces — the npm payload packages, the launcher's pins and its
# runtime resolver — held to .github/release-platforms.json on every pull request.
# `--no-artifacts` skips only the built-artifact check (nothing is built in this job); the
# drift #249 is about is a tree edit, so this catches it here, before a release is ever
# dispatched, rather than only in the release workflow's own dry run.
- run: ./scripts/verify-release-surface.sh --no-artifacts
- run: ./scripts/verify-reserved-paths.sh