feat: add Ed25519 signature verification for website activation #2004
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |