-
Notifications
You must be signed in to change notification settings - Fork 0
762 lines (677 loc) · 31.3 KB
/
Copy pathci.yml
File metadata and controls
762 lines (677 loc) · 31.3 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
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
name: CI
on:
workflow_call:
push:
branches: [ main ]
pull_request:
branches: [ main ]
# Without this, every push to a branch leaves the previous run compiling a
# commit whose result nobody will ever read. A superseded clippy-and-test run
# can burn its full 30-minute budget for nothing.
#
# Cancellation is deliberately OFF on main: the post-merge `bump-version` job
# commits, pushes, and *then* tags. Cancelling it between those two writes would
# leave main carrying an untagged version bump, which is a state no later run
# repairs. Wasted compute on main is cheap; a missing release tag is not.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
CARGO_TERM_COLOR: always
# Incremental artifacts are pure overhead in CI: the target dir is restored
# from cache and never reused across edits, so incremental only bloats what
# gets saved back. nightly.yml already sets this.
CARGO_INCREMENTAL: 0
# Default to least-privilege; jobs that need more (e.g. bump-version) opt in.
permissions:
contents: read
jobs:
# Enforces REPOSITORY_HYGIENE.md (profile: .repo-hygiene.toml): first the
# checker's own unit tests against fixture trees, then static mode over the
# real tracked tree — root allowlist, forbidden/retired paths, undeclared
# binaries, personal paths, generated-file exceptions, archive checksums,
# experiment expiry, and product-version drift. Runs on both OSes because
# path and file-content handling differ. Blocking: bump-version depends on
# it, and any violation fails PR CI.
repo-hygiene:
name: Repository Hygiene
strategy:
matrix:
os: [blacksmith-2vcpu-ubuntu-2404-arm, blacksmith-4vcpu-windows-2025]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Checker unit tests (fixture trees)
run: python -m unittest discover -s tests/tooling -v
- name: Static hygiene check (tracked tree)
run: python scripts/check_repo_hygiene.py --mode static
fmt:
name: Check formatting
# `cargo fmt --check` parses and prints. It is setup- and I/O-bound and
# never saturates one core, let alone four. Smallest ARM runner:
# $0.0025/min against the $0.008 it costs today.
runs-on: blacksmith-2vcpu-ubuntu-2404-arm
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
# The fuzz crate is a standalone workspace excluded from the root build, so a
# normal `cargo build` never compiles it — API drift in wfl could silently
# break every fuzz target. This job type-checks the targets against the current
# API on stable (libFuzzer/nightly is only needed to actually *run* them).
fuzz-check:
name: Fuzz targets compile
# `cargo check` only - no codegen, and it produces no shipped artifact,
# so architecture is irrelevant and ARM is 37.5% cheaper per vCPU.
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
needs: fmt
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo registry and target directory
uses: Swatinem/rust-cache@v2
with:
workspaces: fuzz
shared-key: fuzz-check-cache
# `--locked` enforces the committed fuzz/Cargo.lock, so dependency drift
# fails the job instead of silently regenerating the lockfile.
- name: Type-check fuzz targets against the current API
run: cargo check --locked --manifest-path fuzz/Cargo.toml
# The release publishing scripts never run in PR CI - they only execute in the
# nightly job, against the live bucket, with credentials no PR has. A mistake
# in them is therefore invisible until it has already published (or failed to
# publish) a real release, which is how issue #662 shipped. These tests run
# the real scripts against a recording stub of the AWS CLI and assert the keys,
# bytes and cache headers they write.
release-scripts:
name: Release Script Tests
# Bash, jq and sha256sum over a handful of tiny files.
runs-on: blacksmith-2vcpu-ubuntu-2404-arm
needs: fmt
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Test publish/backfill scripts
run: ./scripts/test_publish_spaces.sh
# Same reasoning for the version-bump push: it only ever runs on pushes to
# main, writing directly to main, so its retry path is untestable in PR CI
# unless it is exercised here against a real local remote.
- name: Test version-bump push script
run: ./scripts/test_push_version_bump.sh
clippy-and-test:
name: Build, Test, Clippy
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: fmt
# This job compiles the workspace three times (debug, release, and a third
# release build for the panic=abort assert), runs the full workspace test
# suite, builds the LSP binary, and runs Clippy. That real work now lands
# right at ~15 minutes even with a warm cache, so a 15-minute limit was
# firing mid-Clippy / during the cache-save step and marking the whole job
# `cancelled` (which also skips bump-version). 30 minutes gives headroom for
# cold-cache builds without letting a genuinely hung job run unbounded.
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
# Keep the R3 dependency regression in this existing required presubmit
# job, so it blocks a PR before the post-merge nightly packaging run.
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: vscode-extension/package-lock.json
- name: Install locked extension dependencies
working-directory: vscode-extension
run: npm ci
- name: Extension dependency security regression
working-directory: vscode-extension
run: npm run test:security
# Reclaim runner disk before building. This job compiles the workspace
# several times (debug + two release builds) plus the whole test suite, and
# the release profile keeps full debuginfo (`debug = true`), so the target
# tree is large; a full GitHub-hosted runner can otherwise exhaust its disk
# mid-link (a linker `Bus error`/SIGBUS). Removing preinstalled SDKs we do
# not use frees ~20 GB with no third-party action.
- name: Free disk space (Linux)
if: runner.os == 'Linux'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/graalvm || true
sudo docker image prune --all --force > /dev/null 2>&1 || true
df -h /
# Cache Cargo registry and target directory for faster builds
- name: Cache Cargo registry and target directory
uses: Swatinem/rust-cache@v2
with:
shared-key: ci-build-cache
# Build debug version first (for development workflow compatibility)
- name: Build (Debug)
run: cargo build --verbose
# Build release version (required for integration tests)
- name: Build (Release)
run: cargo build --release --verbose
# Concurrency Phase 0 (PR-0a) gate: prove the panic-strategy gate is live.
# The `#[cfg(panic = "abort")] compile_error!` in src/lib.rs must fail the
# build when panic=abort is forced (via --config so Cargo actually compiles
# the crate with -C panic=abort). If this build SUCCEEDS the gate is not
# enforced and catch_unwind fault isolation would be a phantom control.
- name: Assert panic=abort is rejected
run: |
set +e
output=$(cargo build --release --config 'profile.release.panic="abort"' --quiet 2>&1)
status=$?
set -e
# The gate must fail the build *with our specific compile_error*, not
# merely error out for some unrelated reason.
if [ "$status" -eq 0 ] || ! grep -Fq 'WFL requires panic = "unwind"' <<<"$output"; then
printf '%s\n' "$output"
echo "::error::panic=abort was not rejected by the src/lib.rs compile_error gate"
exit 1
fi
echo "panic=abort correctly rejected by the compile_error gate"
# Run tests across the WHOLE workspace (root package + wfl-lsp), so the CI
# aggregate is a true full-workspace baseline. Previously this was
# `cargo test` (root package only), which silently skipped the other
# members' tests. (integration tests have access to the release binary)
- name: Run Tests
run: cargo test --workspace --verbose
# Build the LSP binary explicitly as a focused Send/Sync build gate.
# (`cargo test --workspace` above already compiles and runs wfl-lsp's
# tests, so a separate `cargo test -p wfl-lsp` step would only duplicate
# them — it was removed.)
- name: Build LSP
run: cargo build -p wfl-lsp --verbose
- name: Install extension host display dependencies
run: |
sudo apt-get update
sudo apt-get install -y xvfb libasound2t64 libgbm1 libgtk-3-0 libnss3
- name: Extension lint and VS Code host tests
working-directory: vscode-extension
run: xvfb-run -a npm test
# Run Clippy for code quality. `--all-features` matches the binding gate in
# testing.md (the only features are opt-in dhat profiling, so this just
# compiles the feature-gated code for linting — it never runs it).
- name: Run Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
# Cross-platform integration test verification
integration-tests:
name: Integration Tests
strategy:
matrix:
os: [blacksmith-4vcpu-ubuntu-2404, blacksmith-4vcpu-windows-2025]
runs-on: ${{ matrix.os }}
needs: fmt
# The password-hashing tests exercise deliberately slow, memory/CPU-hard
# KDFs (Argon2/scrypt/PBKDF2). Built unoptimized by `cargo test`, they take
# meaningful time on the slower Windows runner, which combined with the cold
# release build was exceeding the previous 15-minute limit.
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# The post-suite working-tree hygiene check needs Python 3.11+ (tomllib);
# the Windows runner's default python is older, so set it up explicitly.
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# Reclaim runner disk before building: this job builds the (debuginfo-heavy)
# release tree AND every integration test binary (`cargo test --test '*'`),
# which together can exhaust a full runner's disk mid-link (linker
# `Bus error`/SIGBUS). Freeing unused preinstalled SDKs gives ~20 GB of
# headroom. Linux only — the Windows runner is not affected.
- name: Free disk space (Linux)
if: runner.os == 'Linux'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/graalvm || true
sudo docker image prune --all --force > /dev/null 2>&1 || true
df -h /
# Cache Cargo registry and target directory for faster builds
- name: Cache Cargo registry and target directory
uses: Swatinem/rust-cache@v2
with:
shared-key: integration-${{ matrix.os }}
# Build release version (required for integration tests)
- name: Build Release Binary
run: cargo build --release --verbose
# Verify release binary exists (platform-specific paths)
- name: Verify Release Binary (Linux/macOS)
if: runner.os != 'Windows'
run: |
if [ ! -f "target/release/wfl" ]; then
echo "::error::Release binary not found at target/release/wfl"
exit 1
fi
echo "✓ Release binary found: target/release/wfl"
- name: Verify Release Binary (Windows)
if: runner.os == 'Windows'
run: |
if (!(Test-Path "target/release/wfl.exe")) {
Write-Host "::error::Release binary not found at target/release/wfl.exe"
exit 1
}
Write-Host "✓ Release binary found: target/release/wfl.exe"
# Run the DOCUMENTED WFL integration gate (testing.md): the same script a
# contributor runs locally. It executes the integration test binaries
# (`cargo test --test '*'`) AND the TestPrograms end-to-end programs —
# crucially including the intentional-error programs, which it asserts exit
# nonzero (previously those assertions lived only in this script and the
# script was never invoked in CI, so they never ran). Uses the release
# binary built above. Run on BOTH OSes so the declared Windows integration
# command is actually exercised, not merely documented.
- name: Run Integration Gate (Unix)
if: runner.os != 'Windows'
run: ./scripts/run_integration_tests.sh
- name: Run Integration Gate (Windows)
if: runner.os == 'Windows'
shell: pwsh
# Nested `execute file` (and other deep interpreter futures) need more
# than the default ~1 MB Windows test-thread stack; without this the
# depth-guard test overflows natively before max_execute_file_depth
# can fire (#681). 8 MB matches a typical Linux default and is well
# under the CLI's dedicated interpreter stack.
env:
RUST_MIN_STACK: '8388608'
run: ./scripts/run_integration_tests.ps1
# Real ordinary execution beyond the historical 300-second CLI ceiling.
# The complete scenario/assertions are WFL; this step only invokes it.
# It cannot use the recursive program sweep's 30-second per-file bound.
- name: Verify explicit execution budget beyond five minutes
timeout-minutes: 6
run: ./target/release/wfl --execution-timeout 330 --test tests/fixtures/cli_budget/long-run.test.wfl
# Validate that documentation examples still parse/analyze/lint against the
# current release binary (testing.md requires docs validation in CI).
# `--force` ignores the committed cache so CI always re-validates rather
# than trusting a stale cached result.
- name: Validate Docs Examples (Unix)
if: runner.os != 'Windows'
run: python3 scripts/validate_docs_examples.py --ci --force
- name: Validate Docs Examples (Windows)
if: runner.os == 'Windows'
run: python scripts/validate_docs_examples.py --ci --force
# Web-server integration tests: start real WFL servers and exercise them
# over HTTP (testing.md requires web tests in CI for the R3 web/streaming
# surface). Uses the release binary built above. Both OSes are covered so
# Windows web-server behavior does not go unvalidated.
- name: Run Web Server Tests (Unix)
if: runner.os != 'Windows'
run: ./scripts/run_web_tests.sh
- name: Run Web Server Tests (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: ./scripts/run_web_tests.ps1
# After every suite above: the checkout must be clean outside the
# approved output roots (REPOSITORY_HYGIENE.md §5). A test or tool that
# littered the source tree fails here, on the OS where it littered.
- name: Working-tree hygiene check (Unix)
if: runner.os != 'Windows'
run: python3 scripts/check_repo_hygiene.py --mode working-tree
- name: Working-tree hygiene check (Windows)
if: runner.os == 'Windows'
run: python scripts/check_repo_hygiene.py --mode working-tree
# Database integration tests against live PostgreSQL and MariaDB servers.
# SQLite database tests need no services and already run everywhere via
# `cargo test`; this job exercises the env-gated PostgreSQL/MariaDB paths.
database-tests:
name: Database Tests (PostgreSQL + MariaDB)
runs-on: blacksmith-4vcpu-ubuntu-2404
needs: fmt
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: wfl
POSTGRES_PASSWORD: wfl
POSTGRES_DB: wfl_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U wfl"
--health-interval 10s
--health-timeout 5s
--health-retries 5
mariadb:
image: mariadb:11
env:
MARIADB_USER: wfl
MARIADB_PASSWORD: wfl
MARIADB_DATABASE: wfl_test
MARIADB_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd "healthcheck.sh --connect --innodb_initialized"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
WFL_TEST_POSTGRES_URL: postgres://wfl:wfl@localhost:5432/wfl_test
WFL_TEST_MYSQL_URL: mysql://wfl:wfl@localhost:3306/wfl_test
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo registry and target directory
uses: Swatinem/rust-cache@v2
with:
shared-key: database-tests
- name: Run Database Tests
run: cargo test --test database_test --verbose
# Run WFL test programs to verify the interpreter works correctly
run-wfl-programs:
name: Run WFL Programs
strategy:
matrix:
os: [blacksmith-4vcpu-ubuntu-2404, blacksmith-4vcpu-windows-2025]
runs-on: ${{ matrix.os }}
needs: fmt
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# The post-suite working-tree hygiene check needs Python 3.11+ (tomllib);
# the Windows runner's default python is older, so set it up explicitly.
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# Cache Cargo registry and target directory for faster builds
- name: Cache Cargo registry and target directory
uses: Swatinem/rust-cache@v2
with:
shared-key: wfl-programs-${{ matrix.os }}
# Build release version
- name: Build Release Binary
run: cargo build --release --verbose
# Run WFL programs (Linux/macOS)
- name: Run WFL Programs (Linux/macOS)
if: runner.os != 'Windows'
shell: bash
run: |
#!/bin/bash
# Note: Do not use 'set -e' as arithmetic operations like ((var++)) return 1 when var is 0
WFL_BINARY="./target/release/wfl"
TESTPROGRAMS_DIR="./TestPrograms"
TIMEOUT_SECONDS=30
# Declare associative array for skip patterns with reasons
# Most web-server tests carry a "// CI-SKIP: <reason>" first line and are
# skipped via that directive; the list below covers the remaining cases.
# Intentional-error programs are asserted by scripts/run_integration_tests.sh.
declare -A SKIP_REASONS=(
["circular_"]="intentional circular-include error (asserted by run_integration_tests.sh)"
["module_include_circular"]="intentional circular-include error (asserted by run_integration_tests.sh)"
["module_helper"]="helper module, not standalone"
["scoped.wfl"]="intentionally references an undefined variable (asserted by run_integration_tests.sh)"
["test_redefinition_error"]="intentional redefinition error (asserted by run_integration_tests.sh)"
["test_assertion_fix"]="intentionally failing assertions (asserted by run_integration_tests.sh)"
)
ERROR_EXAMPLES_REASON="expected to fail (error example)"
passed=0
failed=0
skipped=0
timed_out=0
failed_files=""
echo "=== Running WFL Test Programs ==="
echo ""
# Find all .wfl files
while IFS= read -r -d '' file; do
filename=$(basename "$file")
relative_path="${file#./}"
# Check for CI-SKIP directive in the file
first_line=$(head -1 "$file")
if [[ "$first_line" == *"CI-SKIP:"* ]]; then
ci_skip_reason=$(echo "$first_line" | sed 's/.*CI-SKIP:\s*//')
fi
# Check if file should be skipped
skip_reason=""
# Check error_examples directory first
if [ -n "${ci_skip_reason:-}" ]; then
skip_reason="$ci_skip_reason"
ci_skip_reason=""
elif [[ "$relative_path" == *"error_examples"* ]]; then
skip_reason="$ERROR_EXAMPLES_REASON"
else
# Check against skip patterns
for pattern in "${!SKIP_REASONS[@]}"; do
if [[ "$filename" == *"$pattern"* ]]; then
skip_reason="${SKIP_REASONS[$pattern]}"
break
fi
done
fi
if [ -n "$skip_reason" ]; then
echo "SKIP: $relative_path ($skip_reason)"
skipped=$((skipped + 1))
continue
fi
echo -n "RUN: $relative_path ... "
# Programs with describe blocks must run in test mode
extra_flags=()
if grep -qE '^[[:space:]]*describe "' "$file"; then
extra_flags=(--test)
fi
# Run with timeout
if timeout "$TIMEOUT_SECONDS" "$WFL_BINARY" "${extra_flags[@]}" "$file" > /dev/null 2>&1; then
echo "PASS"
passed=$((passed + 1))
else
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "TIMEOUT (${TIMEOUT_SECONDS}s)"
timed_out=$((timed_out + 1))
else
echo "FAIL (exit code: $exit_code)"
fi
failed=$((failed + 1))
failed_files="$failed_files\n - $relative_path"
fi
done < <(find "$TESTPROGRAMS_DIR" -name "*.wfl" -print0 | sort -z)
echo ""
echo "=== Summary ==="
echo "Passed: $passed"
echo "Failed: $failed (timeouts: $timed_out)"
echo "Skipped: $skipped"
echo "Total: $((passed + failed + skipped))"
if [ $failed -gt 0 ]; then
echo ""
echo "Failed files:$failed_files"
exit 1
fi
# Run WFL programs (Windows)
- name: Run WFL Programs (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = "Continue"
$WflBinary = Join-Path (Get-Location).Path "target\release\wfl.exe"
$TestProgramsDir = ".\TestPrograms"
$TimeoutSeconds = 30
# Skip patterns with reasons (hashtable)
# Most web-server tests carry a "// CI-SKIP: <reason>" first line and are
# skipped via that directive; the list below covers the remaining cases.
# Intentional-error programs are asserted by scripts/run_integration_tests.sh.
$SkipReasons = @{
"circular_" = "intentional circular-include error (asserted by run_integration_tests.sh)"
"module_include_circular" = "intentional circular-include error (asserted by run_integration_tests.sh)"
"module_helper" = "helper module, not standalone"
"subprocess" = "subprocess tests use platform-dependent commands"
"scoped.wfl" = "intentionally references an undefined variable (asserted by run_integration_tests.sh)"
"test_redefinition_error" = "intentional redefinition error (asserted by run_integration_tests.sh)"
"test_assertion_fix" = "intentionally failing assertions (asserted by run_integration_tests.sh)"
}
$ErrorExamplesReason = "expected to fail (error example)"
$passed = 0
$failed = 0
$skipped = 0
$timedOut = 0
$failedFiles = @()
Write-Host "=== Running WFL Test Programs ==="
Write-Host ""
# Find all .wfl files
$wflFiles = Get-ChildItem -Path $TestProgramsDir -Filter "*.wfl" -Recurse | Sort-Object FullName
foreach ($file in $wflFiles) {
$filename = $file.Name
# Get relative path more robustly
$currentDir = (Get-Location).Path
if ($file.FullName.StartsWith($currentDir)) {
$relativePath = $file.FullName.Substring($currentDir.Length).TrimStart('\', '/')
} else {
$relativePath = $file.FullName
}
# Check for CI-SKIP directive in the file
$firstLine = Get-Content $file.FullName -First 1
$ciSkipReason = $null
if ($firstLine -match 'CI-SKIP:\s*(.+)') {
$ciSkipReason = $Matches[1].Trim()
}
# Check if file should be skipped
$skipReason = $null
if ($null -ne $ciSkipReason) {
$skipReason = $ciSkipReason
} elseif ($relativePath -like "*error_examples*") {
$skipReason = $ErrorExamplesReason
} else {
# Check against skip patterns
foreach ($pattern in $SkipReasons.Keys) {
if ($filename -like "*$pattern*") {
$skipReason = $SkipReasons[$pattern]
break
}
}
}
if ($null -ne $skipReason) {
Write-Host "SKIP: $relativePath ($skipReason)"
$skipped++
continue
}
Write-Host -NoNewline "RUN: $relativePath ... "
# Programs with describe blocks must run in test mode
$extraArgs = @()
if (Select-String -Path $file.FullName -Pattern '^\s*describe "' -Quiet) {
$extraArgs = @("--test")
}
# Run with timeout using a job for better process control
$job = Start-Job -ScriptBlock {
param($binary, $filePath, $extraArgs)
& $binary @extraArgs $filePath 2>&1 | Out-Null
$LASTEXITCODE
} -ArgumentList $WflBinary, $file.FullName, $extraArgs
$completed = Wait-Job -Job $job -Timeout $TimeoutSeconds
if ($null -eq $completed) {
Stop-Job -Job $job
Remove-Job -Job $job -Force
Write-Host "TIMEOUT (${TimeoutSeconds}s)"
$timedOut++
$failed++
$failedFiles += $relativePath
} else {
$exitCode = Receive-Job -Job $job
Remove-Job -Job $job
if ($exitCode -eq 0) {
Write-Host "PASS"
$passed++
} else {
Write-Host "FAIL (exit code: $exitCode)"
$failed++
$failedFiles += $relativePath
}
}
}
Write-Host ""
Write-Host "=== Summary ==="
Write-Host "Passed: $passed"
Write-Host "Failed: $failed (timeouts: $timedOut)"
Write-Host "Skipped: $skipped"
Write-Host "Total: $($passed + $failed + $skipped)"
if ($failed -gt 0) {
Write-Host ""
Write-Host "Failed files:"
foreach ($f in $failedFiles) {
Write-Host " - $f"
}
exit 1
}
# The recursive program sweep above must leave the checkout clean outside
# the approved output roots (REPOSITORY_HYGIENE.md §5).
- name: Working-tree hygiene check (Unix)
if: runner.os != 'Windows'
run: python3 scripts/check_repo_hygiene.py --mode working-tree
- name: Working-tree hygiene check (Windows)
if: runner.os == 'Windows'
run: python scripts/check_repo_hygiene.py --mode working-tree
# Version bumping only happens after ALL checks pass — including fuzz-check,
# so API drift that breaks the fuzz targets blocks the post-merge version bump.
bump-version:
name: Bump Version
# Runs the same bump_version.py --update-all as versioning.yml, which shells
# out to a locked `cargo check` on the fuzz crate - so this compiles and
# needs the same 4 cores as fuzz-check. ARM: no x86 artifact produced.
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
needs: [repo-hygiene, fmt, fuzz-check, release-scripts, clippy-and-test, integration-tests, database-tests, run-wfl-programs]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: write
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
# `bump_version.py` shells out to Cargo (`cargo update` for both lockfiles
# and `cargo check --locked` over the fuzz workspace), so this job needs a
# toolchain of its own. Without one it silently used whatever rustc the
# runner image preinstalled — fine until the image shipped one older than
# our `rust-version = "1.94"` MSRV, which failed the locked fuzz check and
# blocked the bump on every push to main.
- uses: dtolnay/rust-toolchain@stable
# Restore the fuzz workspace's target dir so the locked check inside the
# bump script reuses the `fuzz-check` job's build instead of compiling the
# workspace again. Restore-only: `fuzz-check` owns writing this cache.
- name: Cache Cargo registry and target directory
uses: Swatinem/rust-cache@v2
with:
workspaces: fuzz
shared-key: fuzz-check-cache
save-if: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Bump version
run: python scripts/bump_version.py --update-all
# This job writes directly to main, so it must prove hygiene and version
# agreement itself, immediately before pushing (REPOSITORY_HYGIENE.md §8).
- name: Static hygiene and version check before push
run: python scripts/check_repo_hygiene.py --mode static
# The retry loop lives in a script so it can be tested (see
# scripts/test_push_version_bump.sh, run by the `release-scripts` job).
# Each retry re-bumps onto the new tip and re-runs the hygiene check
# above against that new tree before pushing it.
- name: Push changes
if: success()
run: ./scripts/push_version_bump.sh "${{ github.ref_name }}"
- name: Tag the new version
if: success()
run: |
# Fetch existing tags to avoid conflicts
git fetch --tags
# Extract version from version.rs
VERSION=$(grep -oP '(?<=VERSION: &str = ")[0-9]+\.[0-9]+\.[0-9]+' src/version.rs)
# Check if this version tag already exists
if ! git tag -l | grep -q "^v$VERSION$"; then
git tag -a "v$VERSION" -m "Release $VERSION"
git push origin --tags
else
echo "Tag v$VERSION already exists, skipping tagging"
fi