Skip to content

feat: add Ed25519 signature verification for website activation #2004

feat: add Ed25519 signature verification for website activation

feat: add Ed25519 signature verification for website activation #2004

Workflow file for this run

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