From d952e49e31e3791c0878392b12cbbfa12816ca3a Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 28 Jul 2026 15:24:32 -0700 Subject: [PATCH 01/31] Shard smoketests using nextest --- .github/actions/smoketest-build/action.yml | 61 +++++ .../smoketest-run-partition/action.yml | 53 ++++ .github/actions/smoketest-setup/action.yml | 113 ++++++++ .github/workflows/ci.yml | 253 ++++++++---------- crates/guard/src/lib.rs | 6 +- crates/smoketests/DEVELOP.md | 24 ++ crates/smoketests/src/lib.rs | 4 +- .../tests/smoketests/default_module_clippy.rs | 2 +- .../smoketests/tests/smoketests/namespaces.rs | 2 +- .../tests/smoketests/permissions.rs | 2 +- tools/ci/src/smoketest.rs | 104 ++++++- 11 files changed, 465 insertions(+), 159 deletions(-) create mode 100644 .github/actions/smoketest-build/action.yml create mode 100644 .github/actions/smoketest-run-partition/action.yml create mode 100644 .github/actions/smoketest-setup/action.yml diff --git a/.github/actions/smoketest-build/action.yml b/.github/actions/smoketest-build/action.yml new file mode 100644 index 00000000000..461ad7d03d9 --- /dev/null +++ b/.github/actions/smoketest-build/action.yml @@ -0,0 +1,61 @@ +name: Build smoketest artifacts +description: Build and upload the nextest archive and smoketest runtime artifacts. + +inputs: + artifact-name: + description: Name of the workflow artifact to upload. + required: true + +runs: + using: composite + steps: + - uses: ./.github/actions/smoketest-setup + with: + runtime-dependencies: "false" + + # This step shouldn't be needed, but some caches are missing librusty_v8.a. + # This job mixes `cargo build -p` with `cargo build --manifest-path`, which can + # produce different dependency trees in the same target directory. + - name: Check v8 outputs + shell: bash + run: | + find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true + if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then + echo "Could not find v8 output file librusty_v8.a; rebuilding manually." + cargo clean --release -p v8 || true + cargo build --release -p v8 + fi + + - name: Build smoketest dependencies and archive test binaries + shell: bash + run: | + cargo ci smoketests archive --archive-file smoketest-nextest.tar.zst + + if [[ "${RUNNER_OS}" == "Windows" ]]; then + executable_suffix=".exe" + else + executable_suffix="" + fi + + shopt -s nullglob + precompiled_modules=(target/wasm32-unknown-unknown/release/smoketest_module_*.wasm) + if (( ${#precompiled_modules[@]} == 0 )); then + echo "No precompiled smoketest modules were produced." + exit 1 + fi + + tar -czf smoketest-support.tar.gz \ + "target/debug/ci${executable_suffix}" \ + "target/release/spacetimedb-cli${executable_suffix}" \ + "target/release/spacetimedb-standalone${executable_suffix}" \ + "${precompiled_modules[@]}" + + - name: Upload smoketest build + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: | + smoketest-nextest.tar.zst + smoketest-support.tar.gz + if-no-files-found: error + overwrite: true diff --git a/.github/actions/smoketest-run-partition/action.yml b/.github/actions/smoketest-run-partition/action.yml new file mode 100644 index 00000000000..0846c633cda --- /dev/null +++ b/.github/actions/smoketest-run-partition/action.yml @@ -0,0 +1,53 @@ +name: Run smoketest partition +description: Download a smoketest build and run one nextest hash partition. + +inputs: + artifact-name: + description: Name of the workflow artifact to download. + required: true + partition: + description: One-based hash partition number. + required: true + +runs: + using: composite + steps: + - uses: ./.github/actions/smoketest-setup + + - name: Download smoketest build + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + + - name: Extract smoketest support files + shell: bash + run: tar -xzf smoketest-support.tar.gz + + # --test-threads=1 eliminates contention in the C# tests where they fight over + # bindings build artifacts. It also improved unpartitioned job performance. + - name: Run smoketest partition (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + if [ -f ~/emsdk/emsdk_env.sh ]; then + source ~/emsdk/emsdk_env.sh + fi + ./target/debug/ci smoketests run-archive \ + --archive-file smoketest-nextest.tar.zst \ + -- \ + --test-threads=1 \ + --partition hash:${{ inputs.partition }}/4 + + # Due to Emscripten PATH issues this remains separate so OpenSSL builds correctly. + - name: Run smoketest partition (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + if (Test-Path "$env:USERPROFILE\emsdk\emsdk_env.ps1") { + & "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null + } + & .\target\debug\ci.exe smoketests run-archive ` + --archive-file smoketest-nextest.tar.zst ` + -- ` + --test-threads=1 ` + --partition "hash:${{ inputs.partition }}/4" diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml new file mode 100644 index 00000000000..45a5ef8a015 --- /dev/null +++ b/.github/actions/smoketest-setup/action.yml @@ -0,0 +1,113 @@ +name: Set up smoketests +description: Set up Rust and the language toolchains used by the smoketests. + +inputs: + runtime-dependencies: + description: Install dependencies needed while executing smoketests. + required: false + default: "true" + +runs: + using: composite + steps: + - uses: dsherret/rust-toolchain-file@v1 + + - name: Set default Rust toolchain + shell: bash + run: rustup default "$(rustup show active-toolchain | cut -d' ' -f1)" + + - name: Set up sccache + uses: mozilla-actions/sccache-action@v0.0.10 + + - name: Enable sccache + shell: bash + run: | + echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" + echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: ${{ github.workspace }} + shared-key: spacetimedb + cache-on-failure: false + cache-all-crates: true + cache-workspace-crates: true + prefix-key: v1 + + - name: Set up .NET + if: inputs.runtime-dependencies == 'true' + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + # Node.js and pnpm are required for the TypeScript quickstart smoketest. + - name: Set up Node.js + if: inputs.runtime-dependencies == 'true' + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Set up pnpm + if: inputs.runtime-dependencies == 'true' + uses: ./.github/actions/setup-pnpm + with: + run_install: true + + # Install Emscripten for C++ module compilation tests. + - name: Install Emscripten (Linux) + if: inputs.runtime-dependencies == 'true' && runner.os == 'Linux' + shell: bash + run: | + git clone https://github.com/emscripten-core/emsdk.git ~/emsdk + cd ~/emsdk + ./emsdk install 4.0.21 + ./emsdk activate 4.0.21 + + - name: Install Emscripten (Windows) + if: inputs.runtime-dependencies == 'true' && runner.os == 'Windows' + shell: pwsh + run: | + git clone https://github.com/emscripten-core/emsdk.git $env:USERPROFILE\emsdk + cd $env:USERPROFILE\emsdk + .\emsdk install 4.0.21 + .\emsdk activate 4.0.21 + + - name: Install psql (Windows) + if: inputs.runtime-dependencies == 'true' && runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + choco install psql -y --no-progress + # Chocolatey does not reliably fail the step when installation fails. + Get-Command psql + + - name: Update .NET workloads (Windows) + if: inputs.runtime-dependencies == 'true' && runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + + cd modules + # The sdk-manifests on windows-latest need to be updated before installing workloads. + dotnet workload config --update-mode manifests + dotnet workload update --from-previous-sdk + # Explicitly install wasi-experimental for .NET 8 SDK. + $sdk8Json = '{"sdk":{"version":"8.0.400","rollForward":"latestFeature"}}' + $sdk8Json | Out-File -FilePath global.json -Encoding utf8 + dotnet workload install wasi-experimental + Remove-Item global.json + + - name: Override NuGet packages + if: inputs.runtime-dependencies == 'true' + shell: bash + run: | + dotnet pack -c Release crates/bindings-csharp/BSATN.Runtime + dotnet pack -c Release crates/bindings-csharp/Runtime + cd sdks/csharp + ./tools~/write-nuget-config.sh ../.. + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 038334613ec..12bc4934118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: runs-on: ubuntu-latest outputs: skip: ${{ steps.compare.outputs.skip }} + source_sha: ${{ steps.source.outputs.sha }} steps: - name: Checkout merge queue commit uses: actions/checkout@v4 @@ -70,175 +71,117 @@ jobs: echo "Merge queue commit ${GITHUB_SHA} differs from PR #${pr_number} head ${pr_head_sha}; running CI normally." fi - smoketests: + - name: Resolve source revision + id: source + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + + pr_number="${{ github.event.inputs.pr_number || null }}" + if [[ -n "${pr_number}" ]]; then + source_sha="$(gh pr view --repo clockworklabs/SpacetimeDB "${pr_number}" --json headRefOid --jq .headRefOid)" + else + source_sha="${GITHUB_SHA}" + fi + + if [[ -z "${source_sha}" || "${source_sha}" == "null" ]]; then + echo "Could not resolve the source revision." + exit 1 + fi + + echo "sha=${source_sha}" >>"$GITHUB_OUTPUT" + + smoketest_build_linux: needs: [merge_queue_noop, lints] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} - name: Smoketests (${{ matrix.name }}) - strategy: - matrix: - include: - - name: Linux - runner: spacetimedb-new-runner-2 - - name: Windows - runner: spacetimedb-windows-runner - runs-on: ${{ matrix.runner }} + name: Build smoketests (Linux) + runs-on: spacetimedb-new-runner-2 timeout-minutes: 120 env: CARGO_TARGET_DIR: ${{ github.workspace }}/target RUST_BACKTRACE: full SPACETIMEDB_CPP_DIR: ${{ github.workspace }}/crates/bindings-cpp steps: - - name: Find Git ref - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - PR_NUMBER="${{ github.event.inputs.pr_number || null }}" - if test -n "${PR_NUMBER}"; then - GIT_REF="$( gh pr view --repo clockworklabs/SpacetimeDB $PR_NUMBER --json headRefName --jq .headRefName )" - else - GIT_REF="${{ github.ref }}" - fi - echo "GIT_REF=${GIT_REF}" >>"$GITHUB_ENV" - - name: Checkout sources uses: actions/checkout@v4 with: - ref: ${{ env.GIT_REF }} - - - uses: dsherret/rust-toolchain-file@v1 - - name: Set default rust toolchain - run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - - name: Set up sccache - uses: mozilla-actions/sccache-action@v0.0.10 - - - name: Enable sccache - shell: bash - run: | - echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" - echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 + - uses: ./.github/actions/smoketest-build with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - cache-on-failure: false - cache-all-crates: true - cache-workspace-crates: true - prefix-key: v1 + artifact-name: smoketest-build-Linux - - uses: actions/setup-dotnet@v4 + smoketest_build_windows: + needs: [merge_queue_noop, lints] + if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} + name: Build smoketests (Windows) + runs-on: spacetimedb-windows-runner + timeout-minutes: 120 + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + RUST_BACKTRACE: full + SPACETIMEDB_CPP_DIR: ${{ github.workspace }}/crates/bindings-cpp + steps: + - name: Checkout sources + uses: actions/checkout@v4 with: - global-json-file: global.json + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - # nodejs and pnpm are required for the typescript quickstart smoketest - - name: Set up Node.js - uses: actions/setup-node@v4 + - uses: ./.github/actions/smoketest-build with: - node-version: 18 + artifact-name: smoketest-build-Windows - - uses: ./.github/actions/setup-pnpm + smoketests_linux: + needs: [merge_queue_noop, smoketest_build_linux] + if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} + name: Smoketests (Linux, ${{ matrix.partition }}/4) + strategy: + fail-fast: false + matrix: + partition: [1, 2, 3, 4] + runs-on: spacetimedb-new-runner-2 + timeout-minutes: 120 + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + RUST_BACKTRACE: full + SPACETIMEDB_CPP_DIR: ${{ github.workspace }}/crates/bindings-cpp + steps: + - name: Checkout sources + uses: actions/checkout@v4 with: - run_install: true - - # Install emscripten for C++ module compilation tests. - - name: Install emscripten (Linux) - if: runner.os == 'Linux' - shell: bash - run: | - git clone https://github.com/emscripten-core/emsdk.git ~/emsdk - cd ~/emsdk - ./emsdk install 4.0.21 - ./emsdk activate 4.0.21 - - - name: Install emscripten (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - git clone https://github.com/emscripten-core/emsdk.git $env:USERPROFILE\emsdk - cd $env:USERPROFILE\emsdk - .\emsdk install 4.0.21 - .\emsdk activate 4.0.21 - - - name: Install psql (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - # Fail properly if any individual command fails - $ErrorActionPreference = 'Stop' - $PSNativeCommandUseErrorActionPreference = $true - choco install psql -y --no-progress - # Check for existence, since `choco` doesn't seem to fail the step if it fails to install.. - # See https://github.com/clockworklabs/SpacetimeDB/pull/4399 for more background. - Get-Command psql - - - name: Update dotnet workloads - if: runner.os == 'Windows' - run: | - # Fail properly if any individual command fails - $ErrorActionPreference = 'Stop' - $PSNativeCommandUseErrorActionPreference = $true + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - cd modules - # the sdk-manifests on windows-latest are messed up, so we need to update them - dotnet workload config --update-mode manifests - dotnet workload update --from-previous-sdk - # Explicitly install wasi-experimental for .NET 8 SDK (needed for test_build_csharp_module) - # Create temp global.json to target .NET 8 SDK for workload install - $sdk8Json = '{"sdk":{"version":"8.0.400","rollForward":"latestFeature"}}' - $sdk8Json | Out-File -FilePath global.json -Encoding utf8 - dotnet workload install wasi-experimental - Remove-Item global.json - - - name: Override NuGet packages - shell: bash - run: | - dotnet pack -c Release crates/bindings-csharp/BSATN.Runtime - dotnet pack -c Release crates/bindings-csharp/Runtime - cd sdks/csharp - ./tools~/write-nuget-config.sh ../.. - - # This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a. - # ChatGPT suspects that this could be due to different build invocations using the same target dir, - # and this makes sense to me because we only see it in this job where we mix `cargo build -p` with - # `cargo build --manifest-path` (which apparently build different dependency trees). - # However, we've been unable to fix it so... /shrug - - name: Check v8 outputs - shell: bash - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean --release -p v8 || true - cargo build --release -p v8 - fi - - - name: Install cargo-nextest - uses: taiki-e/install-action@nextest + - uses: ./.github/actions/smoketest-run-partition + with: + artifact-name: smoketest-build-Linux + partition: ${{ matrix.partition }} - # --test-threads=1 eliminates contention in the C# tests where they fight over bindings - # build artifacts. - # It also seemed to improve performance a fair amount (11m -> 6m) - - name: Run smoketests (Linux) - if: runner.os == 'Linux' - shell: bash - run: | - if [ -f ~/emsdk/emsdk_env.sh ]; then - source ~/emsdk/emsdk_env.sh - fi - cargo ci smoketests -- --test-threads=1 + smoketests_windows: + needs: [merge_queue_noop, smoketest_build_windows] + if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} + name: Smoketests (Windows, ${{ matrix.partition }}/4) + strategy: + fail-fast: false + matrix: + partition: [1, 2, 3, 4] + runs-on: spacetimedb-windows-runner + timeout-minutes: 120 + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + RUST_BACKTRACE: full + SPACETIMEDB_CPP_DIR: ${{ github.workspace }}/crates/bindings-cpp + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - # Due to Emscripten PATH issues this was separated to make sure OpenSSL still builds correctly - - name: Run smoketests (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - if (Test-Path "$env:USERPROFILE\emsdk\emsdk_env.ps1") { - & "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null - } - cargo ci smoketests -- --test-threads=1 + - uses: ./.github/actions/smoketest-run-partition + with: + artifact-name: smoketest-build-Windows + partition: ${{ matrix.partition }} # this is a no-op version of the above check with a trivially-passing body. # we can't just let the check be entirely skipped because each matrix target is a required check, @@ -246,12 +189,26 @@ jobs: smoketests_noop: needs: [merge_queue_noop] if: ${{ needs.merge_queue_noop.outputs.skip == 'true' }} - name: Smoketests (${{ matrix.name }}) + name: Smoketests (${{ matrix.name }}, ${{ matrix.partition }}/4) strategy: matrix: include: - name: Linux + partition: 1 + - name: Linux + partition: 2 + - name: Linux + partition: 3 + - name: Linux + partition: 4 + - name: Windows + partition: 1 + - name: Windows + partition: 2 + - name: Windows + partition: 3 - name: Windows + partition: 4 runs-on: ubuntu-latest steps: - name: Skip duplicate merge queue smoketest diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index eead324c936..d292e417ac7 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -24,7 +24,8 @@ fn next_spawn_id() -> u64 { /// Returns the workspace root directory. // TODO: Should this use something like `git rev-parse --show-toplevel` to avoid being directory-relative? Or perhaps `CARGO_WORKSPACE_DIR` is set? fn workspace_root() -> PathBuf { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")); manifest_dir .parent() // crates/ .and_then(|p| p.parent()) // workspace root @@ -34,10 +35,9 @@ fn workspace_root() -> PathBuf { /// Returns the target directory. fn target_dir() -> PathBuf { - let workspace_root = workspace_root(); env::var("CARGO_TARGET_DIR") .map(PathBuf::from) - .unwrap_or_else(|_| workspace_root.join("target")) + .unwrap_or_else(|_| workspace_root().join("target")) } /// Returns the expected CLI binary path. diff --git a/crates/smoketests/DEVELOP.md b/crates/smoketests/DEVELOP.md index 6acba49aea2..f0bfb33320b 100644 --- a/crates/smoketests/DEVELOP.md +++ b/crates/smoketests/DEVELOP.md @@ -18,6 +18,30 @@ cargo smoketest test_sql_format cargo smoketest "cli::" # Run all CLI tests ``` +### Archived and partitioned runs + +CI can build the CLI, standalone server, precompiled modules, and smoketest +binaries once: + +```bash +cargo ci smoketests archive --archive-file smoketest-nextest.tar.zst +``` + +After transferring the nextest archive and the required support binaries to a +runner, a partition can be executed without rebuilding the CLI, server, or test +binaries: + +```bash +target/debug/ci smoketests run-archive \ + --archive-file smoketest-nextest.tar.zst \ + -- \ + --partition hash:1/4 +``` + +The four hash partitions are deterministic and independently runnable. Tests +that intentionally verify module compilation still compile their temporary +module as part of the test itself. + ### Remote Servers Run against a standalone-compatible remote server with: diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 5b90de38a03..a89a09e7537 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -56,7 +56,6 @@ pub mod modules; use anyhow::{bail, Context, Result}; use regex::Regex; use spacetimedb_guard::{ensure_binaries_built, SpacetimeDbGuard}; -use std::env; use std::fs; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; @@ -181,7 +180,8 @@ macro_rules! timed { /// Returns the workspace root directory. pub fn workspace_root() -> PathBuf { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let manifest_dir = + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")); manifest_dir .parent() .and_then(|p| p.parent()) diff --git a/crates/smoketests/tests/smoketests/default_module_clippy.rs b/crates/smoketests/tests/smoketests/default_module_clippy.rs index f32b0fd94e2..83d80e2f941 100644 --- a/crates/smoketests/tests/smoketests/default_module_clippy.rs +++ b/crates/smoketests/tests/smoketests/default_module_clippy.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::process::Command; fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")) .parent() .unwrap() .parent() diff --git a/crates/smoketests/tests/smoketests/namespaces.rs b/crates/smoketests/tests/smoketests/namespaces.rs index 3d632edd7e2..c2d5bbe008f 100644 --- a/crates/smoketests/tests/smoketests/namespaces.rs +++ b/crates/smoketests/tests/smoketests/namespaces.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")) .parent() .unwrap() .parent() diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index d1c9c92b19f..94c6b52da29 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -2,7 +2,7 @@ use spacetimedb_smoketests::Smoketest; use std::path::PathBuf; fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")) .parent() .unwrap() .parent() diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 41bc8619841..d0c12232ffb 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -4,7 +4,7 @@ use clap::{Args, Subcommand}; use duct::cmd; use spacetimedb_guard::ensure_binaries_built; use std::ffi::OsStr; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::{env, fs}; use tempfile::TempDir; @@ -51,22 +51,50 @@ enum SmoketestCmd { /// /// Use this before running `cargo test --all` to ensure binaries are built. Prepare, + /// Build the smoketest dependencies and create a nextest archive. + Archive { + /// Path to the nextest archive to create. + #[arg(long)] + archive_file: PathBuf, + }, + /// Run smoketests from an existing nextest archive without rebuilding. + RunArchive { + /// Path to the nextest archive to run. + #[arg(long)] + archive_file: PathBuf, + + /// Additional arguments to pass to nextest. + #[arg(trailing_var_arg = true)] + args: Vec, + }, CheckModList, } pub fn run(args: SmoketestsArgs) -> Result<()> { - match args.cmd { + let SmoketestsArgs { + cmd, + server, + auth_host, + dotnet, + args, + } = args; + + match cmd { Some(SmoketestCmd::Prepare) => { build_binaries()?; eprintln!("Binaries ready. You can now run `cargo test --all`."); Ok(()) } + Some(SmoketestCmd::Archive { archive_file }) => archive_smoketests(&archive_file), + Some(SmoketestCmd::RunArchive { archive_file, args }) => { + run_smoketest_archive(server, dotnet, auth_host.as_deref(), &archive_file, args) + } Some(SmoketestCmd::CheckModList) => { check_smoketests_mod_rs_complete()?; eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest(args.server, args.dotnet, args.auth_host.as_deref(), args.args), + None => run_smoketest(server, dotnet, auth_host.as_deref(), args), } } @@ -137,6 +165,29 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; +fn archive_smoketests(archive_file: &Path) -> Result<()> { + build_binaries()?; + build_precompiled_modules()?; + + eprintln!("Building and archiving smoketest binaries..."); + let status = Command::new("cargo") + .args([ + "nextest", + "archive", + "--release", + "-p", + "spacetimedb-smoketests", + "--archive-file", + ]) + .arg(archive_file) + .status() + .context("failed to run cargo nextest archive")?; + + ensure!(status.success(), "Failed to archive smoketest binaries"); + eprintln!("Smoketest archive written to {}.\n", archive_file.display()); + Ok(()) +} + fn run_smoketest(server: Option, dotnet: bool, auth_host: Option<&str>, args: Vec) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; @@ -202,6 +253,53 @@ fn run_smoketest(server: Option, dotnet: bool, auth_host: Option<&str>, Ok(()) } +fn run_smoketest_archive( + server: Option, + dotnet: bool, + auth_host: Option<&str>, + archive_file: &Path, + args: Vec, +) -> Result<()> { + let cli_path = ensure_binaries_built(); + let base_config_dir = prepare_base_config(&cli_path, server.as_deref(), auth_host)?; + let base_config_path = base_config_dir.path().join("config.toml"); + let workspace_root = env::current_dir().context("failed to resolve workspace root")?; + + if let Some(ref server_url) = server { + eprintln!("Running archived smoketests against remote server {server_url}...\n"); + } else { + eprintln!("Running smoketests from archive {}...\n", archive_file.display()); + } + + let mut cmd = Command::new("cargo"); + set_env(&mut cmd, server, dotnet, auth_host.is_some(), &base_config_path); + cmd.args(["nextest", "run", "--archive-file"]) + .arg(archive_file) + .arg("--workspace-remap") + .arg(workspace_root) + .arg("--no-fail-fast"); + + if !args + .iter() + .any(|a| a.starts_with("-j") || a.starts_with("--jobs") || a.starts_with("--test-threads")) + { + cmd.args(["-j", DEFAULT_PARALLELISM]); + } + + let status = cmd + .args(&args) + .status() + .context("failed to run smoketests from nextest archive")?; + ensure!(status.success(), "Tests failed"); + + let diff_status = cmd!("bash", "tools/check-diff.sh", "crates/smoketests").run()?; + ensure!( + diff_status.status.success(), + "There is a diff in the smoketests directory." + ); + Ok(()) +} + fn prepare_base_config(cli_path: &Path, server: Option<&str>, auth_host: Option<&str>) -> Result { if server.is_none() && auth_host.is_some() { bail!("--auth-host requires --server"); From 1daa84d90aa0731b56da4c4de02754ba9e125a6d Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 28 Jul 2026 15:35:39 -0700 Subject: [PATCH 02/31] fix docs --- tools/ci/README.md | 27 ++++++++++++++++++++++++++- tools/ci/src/smoketest.rs | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/ci/README.md b/tools/ci/README.md index 78147590f46..3cb0a382a2a 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -108,7 +108,7 @@ This is required for servers that reject direct server-issued logins for privile Optionally accepts an auth host to pass through to `spacetime login`, for example `--auth-host=https://spacetimedb.com`. -- `--dotnet `: +- `--dotnet `: Whether to run smoketests that require .NET - `args `: Additional arguments to pass to the test runner - `--help`: Print help (see a summary with '-h') @@ -127,6 +127,31 @@ Usage: prepare - `--help`: Print help (see a summary with '-h') +#### `archive` + +**Usage:** +```bash +Usage: archive --archive-file +``` + +**Options:** + +- `--archive-file `: Path to the nextest archive to create +- `--help`: Print help + +#### `run-archive` + +**Usage:** +```bash +Usage: run-archive --archive-file [ARGS]... +``` + +**Options:** + +- `--archive-file `: Path to the nextest archive to run +- `args `: Additional arguments to pass to nextest +- `--help`: Print help + #### `check-mod-list` **Usage:** diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index d0c12232ffb..cf6706cb574 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -37,6 +37,7 @@ pub struct SmoketestsArgs { #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "")] auth_host: Option, + /// Whether to run smoketests that require .NET. #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, From 5c426cb19bcf4387cc29923b7fc583e4612d10af Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 28 Jul 2026 16:01:16 -0700 Subject: [PATCH 03/31] update pnpm/action-setup to v6 --- .github/actions/setup-pnpm/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 2bf9ecdbdec..392302eb33e 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -16,7 +16,7 @@ inputs: runs: using: composite steps: - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: package_json_file: ${{ inputs.package_json_file }} From 7dbfaaddf651bd14ce2a9eacc531c0d34a01d14b Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 28 Jul 2026 16:59:04 -0700 Subject: [PATCH 04/31] revert pnpm/action-setup to v4 --- .github/actions/setup-pnpm/action.yml | 2 +- .github/actions/smoketest-build/action.yml | 4 --- .../smoketest-run-partition/action.yml | 2 -- .github/actions/smoketest-setup/action.yml | 13 -------- .github/workflows/ci.yml | 32 +++++++++++++++++++ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 392302eb33e..2bf9ecdbdec 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -16,7 +16,7 @@ inputs: runs: using: composite steps: - - uses: pnpm/action-setup@v6 + - uses: pnpm/action-setup@v4 with: package_json_file: ${{ inputs.package_json_file }} diff --git a/.github/actions/smoketest-build/action.yml b/.github/actions/smoketest-build/action.yml index 461ad7d03d9..4769f6f3f7a 100644 --- a/.github/actions/smoketest-build/action.yml +++ b/.github/actions/smoketest-build/action.yml @@ -9,10 +9,6 @@ inputs: runs: using: composite steps: - - uses: ./.github/actions/smoketest-setup - with: - runtime-dependencies: "false" - # This step shouldn't be needed, but some caches are missing librusty_v8.a. # This job mixes `cargo build -p` with `cargo build --manifest-path`, which can # produce different dependency trees in the same target directory. diff --git a/.github/actions/smoketest-run-partition/action.yml b/.github/actions/smoketest-run-partition/action.yml index 0846c633cda..7415c7eefb8 100644 --- a/.github/actions/smoketest-run-partition/action.yml +++ b/.github/actions/smoketest-run-partition/action.yml @@ -12,8 +12,6 @@ inputs: runs: using: composite steps: - - uses: ./.github/actions/smoketest-setup - - name: Download smoketest build uses: actions/download-artifact@v4 with: diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml index 45a5ef8a015..8d366dd89b0 100644 --- a/.github/actions/smoketest-setup/action.yml +++ b/.github/actions/smoketest-setup/action.yml @@ -41,19 +41,6 @@ runs: with: global-json-file: global.json - # Node.js and pnpm are required for the TypeScript quickstart smoketest. - - name: Set up Node.js - if: inputs.runtime-dependencies == 'true' - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Set up pnpm - if: inputs.runtime-dependencies == 'true' - uses: ./.github/actions/setup-pnpm - with: - run_install: true - # Install Emscripten for C++ module compilation tests. - name: Install Emscripten (Linux) if: inputs.runtime-dependencies == 'true' && runner.os == 'Linux' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12bc4934118..ba4e6f3965d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,10 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + - uses: ./.github/actions/smoketest-setup + with: + runtime-dependencies: "false" + - uses: ./.github/actions/smoketest-build with: artifact-name: smoketest-build-Linux @@ -129,6 +133,10 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + - uses: ./.github/actions/smoketest-setup + with: + runtime-dependencies: "false" + - uses: ./.github/actions/smoketest-build with: artifact-name: smoketest-build-Windows @@ -153,6 +161,18 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + - uses: ./.github/actions/smoketest-setup + + # Node.js and pnpm are required for the TypeScript quickstart smoketest. + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: ./.github/actions/setup-pnpm + with: + run_install: true + - uses: ./.github/actions/smoketest-run-partition with: artifact-name: smoketest-build-Linux @@ -178,6 +198,18 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + - uses: ./.github/actions/smoketest-setup + + # Node.js and pnpm are required for the TypeScript quickstart smoketest. + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: ./.github/actions/setup-pnpm + with: + run_install: true + - uses: ./.github/actions/smoketest-run-partition with: artifact-name: smoketest-build-Windows From 53f7cea13815f3e7170b4642ffd6700b3b9e53a6 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 11:21:58 -0700 Subject: [PATCH 05/31] pnpm caching --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba4e6f3965d..3c38a8aadf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,15 +163,24 @@ jobs: - uses: ./.github/actions/smoketest-setup - # Node.js and pnpm are required for the TypeScript quickstart smoketest. + # Node.js and pnpm are required for the TypeScript smoketests. + # pnpm must be available before setup-node can locate and cache its store. + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: package.json + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 18 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - - uses: ./.github/actions/setup-pnpm - with: - run_install: true + - name: Install pnpm dependencies + run: | + pnpm config set --global minimumReleaseAge 1440 + pnpm install --frozen-lockfile - uses: ./.github/actions/smoketest-run-partition with: @@ -200,15 +209,24 @@ jobs: - uses: ./.github/actions/smoketest-setup - # Node.js and pnpm are required for the TypeScript quickstart smoketest. + # Node.js and pnpm are required for the TypeScript smoketests. + # pnpm must be available before setup-node can locate and cache its store. + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: package.json + - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 18 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - - uses: ./.github/actions/setup-pnpm - with: - run_install: true + - name: Install pnpm dependencies + run: | + pnpm config set --global minimumReleaseAge 1440 + pnpm install --frozen-lockfile - uses: ./.github/actions/smoketest-run-partition with: From 408beb4f31aece7ff757333beb56e52bd46122ad Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 12:00:48 -0700 Subject: [PATCH 06/31] dependency-only caching in smoketest partitions --- .github/actions/smoketest-setup/action.yml | 12 +++++++++++- .github/workflows/ci.yml | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml index 8d366dd89b0..260b0d62457 100644 --- a/.github/actions/smoketest-setup/action.yml +++ b/.github/actions/smoketest-setup/action.yml @@ -6,6 +6,14 @@ inputs: description: Install dependencies needed while executing smoketests. required: false default: "true" + rust-cache-targets: + description: Cache Cargo target directories and installed binaries in addition to dependencies. + required: false + default: "true" + rust-cache-shared-key: + description: Shared key used for the Rust cache. + required: false + default: spacetimedb runs: using: composite @@ -29,7 +37,9 @@ runs: uses: Swatinem/rust-cache@v2 with: workspaces: ${{ github.workspace }} - shared-key: spacetimedb + shared-key: ${{ inputs.rust-cache-shared-key }} + cache-targets: ${{ inputs.rust-cache-targets }} + cache-bin: ${{ inputs.rust-cache-targets }} cache-on-failure: false cache-all-crates: true cache-workspace-crates: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c38a8aadf7..6f3f10f20f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,9 @@ jobs: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - uses: ./.github/actions/smoketest-setup + with: + rust-cache-targets: "false" + rust-cache-shared-key: spacetimedb-smoketest-runtime # Node.js and pnpm are required for the TypeScript smoketests. # pnpm must be available before setup-node can locate and cache its store. @@ -208,6 +211,9 @@ jobs: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - uses: ./.github/actions/smoketest-setup + with: + rust-cache-targets: "false" + rust-cache-shared-key: spacetimedb-smoketest-runtime # Node.js and pnpm are required for the TypeScript smoketests. # pnpm must be available before setup-node can locate and cache its store. From 0ad2bd6e375b50b49ec3ea6b70022ed4bcd0c581 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 12:30:32 -0700 Subject: [PATCH 07/31] fix pnpm policy lint to allow pnpm/action-setup@v4 --- tools/ci/src/main.rs | 117 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 111 insertions(+), 6 deletions(-) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 4d506a4e5f4..262346233a7 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -182,6 +182,59 @@ fn npmrc_minimum_release_age(path: &Path, expected_minimum_release_age: u64) -> }) } +fn workflow_job_bounds(lines: &[&str], line_index: usize) -> (usize, usize) { + let is_job_header = |line: &str| { + line.starts_with(" ") + && !line.starts_with(" ") + && !line.trim_start().starts_with('#') + && line.trim_end().ends_with(':') + }; + + let start = (0..line_index) + .rev() + .find(|&index| is_job_header(lines[index])) + .unwrap_or(0); + let end = (line_index + 1..lines.len()) + .find(|&index| is_job_header(lines[index])) + .unwrap_or(lines.len()); + (start, end) +} + +fn check_workflow_pnpm_release_age_policy( + workflow_path: &Path, + contents: &str, + expected_minimum_release_age: u64, +) -> Result<()> { + const DIRECT_PNPM_SETUP: &str = "uses: pnpm/action-setup@v4"; + let required_policy = format!("pnpm config set --global minimumReleaseAge {expected_minimum_release_age}"); + let lines: Vec<_> = contents.lines().collect(); + + for (setup_index, _) in lines + .iter() + .enumerate() + .filter(|(_, line)| line.trim().trim_start_matches("- ") == DIRECT_PNPM_SETUP) + { + let (_, job_end) = workflow_job_bounds(&lines, setup_index); + let following_steps = &lines[setup_index + 1..job_end]; + let policy_index = following_steps.iter().position(|line| line.contains(&required_policy)); + let install_index = following_steps.iter().position(|line| line.contains("pnpm install")); + let policy_is_missing_or_late = match policy_index { + Some(policy_index) => install_index.is_some_and(|install_index| policy_index > install_index), + None => true, + }; + + if policy_is_missing_or_late { + bail!( + "{} must run `{}` after pnpm/action-setup@v4 and before pnpm install", + workflow_path.display(), + required_policy + ); + } + } + + Ok(()) +} + fn check_pnpm_release_age_policy() -> Result<()> { ensure_repo_root()?; @@ -283,17 +336,69 @@ fn check_pnpm_release_age_policy() -> Result<()> { for workflow_path in git_tracked_files(".github/workflows/*")? { let contents = fs::read_to_string(&workflow_path)?; - if contents.contains("pnpm/action-setup@v4") { - bail!( - "{} must use ./.github/actions/setup-pnpm instead of pnpm/action-setup@v4", - workflow_path.display() - ); - } + check_workflow_pnpm_release_age_policy(&workflow_path, &contents, root_minimum_release_age)?; } Ok(()) } +#[cfg(test)] +mod tests { + use super::check_workflow_pnpm_release_age_policy; + use std::path::Path; + + const WORKFLOW_PATH: &str = ".github/workflows/ci.yml"; + + #[test] + fn ci_workflow_obeys_direct_pnpm_release_age_policy() { + let workflow = include_str!("../../../.github/workflows/ci.yml"); + check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap(); + } + + #[test] + fn direct_pnpm_setup_requires_release_age_before_install() { + let workflow = r#" +jobs: + smoketests: + steps: + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + - run: | + pnpm config set --global minimumReleaseAge 1440 + pnpm install --frozen-lockfile +"#; + check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap(); + } + + #[test] + fn direct_pnpm_setup_rejects_missing_release_age() { + let workflow = r#" +jobs: + smoketests: + steps: + - uses: pnpm/action-setup@v4 + - run: pnpm install --frozen-lockfile +"#; + let error = check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap_err(); + assert!(error.to_string().contains("minimumReleaseAge 1440")); + } + + #[test] + fn direct_pnpm_setup_rejects_release_age_after_install() { + let workflow = r#" +jobs: + smoketests: + steps: + - uses: pnpm/action-setup@v4 + - run: | + pnpm install --frozen-lockfile + pnpm config set --global minimumReleaseAge 1440 +"#; + let error = check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap_err(); + assert!(error.to_string().contains("before pnpm install")); + } +} + #[derive(Subcommand)] enum CiCmd { /// Runs tests From c02cfd0ccc9a57a5b9e4c6559c696851c6fb1591 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 15:35:35 -0700 Subject: [PATCH 08/31] stop rebuilding ts deps --- .github/actions/smoketest-build/action.yml | 1 + .../smoketest-run-partition/action.yml | 4 + .github/workflows/ci.yml | 92 +++++++++++++++++-- crates/smoketests/src/lib.rs | 25 ++++- .../smoketests/tests/smoketests/templates.rs | 13 +-- 5 files changed, 111 insertions(+), 24 deletions(-) diff --git a/.github/actions/smoketest-build/action.yml b/.github/actions/smoketest-build/action.yml index 4769f6f3f7a..ca6c084e524 100644 --- a/.github/actions/smoketest-build/action.yml +++ b/.github/actions/smoketest-build/action.yml @@ -41,6 +41,7 @@ runs: fi tar -czf smoketest-support.tar.gz \ + crates/bindings-typescript/dist \ "target/debug/ci${executable_suffix}" \ "target/release/spacetimedb-cli${executable_suffix}" \ "target/release/spacetimedb-standalone${executable_suffix}" \ diff --git a/.github/actions/smoketest-run-partition/action.yml b/.github/actions/smoketest-run-partition/action.yml index 7415c7eefb8..33e7578c2db 100644 --- a/.github/actions/smoketest-run-partition/action.yml +++ b/.github/actions/smoketest-run-partition/action.yml @@ -26,6 +26,8 @@ runs: - name: Run smoketest partition (Linux) if: runner.os == 'Linux' shell: bash + env: + SPACETIME_PREBUILT_TYPESCRIPT_SDK: "1" run: | if [ -f ~/emsdk/emsdk_env.sh ]; then source ~/emsdk/emsdk_env.sh @@ -40,6 +42,8 @@ runs: - name: Run smoketest partition (Windows) if: runner.os == 'Windows' shell: pwsh + env: + SPACETIME_PREBUILT_TYPESCRIPT_SDK: "1" run: | if (Test-Path "$env:USERPROFILE\emsdk\emsdk_env.ps1") { & "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f3f10f20f1..a7ef851427e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,8 +93,72 @@ jobs: echo "sha=${source_sha}" >>"$GITHUB_OUTPUT" + typescript_sdk_build: + needs: [merge_queue_noop] + if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} + name: Build TypeScript SDK + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + + - name: Restore TypeScript SDK build + id: typescript-sdk-cache + uses: actions/cache@v4 + with: + path: crates/bindings-typescript/dist + key: typescript-sdk-v1-node22-pnpm10.16.0-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'package.json', '.npmrc', 'crates/bindings-typescript/package.json', 'crates/bindings-typescript/.npmrc', 'crates/bindings-typescript/tsconfig*.json', 'crates/bindings-typescript/tsup.config.ts', 'crates/bindings-typescript/src/**') }} + + - name: Set up pnpm for TypeScript SDK build + if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' + uses: pnpm/action-setup@v4 + with: + package_json_file: package.json + + - name: Set up Node.js for TypeScript SDK build + if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Build TypeScript SDK + if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' + run: | + pnpm config set --global minimumReleaseAge 1440 + pnpm --filter spacetimedb... install --frozen-lockfile + pnpm --filter spacetimedb build + + - name: Validate TypeScript SDK build + shell: bash + run: | + typescript_sdk_dist=crates/bindings-typescript/dist + for typescript_sdk_artifact in \ + "$typescript_sdk_dist/index.mjs" \ + "$typescript_sdk_dist/index.d.ts" \ + "$typescript_sdk_dist/server/index.mjs" \ + "$typescript_sdk_dist/server/index.d.ts" + do + if [[ ! -f "$typescript_sdk_artifact" ]]; then + echo "Prebuilt TypeScript SDK artifact is missing: $typescript_sdk_artifact" + exit 1 + fi + done + + - name: Upload TypeScript SDK build + uses: actions/upload-artifact@v4 + with: + name: typescript-sdk-dist + path: crates/bindings-typescript/dist + if-no-files-found: error + overwrite: true + smoketest_build_linux: - needs: [merge_queue_noop, lints] + needs: [merge_queue_noop, lints, typescript_sdk_build] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} name: Build smoketests (Linux) runs-on: spacetimedb-new-runner-2 @@ -109,6 +173,12 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + - name: Download TypeScript SDK build + uses: actions/download-artifact@v4 + with: + name: typescript-sdk-dist + path: crates/bindings-typescript/dist + - uses: ./.github/actions/smoketest-setup with: runtime-dependencies: "false" @@ -118,7 +188,7 @@ jobs: artifact-name: smoketest-build-Linux smoketest_build_windows: - needs: [merge_queue_noop, lints] + needs: [merge_queue_noop, lints, typescript_sdk_build] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} name: Build smoketests (Windows) runs-on: spacetimedb-windows-runner @@ -133,6 +203,12 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + - name: Download TypeScript SDK build + uses: actions/download-artifact@v4 + with: + name: typescript-sdk-dist + path: crates/bindings-typescript/dist + - uses: ./.github/actions/smoketest-setup with: runtime-dependencies: "false" @@ -180,10 +256,8 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Install pnpm dependencies - run: | - pnpm config set --global minimumReleaseAge 1440 - pnpm install --frozen-lockfile + - name: Configure pnpm package age policy + run: pnpm config set --global minimumReleaseAge 1440 - uses: ./.github/actions/smoketest-run-partition with: @@ -229,10 +303,8 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Install pnpm dependencies - run: | - pnpm config set --global minimumReleaseAge 1440 - pnpm install --frozen-lockfile + - name: Configure pnpm package age policy + run: pnpm config set --global minimumReleaseAge 1440 - uses: ./.github/actions/smoketest-run-partition with: diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index a89a09e7537..59dc20d346e 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -393,8 +393,29 @@ pub fn pnpm(args: &[&str], cwd: &Path) -> Result { pub fn build_typescript_sdk() -> Result<()> { let workspace = workspace_root(); let ts_bindings = workspace.join("crates/bindings-typescript"); - pnpm(&["install"], &ts_bindings)?; - pnpm(&["build"], &ts_bindings)?; + if std::env::var("SPACETIME_PREBUILT_TYPESCRIPT_SDK").ok().as_deref() == Some("1") { + for relative_path in [ + "dist/index.mjs", + "dist/index.d.ts", + "dist/server/index.mjs", + "dist/server/index.d.ts", + ] { + let artifact = ts_bindings.join(relative_path); + if !artifact.is_file() { + bail!( + "SPACETIME_PREBUILT_TYPESCRIPT_SDK is set, but the TypeScript SDK artifact is missing: {}", + artifact.display() + ); + } + } + return Ok(()); + } + + pnpm( + &["--filter", "spacetimedb...", "install", "--frozen-lockfile"], + &workspace, + )?; + pnpm(&["--filter", "spacetimedb", "build"], &workspace)?; Ok(()) } diff --git a/crates/smoketests/tests/smoketests/templates.rs b/crates/smoketests/tests/smoketests/templates.rs index 37b23f1a51a..793fd5659af 100644 --- a/crates/smoketests/tests/smoketests/templates.rs +++ b/crates/smoketests/tests/smoketests/templates.rs @@ -13,7 +13,7 @@ use anyhow::{bail, Context, Result}; use regex::Regex; use serde_json::Value; use spacetimedb_guard::ensure_binaries_built; -use spacetimedb_smoketests::{pnpm, random_string, workspace_root, Smoketest}; +use spacetimedb_smoketests::{build_typescript_sdk, pnpm, random_string, workspace_root, Smoketest}; use std::env; use std::fs; use std::path::{Path, PathBuf}; @@ -722,17 +722,6 @@ fn pin_csharp_client_sdk_package_version(project_path: &Path) -> Result<()> { Ok(()) } -/// Builds the TypeScript SDK (`crates/bindings-typescript`). -/// -/// Should be called once before testing any TypeScript templates. -fn build_typescript_sdk() -> Result<()> { - let sdk_path = workspace_root().join("crates/bindings-typescript"); - eprintln!("[TEMPLATES] Building TypeScript SDK at {:?}", sdk_path); - run_pnpm(&["install"], &sdk_path)?; - run_pnpm(&["build"], &sdk_path)?; - Ok(()) -} - /// Points the `spacetimedb` entry in `package.json` at the local TypeScript /// SDK and removes the template's lockfile so pnpm re-resolves dependencies. fn setup_typescript_sdk_in_package_json(package_json_path: &Path) -> Result<()> { From 64ec710782e8305782d9fca361f19490a3d9c900 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 15:47:08 -0700 Subject: [PATCH 09/31] remove rust-cache from partition jobs --- .github/actions/smoketest-setup/action.yml | 5 +++++ .github/workflows/ci.yml | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml index 260b0d62457..d18b053219c 100644 --- a/.github/actions/smoketest-setup/action.yml +++ b/.github/actions/smoketest-setup/action.yml @@ -6,6 +6,10 @@ inputs: description: Install dependencies needed while executing smoketests. required: false default: "true" + rust-cache-enabled: + description: Restore and save the Rust cache. + required: false + default: "true" rust-cache-targets: description: Cache Cargo target directories and installed binaries in addition to dependencies. required: false @@ -34,6 +38,7 @@ runs: echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" - name: Cache Rust dependencies + if: inputs.rust-cache-enabled == 'true' uses: Swatinem/rust-cache@v2 with: workspaces: ${{ github.workspace }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7ef851427e..6560a1d93aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -239,8 +239,7 @@ jobs: - uses: ./.github/actions/smoketest-setup with: - rust-cache-targets: "false" - rust-cache-shared-key: spacetimedb-smoketest-runtime + rust-cache-enabled: "false" # Node.js and pnpm are required for the TypeScript smoketests. # pnpm must be available before setup-node can locate and cache its store. @@ -286,8 +285,7 @@ jobs: - uses: ./.github/actions/smoketest-setup with: - rust-cache-targets: "false" - rust-cache-shared-key: spacetimedb-smoketest-runtime + rust-cache-enabled: "false" # Node.js and pnpm are required for the TypeScript smoketests. # pnpm must be available before setup-node can locate and cache its store. From 4a14feee4e5e32a1de5860a4601acfd25c0d9ba3 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 16:45:49 -0700 Subject: [PATCH 10/31] cargo -- timings --- .github/actions/smoketest-build/action.yml | 9 +++++++++ tools/ci/src/smoketest.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/.github/actions/smoketest-build/action.yml b/.github/actions/smoketest-build/action.yml index ca6c084e524..604a02b3bd1 100644 --- a/.github/actions/smoketest-build/action.yml +++ b/.github/actions/smoketest-build/action.yml @@ -47,6 +47,15 @@ runs: "target/release/spacetimedb-standalone${executable_suffix}" \ "${precompiled_modules[@]}" + - name: Upload Cargo timings + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }}-cargo-timings + path: ${{ env.CARGO_TARGET_DIR }}/cargo-timings + if-no-files-found: warn + overwrite: true + - name: Upload smoketest build uses: actions/upload-artifact@v4 with: diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index cf6706cb574..e4010d5ff98 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -105,6 +105,7 @@ fn build_binaries() -> Result<()> { let mut cmd = Command::new("cargo"); cmd.args([ "build", + "--timings", "--release", "-p", "spacetimedb-cli", From 82fad413514497d190f372dc84a5e74ef287ad37 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 16:46:56 -0700 Subject: [PATCH 11/31] remove rust-cache from build jobs --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6560a1d93aa..0ed270e23d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,7 @@ jobs: - uses: ./.github/actions/smoketest-setup with: runtime-dependencies: "false" + rust-cache-enabled: "false" - uses: ./.github/actions/smoketest-build with: @@ -212,6 +213,7 @@ jobs: - uses: ./.github/actions/smoketest-setup with: runtime-dependencies: "false" + rust-cache-enabled: "false" - uses: ./.github/actions/smoketest-build with: From 280a8603be9e8354bb90835d36ac70452289b5db Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 17:08:22 -0700 Subject: [PATCH 12/31] fix pnpm build --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ed270e23d9..289241d131b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -260,6 +260,9 @@ jobs: - name: Configure pnpm package age policy run: pnpm config set --global minimumReleaseAge 1440 + - name: Install TypeScript SDK runtime dependencies + run: pnpm --filter spacetimedb install --prod --frozen-lockfile --prefer-offline + - uses: ./.github/actions/smoketest-run-partition with: artifact-name: smoketest-build-Linux @@ -306,6 +309,9 @@ jobs: - name: Configure pnpm package age policy run: pnpm config set --global minimumReleaseAge 1440 + - name: Install TypeScript SDK runtime dependencies + run: pnpm --filter spacetimedb install --prod --frozen-lockfile --prefer-offline + - uses: ./.github/actions/smoketest-run-partition with: artifact-name: smoketest-build-Windows From 3b4d6825ba994086a642c4500e25d235b8b1ec90 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 17:26:21 -0700 Subject: [PATCH 13/31] have all tests restore from the smoketest build --- .../actions/keynote-bench-setup/action.yml | 10 -- .../restore-smoketest-build/action.yml | 33 +++++ .github/workflows/ci.yml | 113 ++++-------------- 3 files changed, 57 insertions(+), 99 deletions(-) create mode 100644 .github/actions/restore-smoketest-build/action.yml diff --git a/.github/actions/keynote-bench-setup/action.yml b/.github/actions/keynote-bench-setup/action.yml index 8fa03d744f7..9591cb2ab85 100644 --- a/.github/actions/keynote-bench-setup/action.yml +++ b/.github/actions/keynote-bench-setup/action.yml @@ -46,13 +46,3 @@ runs: cargo build --release -p v8 fi working-directory: ${{ inputs.public_root }} - - - name: Build public keynote benchmark binaries - shell: bash - run: cargo build --release -p spacetimedb-cli -p spacetimedb-standalone - working-directory: ${{ inputs.public_root }} - - - name: Build TypeScript SDK - shell: bash - run: pnpm build - working-directory: ${{ inputs.public_root }}/crates/bindings-typescript diff --git a/.github/actions/restore-smoketest-build/action.yml b/.github/actions/restore-smoketest-build/action.yml new file mode 100644 index 00000000000..d93ffed7b65 --- /dev/null +++ b/.github/actions/restore-smoketest-build/action.yml @@ -0,0 +1,33 @@ +name: Restore smoketest build +description: Restore prebuilt Linux CLI and standalone binaries from a smoketest build artifact. + +inputs: + artifact-name: + description: Name of the smoketest build artifact to download. + required: true + support-archive: + description: Path to the support archive within the downloaded artifact. + required: false + default: smoketest-support.tar.gz + +runs: + using: composite + steps: + - name: Download smoketest build + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: ${{ runner.temp }}/smoketest-build + + - name: Extract smoketest support files + shell: bash + run: tar -xzf "${RUNNER_TEMP}/smoketest-build/${{ inputs.support-archive }}" + + - name: Configure prebuilt SpacetimeDB binaries + shell: bash + run: | + test -x "${CARGO_TARGET_DIR}/release/spacetimedb-cli" + test -x "${CARGO_TARGET_DIR}/release/spacetimedb-standalone" + ln -sf spacetimedb-cli "${CARGO_TARGET_DIR}/release/spacetime" + echo "${CARGO_TARGET_DIR}/release" >>"$GITHUB_PATH" + echo "SPACETIME_PREBUILT_BINARIES=1" >>"$GITHUB_ENV" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 289241d131b..3c6b89e30bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -451,7 +451,7 @@ jobs: cargo ci test keynote_bench: - needs: [merge_queue_noop, lints] + needs: [merge_queue_noop, lints, smoketest_build_linux] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} name: Keynote Bench runs-on: spacetimedb-benchmark-runner @@ -463,22 +463,10 @@ jobs: CARGO_TARGET_DIR: ${{ github.workspace }}/target RUST_BACKTRACE: full steps: - - name: Find Git ref - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUMBER="${{ github.event.inputs.pr_number || null }}" - if test -n "${PR_NUMBER}"; then - GIT_REF="$( gh pr view --repo clockworklabs/SpacetimeDB $PR_NUMBER --json headRefName --jq .headRefName )" - else - GIT_REF="${{ github.ref }}" - fi - echo "GIT_REF=${GIT_REF}" >>"$GITHUB_ENV" - - name: Checkout sources uses: actions/checkout@v4 with: - ref: ${{ env.GIT_REF }} + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} # Node 24 is the current Active LTS line. - name: Set up Node.js @@ -497,6 +485,10 @@ jobs: public_root: . rust_cache_workspaces: ${{ github.workspace }} + - uses: ./.github/actions/restore-smoketest-build + with: + artifact-name: smoketest-build-Linux + - name: Run keynote-2 benchmark regression check run: cargo ci keynote-bench @@ -883,7 +875,7 @@ jobs: cargo ci cli-docs unity-testsuite: - needs: [merge_queue_noop, lints] + needs: [merge_queue_noop, lints, smoketest_build_linux] # Skip if this is an external contribution. # The license secrets will be empty, so the step would fail anyway. if: ${{ needs.merge_queue_noop.outputs.skip != 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) }} @@ -899,6 +891,8 @@ jobs: - name: Checkout repository id: checkout-stdb uses: actions/checkout@v4 + with: + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} # Run cheap .NET tests first. If those fail, no need to run expensive Unity tests. @@ -946,28 +940,9 @@ jobs: save-if: false prefix-key: v1 - # This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a. - # ChatGPT suspects that this could be due to different build invocations using the same target dir, - # and this makes sense to me because we only see it in this job where we mix `cargo build -p` with - # `cargo build --manifest-path` (which apparently build different dependency trees). - # However, we've been unable to fix it so... /shrug - - name: Check v8 outputs - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean --release -p v8 || true - cargo build --release -p v8 - fi - - - name: Install SpacetimeDB CLI from the local checkout - run: | - export CARGO_HOME="$HOME/.cargo" - echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - cargo install --force --path crates/cli --locked --message-format=short - cargo install --force --path crates/standalone --locked --message-format=short - # Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules). - ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime + - uses: ./.github/actions/restore-smoketest-build + with: + artifact-name: smoketest-build-Linux - name: Generate client bindings working-directory: demo/Blackholio/server-rust @@ -1029,7 +1004,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} godot-testsuite: - needs: [merge_queue_noop, lints] + needs: [merge_queue_noop, lints, smoketest_build_linux] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} permissions: contents: read @@ -1041,6 +1016,8 @@ jobs: - name: Checkout repository id: checkout-stdb uses: actions/checkout@v4 + with: + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - name: Setup dotnet uses: actions/setup-dotnet@v3 @@ -1086,28 +1063,9 @@ jobs: save-if: false prefix-key: v1 - # This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a. - # ChatGPT suspects that this could be due to different build invocations using the same target dir, - # and this makes sense to me because we only see it in this job where we mix `cargo build -p` with - # `cargo build --manifest-path` (which apparently build different dependency trees). - # However, we've been unable to fix it so... /shrug - - name: Check v8 outputs - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean --release -p v8 || true - cargo build --release -p v8 - fi - - - name: Install SpacetimeDB CLI from the local checkout - run: | - export CARGO_HOME="$HOME/.cargo" - echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - cargo install --force --path crates/cli --locked --message-format=short - cargo install --force --path crates/standalone --locked --message-format=short - # Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules). - ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime + - uses: ./.github/actions/restore-smoketest-build + with: + artifact-name: smoketest-build-Linux - name: Generate client bindings working-directory: demo/Blackholio/server-rust @@ -1157,7 +1115,7 @@ jobs: run: godot --headless --scene res://tests/GodotPlayModeTests.tscn csharp-testsuite: - needs: [merge_queue_noop, lints] + needs: [merge_queue_noop, lints, smoketest_build_linux] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} runs-on: spacetimedb-new-runner-2 timeout-minutes: 30 @@ -1168,6 +1126,8 @@ jobs: - name: Checkout repository id: checkout-stdb uses: actions/checkout@v4 + with: + ref: ${{ needs.merge_queue_noop.outputs.source_sha }} - name: Setup dotnet uses: actions/setup-dotnet@v3 @@ -1224,34 +1184,9 @@ jobs: save-if: false prefix-key: v1 - # This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a. - # ChatGPT suspects that this could be due to different build invocations using the same target dir, - # and this makes sense to me because we only see it in this job where we mix `cargo build -p` with - # `cargo build --manifest-path` (which apparently build different dependency trees). - # However, we've been unable to fix it so... /shrug - - name: Check v8 outputs - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/debug/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean -p v8 || true - cargo build -p v8 - fi - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean --release -p v8 || true - cargo build --release -p v8 - fi - - - name: Install SpacetimeDB CLI from the local checkout - run: | - export CARGO_HOME="$HOME/.cargo" - echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - cargo install --force --path crates/cli --locked --message-format=short - cargo install --force --path crates/standalone --features allow_loopback_http_for_tests --locked --message-format=short - # Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules). - ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime + - uses: ./.github/actions/restore-smoketest-build + with: + artifact-name: smoketest-build-Linux - name: Check quickstart-chat bindings are up to date run: | From 04920ef95b18431ed2b817cdb4b9120651342f95 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 17:43:05 -0700 Subject: [PATCH 14/31] use SPACETIMEDB_WORKSPACE_ROOT instead of CARGO_MANIFEST_DIR --- crates/guard/src/lib.rs | 7 +++++++ crates/smoketests/src/lib.rs | 7 +++++++ .../tests/smoketests/default_module_clippy.rs | 11 +---------- crates/smoketests/tests/smoketests/namespaces.rs | 13 ++----------- crates/smoketests/tests/smoketests/permissions.rs | 12 +----------- 5 files changed, 18 insertions(+), 32 deletions(-) diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index d292e417ac7..24d14d956bf 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -24,6 +24,13 @@ fn next_spawn_id() -> u64 { /// Returns the workspace root directory. // TODO: Should this use something like `git rev-parse --show-toplevel` to avoid being directory-relative? Or perhaps `CARGO_WORKSPACE_DIR` is set? fn workspace_root() -> PathBuf { + // Private CI builds this crate into a nextest archive in one job and runs it in + // another, so prefer the public root supplied by the runner instead of embedding + // the build job's absolute path with `env!`. + if let Ok(workspace_root) = env::var("SPACETIMEDB_WORKSPACE_ROOT") { + return PathBuf::from(workspace_root); + } + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")); manifest_dir diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 59dc20d346e..33c60e2b935 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -180,6 +180,13 @@ macro_rules! timed { /// Returns the workspace root directory. pub fn workspace_root() -> PathBuf { + // Nextest archives are built and executed in different jobs, so prefer the + // current public checkout supplied by the runner instead of embedding the build + // job's absolute path with `env!`. + if let Ok(workspace_root) = std::env::var("SPACETIMEDB_WORKSPACE_ROOT") { + return PathBuf::from(workspace_root); + } + let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")); manifest_dir diff --git a/crates/smoketests/tests/smoketests/default_module_clippy.rs b/crates/smoketests/tests/smoketests/default_module_clippy.rs index 83d80e2f941..73e76f40357 100644 --- a/crates/smoketests/tests/smoketests/default_module_clippy.rs +++ b/crates/smoketests/tests/smoketests/default_module_clippy.rs @@ -1,17 +1,8 @@ //! These tests verify that the Rust module templates have no clippy warnings. -use std::path::PathBuf; +use spacetimedb_smoketests::workspace_root; use std::process::Command; -fn workspace_root() -> PathBuf { - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() -} - /// Run clippy on a template's spacetimedb module directory. /// Both templates use workspace dependencies, so they can be checked in place. fn check_template_clippy(template_name: &str) { diff --git a/crates/smoketests/tests/smoketests/namespaces.rs b/crates/smoketests/tests/smoketests/namespaces.rs index c2d5bbe008f..971de3ae027 100644 --- a/crates/smoketests/tests/smoketests/namespaces.rs +++ b/crates/smoketests/tests/smoketests/namespaces.rs @@ -1,15 +1,6 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{workspace_root, Smoketest}; use std::fs; -use std::path::{Path, PathBuf}; - -fn workspace_root() -> PathBuf { - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() -} +use std::path::Path; /// Count occurrences of a needle string in all .cs files under a directory fn count_matches(dir: &Path, needle: &str) -> usize { diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index 94c6b52da29..f6cf93c0d3d 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -1,14 +1,4 @@ -use spacetimedb_smoketests::Smoketest; -use std::path::PathBuf; - -fn workspace_root() -> PathBuf { - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() -} +use spacetimedb_smoketests::{workspace_root, Smoketest}; /// Ensure that anyone has the permission to call any standard reducer #[test] From 59baa45f5a83f1c83358bd0fb66260b0329ad1de Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 21:27:26 -0700 Subject: [PATCH 15/31] cache dependencies for v8 and openssl --- .github/actions/smoketest-build/action.yml | 13 ------- .github/actions/smoketest-setup/action.yml | 8 ++-- .github/workflows/ci.yml | 43 +++++++++++++++++++++- tools/ci/src/smoketest.rs | 12 ++++-- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/.github/actions/smoketest-build/action.yml b/.github/actions/smoketest-build/action.yml index 604a02b3bd1..fde5d085b65 100644 --- a/.github/actions/smoketest-build/action.yml +++ b/.github/actions/smoketest-build/action.yml @@ -9,19 +9,6 @@ inputs: runs: using: composite steps: - # This step shouldn't be needed, but some caches are missing librusty_v8.a. - # This job mixes `cargo build -p` with `cargo build --manifest-path`, which can - # produce different dependency trees in the same target directory. - - name: Check v8 outputs - shell: bash - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean --release -p v8 || true - cargo build --release -p v8 - fi - - name: Build smoketest dependencies and archive test binaries shell: bash run: | diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml index d18b053219c..7f56f38d53d 100644 --- a/.github/actions/smoketest-setup/action.yml +++ b/.github/actions/smoketest-setup/action.yml @@ -11,7 +11,7 @@ inputs: required: false default: "true" rust-cache-targets: - description: Cache Cargo target directories and installed binaries in addition to dependencies. + description: Cache compiled dependency artifacts in Cargo target directories. required: false default: "true" rust-cache-shared-key: @@ -44,10 +44,10 @@ runs: workspaces: ${{ github.workspace }} shared-key: ${{ inputs.rust-cache-shared-key }} cache-targets: ${{ inputs.rust-cache-targets }} - cache-bin: ${{ inputs.rust-cache-targets }} + cache-bin: false cache-on-failure: false - cache-all-crates: true - cache-workspace-crates: true + cache-all-crates: false + cache-workspace-crates: false prefix-key: v1 - name: Set up .NET diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c6b89e30bc..10aa55c806b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,9 @@ jobs: timeout-minutes: 120 env: CARGO_TARGET_DIR: ${{ github.workspace }}/target + # Keep cross-crate ThinLTO for the Linux build consumed by benchmarks, but + # skip it for Windows smoketest binaries where build latency matters more. + CARGO_PROFILE_RELEASE_LTO: "false" RUST_BACKTRACE: full SPACETIMEDB_CPP_DIR: ${{ github.workspace }}/crates/bindings-cpp steps: @@ -213,9 +216,47 @@ jobs: - uses: ./.github/actions/smoketest-setup with: runtime-dependencies: "false" - rust-cache-enabled: "false" + rust-cache-enabled: "true" + rust-cache-targets: "true" + rust-cache-shared-key: smoketest-build-dependencies + + - name: Resolve V8 version + id: v8-version + shell: bash + run: | + v8_version="$(sed -n 's/^v8 = "=\([^"]*\)"$/\1/p' Cargo.toml)" + test -n "${v8_version}" + echo "v8=${v8_version}" >> "${GITHUB_OUTPUT}" + + - name: Restore native V8 library + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/spacetimedb-native-cache/rusty-v8/rusty_v8.lib.gz + key: native-v1-rusty-v8-${{ steps.v8-version.outputs.v8 }}-x86_64-pc-windows-msvc-release-pc0-sb0 + + - name: Prepare native V8 library + shell: pwsh + env: + V8_VERSION: ${{ steps.v8-version.outputs.v8 }} + run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + $cacheDir = Join-Path '${{ runner.temp }}' 'spacetimedb-native-cache/rusty-v8' + $archive = Join-Path $cacheDir 'rusty_v8.lib.gz' + New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null + + if (-not (Test-Path $archive)) { + $url = "https://github.com/denoland/rusty_v8/releases/download/v$env:V8_VERSION/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz" + $download = "$archive.tmp" + Remove-Item -Force -ErrorAction SilentlyContinue $download + & curl.exe --fail --location --retry 3 --output $download $url + Move-Item -Force $download $archive + } - uses: ./.github/actions/smoketest-build + env: + # rust-cache removes V8's nonstandard target/release/gn_out directory. + RUSTY_V8_ARCHIVE: ${{ runner.temp }}/spacetimedb-native-cache/rusty-v8/rusty_v8.lib.gz with: artifact-name: smoketest-build-Windows diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index e4010d5ff98..0fa5fcc08ae 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -116,10 +116,16 @@ fn build_binaries() -> Result<()> { ]); // Remove cargo/rust env vars that could cause fingerprint mismatches - // when the test later runs cargo build from a different environment + // when the test later runs cargo build from a different environment. + // CI intentionally overrides release LTO on Windows and supplies a cached + // V8 archive, so preserve those settings across every build performed by + // the smoketest build job. for (key, _) in env::vars() { - let should_remove = (key.starts_with("CARGO") && key != "CARGO_HOME" && key != "CARGO_TARGET_DIR") - || key.starts_with("RUST") + let should_remove = (key.starts_with("CARGO") + && key != "CARGO_HOME" + && key != "CARGO_TARGET_DIR" + && key != "CARGO_PROFILE_RELEASE_LTO") + || (key.starts_with("RUST") && key != "RUSTY_V8_ARCHIVE") // > The environment variable `__CARGO_FIX_YOLO` is an undocumented, internal-use-only feature // > for the Rust cargo fix command (and cargo clippy --fix) that forces the application of all // > available suggestions, including those that are marked as potentially incorrect or dangerous. From 74f6dca6160871aa9cdb5499b64863f679d2e4ec Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 21:46:25 -0700 Subject: [PATCH 16/31] Make windows smoketest build job the only rust-cache writer --- .../actions/keynote-bench-setup/action.yml | 28 ---- .github/workflows/ci.yml | 150 ------------------ .github/workflows/llm-benchmark-periodic.yml | 1 - .../llm-benchmark-validate-goldens.yml | 1 - tools/ci/src/smoketest.rs | 40 +++-- 5 files changed, 23 insertions(+), 197 deletions(-) diff --git a/.github/actions/keynote-bench-setup/action.yml b/.github/actions/keynote-bench-setup/action.yml index 9591cb2ab85..59d900fd7b1 100644 --- a/.github/actions/keynote-bench-setup/action.yml +++ b/.github/actions/keynote-bench-setup/action.yml @@ -1,15 +1,6 @@ name: Keynote Bench Setup description: Shared setup for the keynote benchmark job -inputs: - public_root: - description: Path to the public SpacetimeDB checkout in the caller workspace - required: false - default: . - rust_cache_workspaces: - description: Workspace paths passed to Swatinem/rust-cache - required: true - runs: using: composite steps: @@ -27,22 +18,3 @@ runs: run: | echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ inputs.rust_cache_workspaces }} - shared-key: spacetimedb - save-if: false - prefix-key: v1 - - - name: Check v8 outputs - shell: bash - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/release/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean --release -p v8 || true - cargo build --release -p v8 - fi - working-directory: ${{ inputs.public_root }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10aa55c806b..2a3c79b77df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -419,15 +419,6 @@ jobs: - uses: dsherret/rust-toolchain-file@v1 - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the smoketests job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - name: Check v8 outputs shell: bash run: | @@ -522,9 +513,6 @@ jobs: run_install: "true" - uses: ./.github/actions/keynote-bench-setup - with: - public_root: . - rust_cache_workspaces: ${{ github.workspace }} - uses: ./.github/actions/restore-smoketest-build with: @@ -575,14 +563,6 @@ jobs: echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb-index-scan - cache-on-failure: false - prefix-key: v1 - - name: Run index scan benchmark regression check run: cargo run --release -p spacetimedb-index-scan-gate @@ -603,15 +583,6 @@ jobs: run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - run: echo ::add-matcher::.github/workflows/rust_matcher.json - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the smoketests job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - uses: actions/setup-dotnet@v3 with: global-json-file: global.json @@ -644,15 +615,6 @@ jobs: run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - run: echo ::add-matcher::.github/workflows/rust_matcher.json - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the smoketests job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - name: Run bindgen tests run: cargo ci wasm-bindings @@ -902,15 +864,6 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the smoketests job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - name: Check for docs change run: | cargo ci cli-docs @@ -972,15 +925,6 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the main CI job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - uses: ./.github/actions/restore-smoketest-build with: artifact-name: smoketest-build-Linux @@ -1095,15 +1039,6 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the main CI job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - uses: ./.github/actions/restore-smoketest-build with: artifact-name: smoketest-build-Linux @@ -1216,15 +1151,6 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the main CI job save the cache since it builds the most things - save-if: false - prefix-key: v1 - - uses: ./.github/actions/restore-smoketest-build with: artifact-name: smoketest-build-Linux @@ -1307,76 +1233,9 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - save-if: false - prefix-key: v1 - - name: Check global.json policy run: cargo ci global-json-policy - smoketests_mod_rs_complete: - needs: [merge_queue_noop] - if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} - name: Check smoketests/mod.rs is complete - runs-on: ubuntu-latest - permissions: - contents: read - env: - CARGO_TARGET_DIR: ${{ github.workspace }}/target - RUST_BACKTRACE: full - steps: - - name: Find Git ref - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - PR_NUMBER="${{ github.event.inputs.pr_number || null }}" - if test -n "${PR_NUMBER}"; then - GIT_REF="$( gh pr view --repo clockworklabs/SpacetimeDB $PR_NUMBER --json headRefName --jq .headRefName )" - else - GIT_REF="${{ github.ref }}" - fi - echo "GIT_REF=${GIT_REF}" >>"$GITHUB_ENV" - - name: Checkout sources - uses: actions/checkout@v4 - with: - ref: ${{ env.GIT_REF }} - - uses: dsherret/rust-toolchain-file@v1 - - name: Set default rust toolchain - run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - cache-on-failure: false - cache-all-crates: true - cache-workspace-crates: true - prefix-key: v1 - - # This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a. - # ChatGPT suspects that this could be due to different build invocations using the same target dir, - # and this makes sense to me because we only see it in this job where we mix `cargo build -p` with - # `cargo build --manifest-path` (which apparently build different dependency trees). - # However, we've been unable to fix it so... /shrug - - name: Check v8 outputs - shell: bash - run: | - find "${CARGO_TARGET_DIR}"/ -type f | grep '[/_]v8' || true - if ! [ -f "${CARGO_TARGET_DIR}"/debug/gn_out/obj/librusty_v8.a ]; then - echo "Could not find v8 output file librusty_v8.a; rebuilding manually." - cargo clean -p v8 || true - cargo build -p v8 - fi - - - name: Verify crates/smoketests/tests/smoketests/mod.rs lists all entries - run: | - cargo ci smoketests check-mod-list - docs-build: needs: [merge_queue_noop] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} @@ -1482,15 +1341,6 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: ${{ github.workspace }} - shared-key: spacetimedb - # Let the main CI job save the cache since it builds the most things - save-if: false - prefix-key: v1 - # # This step shouldn't be needed, but somehow we end up with caches that are missing librusty_v8.a. # # ChatGPT suspects that this could be due to different build invocations using the same target dir, # # and this makes sense to me because we only see it in this job where we mix `cargo build -p` with diff --git a/.github/workflows/llm-benchmark-periodic.yml b/.github/workflows/llm-benchmark-periodic.yml index 8e46d9ebc2b..38235bb13ae 100644 --- a/.github/workflows/llm-benchmark-periodic.yml +++ b/.github/workflows/llm-benchmark-periodic.yml @@ -58,7 +58,6 @@ jobs: fetch-depth: 1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/llm-benchmark-validate-goldens.yml b/.github/workflows/llm-benchmark-validate-goldens.yml index a2d2ef87a3e..086ac20a0cd 100644 --- a/.github/workflows/llm-benchmark-validate-goldens.yml +++ b/.github/workflows/llm-benchmark-validate-goldens.yml @@ -41,7 +41,6 @@ jobs: fetch-depth: 1 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - name: Setup .NET SDK if: matrix.lang == 'csharp' diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 0fa5fcc08ae..319d9337ddf 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -3,7 +3,6 @@ use anyhow::{bail, ensure, Context, Result}; use clap::{Args, Subcommand}; use duct::cmd; use spacetimedb_guard::ensure_binaries_built; -use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::{env, fs}; @@ -174,6 +173,8 @@ fn build_precompiled_modules() -> Result<()> { const DEFAULT_PARALLELISM: &str = "16"; fn archive_smoketests(archive_file: &Path) -> Result<()> { + // Validate the source module list before doing any expensive compilation. + check_smoketests_mod_rs_complete()?; build_binaries()?; build_precompiled_modules()?; @@ -402,27 +403,23 @@ fn check_smoketests_mod_rs_complete() -> Result<()> { let ft = entry.file_type()?; if ft.is_dir() { expected.insert(name.to_string()); - } else if ft.is_file() - && path.extension() == Some(OsStr::new("rs")) - && let Some(stem) = path.file_stem() - { + } else if ft.is_file() && path.extension().is_some_and(|extension| extension == "rs") { + let stem = path.file_stem().context("Rust smoketest module has no file stem")?; expected.insert(stem.to_string_lossy().to_string()); } } - let out = cmd!("cargo", "test", "-p", "spacetimedb-smoketests", "--", "--list",).read()?; - let mut present = std::collections::BTreeSet::::new(); - for line in out.lines() { - let line = line.trim(); - let parts: Vec<&str> = line.split("::").collect(); - if parts.len() < 2 { - continue; + let mod_rs = fs::read_to_string(expected_dir.join("mod.rs"))?; + for line in mod_rs.lines() { + let line = line.split_once("//").map_or(line, |(code, _)| code).trim(); + let declaration = line.strip_prefix("mod ").or_else(|| line.strip_prefix("pub mod ")); + if let Some(module) = declaration.and_then(|declaration| declaration.strip_suffix(';')) { + let module = module.trim(); + if is_rust_identifier(module) { + present.insert(module.strip_prefix("r#").unwrap_or(module).to_string()); + } } - if parts[0] != "smoketests" { - continue; - } - present.insert(parts[1].to_string()); } let missing = expected @@ -432,7 +429,7 @@ fn check_smoketests_mod_rs_complete() -> Result<()> { if !missing.is_empty() { bail!( - "crates/smoketests/tests/smoketests/mod.rs appears incomplete; missing modules (not present in `cargo test -- --list`):\n{}", + "crates/smoketests/tests/smoketests/mod.rs appears incomplete; missing modules:\n{}", missing .iter() .map(|m| format!("- mod {m};")) @@ -443,3 +440,12 @@ fn check_smoketests_mod_rs_complete() -> Result<()> { Ok(()) } + +fn is_rust_identifier(identifier: &str) -> bool { + let identifier = identifier.strip_prefix("r#").unwrap_or(identifier); + let mut chars = identifier.chars(); + chars + .next() + .is_some_and(|first| first == '_' || first.is_ascii_alphabetic()) + && chars.all(|character| character == '_' || character.is_ascii_alphanumeric()) +} From d2346d8a7e7a1602189ac14457110e6192f48f84 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 29 Jul 2026 22:38:24 -0700 Subject: [PATCH 17/31] split out test_all_templates --- .../smoketests/tests/smoketests/templates.rs | 142 +++++++++--------- 1 file changed, 70 insertions(+), 72 deletions(-) diff --git a/crates/smoketests/tests/smoketests/templates.rs b/crates/smoketests/tests/smoketests/templates.rs index 793fd5659af..82fc679cf94 100644 --- a/crates/smoketests/tests/smoketests/templates.rs +++ b/crates/smoketests/tests/smoketests/templates.rs @@ -1167,86 +1167,84 @@ fn test_basic_cs_init_default_dotnet_selection() -> Result<()> { Ok(()) } -/// Tests all templates discovered in the `templates/` directory. +/// Runs the init + publish + client-test cycle for one registered template. /// -/// For each template the test: -/// 1. Runs `spacetime init --template ` into a temp directory -/// 2. Wires local SDK dependencies so the template builds against the current source -/// 3. Publishes the server module and verifies it succeeds -/// 4. Type-checks / builds the client code -/// -/// All templates are exercised even if some fail; a summary is printed at the -/// end and the test fails if any template did. -#[test] -fn test_all_templates() { - let templates = get_templates(); - assert!(!templates.is_empty(), "No templates found in templates/"); +/// Each template is a separate Rust test so nextest can hash it independently +/// into a partition and report or rerun failures independently. +fn run_registered_template(template_id: &str) { + let template = get_templates() + .into_iter() + .find(|template| template.id == template_id) + .unwrap_or_else(|| panic!("Registered template not found: {template_id}")); - let has_typescript = templates.iter().any(|t| t.server_lang.as_deref() == Some("typescript")); - let has_csharp = templates.iter().any(|t| t.server_lang.as_deref() == Some("csharp")); - - // Guard checks - verify required tools are available before starting. - if has_typescript { - spacetimedb_smoketests::require_pnpm!(); - } - if has_csharp { - spacetimedb_smoketests::require_dotnet!(); - } - - // Build the TypeScript SDK once up-front if any TypeScript templates exist. - if has_typescript { - build_typescript_sdk().expect("Failed to build TypeScript SDK"); + match template.server_lang.as_deref() { + Some("typescript") => { + spacetimedb_smoketests::require_pnpm!(); + build_typescript_sdk().expect("Failed to build TypeScript SDK"); + } + Some("csharp") => { + spacetimedb_smoketests::require_dotnet!(); + } + _ => {} } - // One shared server for all templates. let test = Smoketest::builder().autopublish(false).build(); + test_template(&test, &template).unwrap_or_else(|error| panic!("Template {template_id} failed: {error:#}")); +} - let mut results: Vec<(String, Result<()>)> = Vec::new(); - - for template in &templates { - let result = test_template(&test, template); - let passed = result.is_ok(); - eprintln!( - "[TEMPLATES] {} {}", - if passed { "[PASS]" } else { "[FAIL]" }, - template.id - ); - results.push((template.id.clone(), result)); - } +macro_rules! template_tests { + ($($test_name:ident => $template_id:literal),+ $(,)?) => { + const REGISTERED_TEMPLATE_IDS: &[&str] = &[$($template_id),+]; - // Print summary. - eprintln!("\n{}", "=".repeat(60)); - eprintln!("TEMPLATE TEST SUMMARY"); - eprintln!("{}", "=".repeat(60)); - for (id, result) in &results { - eprintln!( - "{:40} {}", - id, - if result.is_ok() { - "[PASS]".to_string() - } else { - format!("[FAIL]: {:#}", result.as_ref().unwrap_err()) + $( + #[test] + fn $test_name() { + run_registered_template($template_id); } - ); - } - let passed = results.iter().filter(|(_, r)| r.is_ok()).count(); - let total = results.len(); - eprintln!("{}", "=".repeat(60)); - eprintln!("TOTAL: {}/{} passed", passed, total); + )+ + }; +} - // Fail if any template failed. - let failures: Vec<_> = results +template_tests! { + test_template_angular_ts => "angular-ts", + test_template_astro_ts => "astro-ts", + test_template_basic_cpp => "basic-cpp", + test_template_basic_cs => "basic-cs", + test_template_basic_rs => "basic-rs", + test_template_basic_ts => "basic-ts", + test_template_browser_ts => "browser-ts", + test_template_bun_ts => "bun-ts", + test_template_chat_console_cs => "chat-console-cs", + test_template_chat_console_rs => "chat-console-rs", + test_template_chat_react_ts => "chat-react-ts", + test_template_deno_ts => "deno-ts", + test_template_hangman_react_ts => "hangman-react-ts", + test_template_llm_chat_ts => "llm-chat-ts", + test_template_money_exchange_react_ts => "money-exchange-react-ts", + test_template_nextjs_ts => "nextjs-ts", + test_template_nodejs_ts => "nodejs-ts", + test_template_nuxt_ts => "nuxt-ts", + test_template_react_ts => "react-ts", + test_template_remix_ts => "remix-ts", + test_template_solid_ts => "solid-ts", + test_template_svelte_ts => "svelte-ts", + test_template_tanstack_ts => "tanstack-ts", + test_template_vue_ts => "vue-ts", +} + +#[test] +fn test_template_registry_matches_discovered_templates() { + let discovered = get_templates() + .into_iter() + .map(|template| template.id) + .collect::>(); + let registered = REGISTERED_TEMPLATE_IDS .iter() - .filter(|(_, r)| r.is_err()) - .map(|(id, r)| format!(" {}: {:#}", id, r.as_ref().unwrap_err())) - .collect(); - - if !failures.is_empty() { - panic!( - "{}/{} template(s) failed:\n{}", - failures.len(), - total, - failures.join("\n") - ); - } + .map(|template_id| (*template_id).to_owned()) + .collect::>(); + + assert_eq!( + discovered, registered, + "Every discovered template must have its own nextest-visible test" + ); } From c03d7f753131a7d4fc9b96ffe8be4073b04f4285 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 11:17:54 -0700 Subject: [PATCH 18/31] remove global pnpm installs --- .github/workflows/ci.yml | 21 ++++++++----------- .github/workflows/docs-publish.yaml | 5 +---- .github/workflows/docs-update-llms.yaml | 5 +---- .github/workflows/llm-benchmark-periodic.yml | 6 ++++-- .../llm-benchmark-validate-goldens.yml | 6 ++++-- tools/ci/src/main.rs | 4 ++-- 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a3c79b77df..0dcf95e4479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -439,8 +439,9 @@ jobs: node-version: 22 - uses: ./.github/actions/setup-pnpm - with: - run_install: true + + - name: Install TypeScript SDK dependencies + run: pnpm --filter spacetimedb... install --frozen-lockfile # Install cmake and emscripten for C++ module compilation tests. - name: Install cmake and emscripten @@ -507,10 +508,9 @@ jobs: node-version: 24 - uses: ./.github/actions/setup-pnpm - with: - package_json_file: package.json - working_directory: . - run_install: "true" + + - name: Install Keynote dependencies + run: pnpm --filter spacetime-simulations... install --frozen-lockfile - uses: ./.github/actions/keynote-bench-setup @@ -844,8 +844,6 @@ jobs: node-version: 22 - uses: ./.github/actions/setup-pnpm - with: - run_install: true - name: Get pnpm store directory shell: bash @@ -1253,8 +1251,6 @@ jobs: node-version: '22' - uses: ./.github/actions/setup-pnpm - with: - run_install: true - name: Get pnpm store directory working-directory: sdks/typescript @@ -1294,8 +1290,6 @@ jobs: node-version: 22 - uses: ./.github/actions/setup-pnpm - with: - run_install: true - name: Get pnpm store directory shell: bash @@ -1311,6 +1305,9 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Install TypeScript test dependencies + run: pnpm install --frozen-lockfile + # - name: Extract SpacetimeDB branch name from file # id: extract-branch # run: | diff --git a/.github/workflows/docs-publish.yaml b/.github/workflows/docs-publish.yaml index 4cbf3b76a25..a5aac56113d 100644 --- a/.github/workflows/docs-publish.yaml +++ b/.github/workflows/docs-publish.yaml @@ -21,8 +21,6 @@ jobs: node-version: '22' - uses: ./.github/actions/setup-pnpm - with: - run_install: true - name: Get pnpm store directory working-directory: sdks/typescript @@ -39,8 +37,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install dependencies - working-directory: docs - run: pnpm install + run: pnpm --filter docs... install --frozen-lockfile - name: Docusaurus build working-directory: docs diff --git a/.github/workflows/docs-update-llms.yaml b/.github/workflows/docs-update-llms.yaml index a4928d55e49..341e1772c24 100644 --- a/.github/workflows/docs-update-llms.yaml +++ b/.github/workflows/docs-update-llms.yaml @@ -27,8 +27,6 @@ jobs: node-version: '22' - uses: ./.github/actions/setup-pnpm - with: - run_install: true - name: Get pnpm store directory working-directory: sdks/typescript @@ -45,8 +43,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install dependencies - working-directory: docs - run: pnpm install + run: pnpm --filter docs... install --frozen-lockfile - name: Docusaurus build working-directory: docs diff --git a/.github/workflows/llm-benchmark-periodic.yml b/.github/workflows/llm-benchmark-periodic.yml index 38235bb13ae..91470592fa1 100644 --- a/.github/workflows/llm-benchmark-periodic.yml +++ b/.github/workflows/llm-benchmark-periodic.yml @@ -87,8 +87,10 @@ jobs: - name: Install pnpm if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'typescript') }} uses: ./.github/actions/setup-pnpm - with: - run_install: true + + - name: Install TypeScript SDK dependencies + if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'typescript') }} + run: pnpm --filter spacetimedb... install --frozen-lockfile - name: Build TypeScript SDK if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'typescript') }} diff --git a/.github/workflows/llm-benchmark-validate-goldens.yml b/.github/workflows/llm-benchmark-validate-goldens.yml index 086ac20a0cd..f89174ceb71 100644 --- a/.github/workflows/llm-benchmark-validate-goldens.yml +++ b/.github/workflows/llm-benchmark-validate-goldens.yml @@ -72,8 +72,10 @@ jobs: - name: Install pnpm if: matrix.lang == 'typescript' uses: ./.github/actions/setup-pnpm - with: - run_install: true + + - name: Install TypeScript SDK dependencies + if: matrix.lang == 'typescript' + run: pnpm --filter spacetimedb... install --frozen-lockfile - name: Build TypeScript SDK if: matrix.lang == 'typescript' diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 262346233a7..76c950f4699 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -562,7 +562,7 @@ fn run_typescript_tests() -> Result<()> { } fn run_docs_build() -> Result<()> { - pnpm(["install"]).dir("docs").run()?; + pnpm(["--filter", "docs...", "install", "--frozen-lockfile"]).run()?; pnpm(["build"]).dir("docs").run()?; Ok(()) } @@ -852,7 +852,7 @@ fn main() -> Result<()> { ); } - pnpm(["install", "--recursive"]).run()?; + pnpm(["--filter", "docs...", "install", "--frozen-lockfile"]).run()?; pnpm(["generate-cli-docs"]).dir("docs").run()?; let out = cmd!("git", "status", "--porcelain", "--", "docs").read()?; if out.is_empty() { From c3279ac92036d6f76e801a6ee909f619b31b6a86 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 11:48:11 -0700 Subject: [PATCH 19/31] simplify --- .../restore-smoketest-build/action.yml | 6 +- .../smoketest-run-partition/action.yml | 16 ++- .github/actions/smoketest-setup/action.yml | 6 +- .github/workflows/ci.yml | 19 +-- crates/guard/src/lib.rs | 11 +- crates/smoketests/src/lib.rs | 11 +- tools/ci/src/main.rs | 117 +----------------- tools/ci/src/smoketest.rs | 13 +- 8 files changed, 31 insertions(+), 168 deletions(-) diff --git a/.github/actions/restore-smoketest-build/action.yml b/.github/actions/restore-smoketest-build/action.yml index d93ffed7b65..682383f64a5 100644 --- a/.github/actions/restore-smoketest-build/action.yml +++ b/.github/actions/restore-smoketest-build/action.yml @@ -5,10 +5,6 @@ inputs: artifact-name: description: Name of the smoketest build artifact to download. required: true - support-archive: - description: Path to the support archive within the downloaded artifact. - required: false - default: smoketest-support.tar.gz runs: using: composite @@ -21,7 +17,7 @@ runs: - name: Extract smoketest support files shell: bash - run: tar -xzf "${RUNNER_TEMP}/smoketest-build/${{ inputs.support-archive }}" + run: tar -xzf "${RUNNER_TEMP}/smoketest-build/smoketest-support.tar.gz" - name: Configure prebuilt SpacetimeDB binaries shell: bash diff --git a/.github/actions/smoketest-run-partition/action.yml b/.github/actions/smoketest-run-partition/action.yml index 33e7578c2db..8e61f9ace0e 100644 --- a/.github/actions/smoketest-run-partition/action.yml +++ b/.github/actions/smoketest-run-partition/action.yml @@ -19,7 +19,9 @@ runs: - name: Extract smoketest support files shell: bash - run: tar -xzf smoketest-support.tar.gz + run: | + tar -xzf smoketest-support.tar.gz + echo "SPACETIME_PREBUILT_TYPESCRIPT_SDK=1" >>"$GITHUB_ENV" # --test-threads=1 eliminates contention in the C# tests where they fight over # bindings build artifacts. It also improved unpartitioned job performance. @@ -27,11 +29,9 @@ runs: if: runner.os == 'Linux' shell: bash env: - SPACETIME_PREBUILT_TYPESCRIPT_SDK: "1" + SPACETIMEDB_WORKSPACE_ROOT: ${{ github.workspace }} run: | - if [ -f ~/emsdk/emsdk_env.sh ]; then - source ~/emsdk/emsdk_env.sh - fi + source ~/emsdk/emsdk_env.sh ./target/debug/ci smoketests run-archive \ --archive-file smoketest-nextest.tar.zst \ -- \ @@ -43,11 +43,9 @@ runs: if: runner.os == 'Windows' shell: pwsh env: - SPACETIME_PREBUILT_TYPESCRIPT_SDK: "1" + SPACETIMEDB_WORKSPACE_ROOT: ${{ github.workspace }} run: | - if (Test-Path "$env:USERPROFILE\emsdk\emsdk_env.ps1") { - & "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null - } + & "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null & .\target\debug\ci.exe smoketests run-archive ` --archive-file smoketest-nextest.tar.zst ` -- ` diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml index 7f56f38d53d..475867345ef 100644 --- a/.github/actions/smoketest-setup/action.yml +++ b/.github/actions/smoketest-setup/action.yml @@ -10,10 +10,6 @@ inputs: description: Restore and save the Rust cache. required: false default: "true" - rust-cache-targets: - description: Cache compiled dependency artifacts in Cargo target directories. - required: false - default: "true" rust-cache-shared-key: description: Shared key used for the Rust cache. required: false @@ -43,7 +39,7 @@ runs: with: workspaces: ${{ github.workspace }} shared-key: ${{ inputs.rust-cache-shared-key }} - cache-targets: ${{ inputs.rust-cache-targets }} + cache-targets: true cache-bin: false cache-on-failure: false cache-all-crates: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dcf95e4479..9f6b7e91b23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,9 +114,7 @@ jobs: - name: Set up pnpm for TypeScript SDK build if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' - uses: pnpm/action-setup@v4 - with: - package_json_file: package.json + uses: ./.github/actions/setup-pnpm - name: Set up Node.js for TypeScript SDK build if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' @@ -129,7 +127,6 @@ jobs: - name: Build TypeScript SDK if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' run: | - pnpm config set --global minimumReleaseAge 1440 pnpm --filter spacetimedb... install --frozen-lockfile pnpm --filter spacetimedb build @@ -287,9 +284,7 @@ jobs: # Node.js and pnpm are required for the TypeScript smoketests. # pnpm must be available before setup-node can locate and cache its store. - name: Set up pnpm - uses: pnpm/action-setup@v4 - with: - package_json_file: package.json + uses: ./.github/actions/setup-pnpm - name: Set up Node.js uses: actions/setup-node@v4 @@ -298,9 +293,6 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Configure pnpm package age policy - run: pnpm config set --global minimumReleaseAge 1440 - - name: Install TypeScript SDK runtime dependencies run: pnpm --filter spacetimedb install --prod --frozen-lockfile --prefer-offline @@ -336,9 +328,7 @@ jobs: # Node.js and pnpm are required for the TypeScript smoketests. # pnpm must be available before setup-node can locate and cache its store. - name: Set up pnpm - uses: pnpm/action-setup@v4 - with: - package_json_file: package.json + uses: ./.github/actions/setup-pnpm - name: Set up Node.js uses: actions/setup-node@v4 @@ -347,9 +337,6 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Configure pnpm package age policy - run: pnpm config set --global minimumReleaseAge 1440 - - name: Install TypeScript SDK runtime dependencies run: pnpm --filter spacetimedb install --prod --frozen-lockfile --prefer-offline diff --git a/crates/guard/src/lib.rs b/crates/guard/src/lib.rs index 24d14d956bf..4d434b3e491 100644 --- a/crates/guard/src/lib.rs +++ b/crates/guard/src/lib.rs @@ -24,15 +24,16 @@ fn next_spawn_id() -> u64 { /// Returns the workspace root directory. // TODO: Should this use something like `git rev-parse --show-toplevel` to avoid being directory-relative? Or perhaps `CARGO_WORKSPACE_DIR` is set? fn workspace_root() -> PathBuf { - // Private CI builds this crate into a nextest archive in one job and runs it in - // another, so prefer the public root supplied by the runner instead of embedding - // the build job's absolute path with `env!`. + // Archived binaries may execute in a different checkout from the build, so + // prefer the workspace root explicitly supplied by the runner. if let Ok(workspace_root) = env::var("SPACETIMEDB_WORKSPACE_ROOT") { return PathBuf::from(workspace_root); } - let manifest_dir = - PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")); + // Cargo and nextest set this at runtime, including nextest's remapped path. + let manifest_dir = PathBuf::from( + std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running smoketests"), + ); manifest_dir .parent() // crates/ .and_then(|p| p.parent()) // workspace root diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 33c60e2b935..500e8232339 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -180,15 +180,16 @@ macro_rules! timed { /// Returns the workspace root directory. pub fn workspace_root() -> PathBuf { - // Nextest archives are built and executed in different jobs, so prefer the - // current public checkout supplied by the runner instead of embedding the build - // job's absolute path with `env!`. + // Archived tests may execute in a different checkout from the build, so + // prefer the workspace root explicitly supplied by the runner. if let Ok(workspace_root) = std::env::var("SPACETIMEDB_WORKSPACE_ROOT") { return PathBuf::from(workspace_root); } - let manifest_dir = - PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running tests")); + // Cargo and nextest set this at runtime, including nextest's remapped path. + let manifest_dir = PathBuf::from( + std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set when running smoketests"), + ); manifest_dir .parent() .and_then(|p| p.parent()) diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 76c950f4699..cb9dab5e8a8 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -182,59 +182,6 @@ fn npmrc_minimum_release_age(path: &Path, expected_minimum_release_age: u64) -> }) } -fn workflow_job_bounds(lines: &[&str], line_index: usize) -> (usize, usize) { - let is_job_header = |line: &str| { - line.starts_with(" ") - && !line.starts_with(" ") - && !line.trim_start().starts_with('#') - && line.trim_end().ends_with(':') - }; - - let start = (0..line_index) - .rev() - .find(|&index| is_job_header(lines[index])) - .unwrap_or(0); - let end = (line_index + 1..lines.len()) - .find(|&index| is_job_header(lines[index])) - .unwrap_or(lines.len()); - (start, end) -} - -fn check_workflow_pnpm_release_age_policy( - workflow_path: &Path, - contents: &str, - expected_minimum_release_age: u64, -) -> Result<()> { - const DIRECT_PNPM_SETUP: &str = "uses: pnpm/action-setup@v4"; - let required_policy = format!("pnpm config set --global minimumReleaseAge {expected_minimum_release_age}"); - let lines: Vec<_> = contents.lines().collect(); - - for (setup_index, _) in lines - .iter() - .enumerate() - .filter(|(_, line)| line.trim().trim_start_matches("- ") == DIRECT_PNPM_SETUP) - { - let (_, job_end) = workflow_job_bounds(&lines, setup_index); - let following_steps = &lines[setup_index + 1..job_end]; - let policy_index = following_steps.iter().position(|line| line.contains(&required_policy)); - let install_index = following_steps.iter().position(|line| line.contains("pnpm install")); - let policy_is_missing_or_late = match policy_index { - Some(policy_index) => install_index.is_some_and(|install_index| policy_index > install_index), - None => true, - }; - - if policy_is_missing_or_late { - bail!( - "{} must run `{}` after pnpm/action-setup@v4 and before pnpm install", - workflow_path.display(), - required_policy - ); - } - } - - Ok(()) -} - fn check_pnpm_release_age_policy() -> Result<()> { ensure_repo_root()?; @@ -336,69 +283,17 @@ fn check_pnpm_release_age_policy() -> Result<()> { for workflow_path in git_tracked_files(".github/workflows/*")? { let contents = fs::read_to_string(&workflow_path)?; - check_workflow_pnpm_release_age_policy(&workflow_path, &contents, root_minimum_release_age)?; + if contents.contains("pnpm/action-setup@v4") { + bail!( + "{} must use ./.github/actions/setup-pnpm instead of pnpm/action-setup@v4", + workflow_path.display() + ); + } } Ok(()) } -#[cfg(test)] -mod tests { - use super::check_workflow_pnpm_release_age_policy; - use std::path::Path; - - const WORKFLOW_PATH: &str = ".github/workflows/ci.yml"; - - #[test] - fn ci_workflow_obeys_direct_pnpm_release_age_policy() { - let workflow = include_str!("../../../.github/workflows/ci.yml"); - check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap(); - } - - #[test] - fn direct_pnpm_setup_requires_release_age_before_install() { - let workflow = r#" -jobs: - smoketests: - steps: - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 - - run: | - pnpm config set --global minimumReleaseAge 1440 - pnpm install --frozen-lockfile -"#; - check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap(); - } - - #[test] - fn direct_pnpm_setup_rejects_missing_release_age() { - let workflow = r#" -jobs: - smoketests: - steps: - - uses: pnpm/action-setup@v4 - - run: pnpm install --frozen-lockfile -"#; - let error = check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap_err(); - assert!(error.to_string().contains("minimumReleaseAge 1440")); - } - - #[test] - fn direct_pnpm_setup_rejects_release_age_after_install() { - let workflow = r#" -jobs: - smoketests: - steps: - - uses: pnpm/action-setup@v4 - - run: | - pnpm install --frozen-lockfile - pnpm config set --global minimumReleaseAge 1440 -"#; - let error = check_workflow_pnpm_release_age_policy(Path::new(WORKFLOW_PATH), workflow, 1440).unwrap_err(); - assert!(error.to_string().contains("before pnpm install")); - } -} - #[derive(Subcommand)] enum CiCmd { /// Runs tests diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 319d9337ddf..44b33f3bad0 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -416,9 +416,7 @@ fn check_smoketests_mod_rs_complete() -> Result<()> { let declaration = line.strip_prefix("mod ").or_else(|| line.strip_prefix("pub mod ")); if let Some(module) = declaration.and_then(|declaration| declaration.strip_suffix(';')) { let module = module.trim(); - if is_rust_identifier(module) { - present.insert(module.strip_prefix("r#").unwrap_or(module).to_string()); - } + present.insert(module.strip_prefix("r#").unwrap_or(module).to_string()); } } @@ -440,12 +438,3 @@ fn check_smoketests_mod_rs_complete() -> Result<()> { Ok(()) } - -fn is_rust_identifier(identifier: &str) -> bool { - let identifier = identifier.strip_prefix("r#").unwrap_or(identifier); - let mut chars = identifier.chars(); - chars - .next() - .is_some_and(|first| first == '_' || first.is_ascii_alphabetic()) - && chars.all(|character| character == '_' || character.is_ascii_alphanumeric()) -} From e4a164599f58120759ae456093db6beca99a77b0 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 11:52:54 -0700 Subject: [PATCH 20/31] remove stale comments --- .github/actions/smoketest-run-partition/action.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/actions/smoketest-run-partition/action.yml b/.github/actions/smoketest-run-partition/action.yml index 8e61f9ace0e..ffc22c517da 100644 --- a/.github/actions/smoketest-run-partition/action.yml +++ b/.github/actions/smoketest-run-partition/action.yml @@ -23,8 +23,6 @@ runs: tar -xzf smoketest-support.tar.gz echo "SPACETIME_PREBUILT_TYPESCRIPT_SDK=1" >>"$GITHUB_ENV" - # --test-threads=1 eliminates contention in the C# tests where they fight over - # bindings build artifacts. It also improved unpartitioned job performance. - name: Run smoketest partition (Linux) if: runner.os == 'Linux' shell: bash @@ -38,7 +36,6 @@ runs: --test-threads=1 \ --partition hash:${{ inputs.partition }}/4 - # Due to Emscripten PATH issues this remains separate so OpenSSL builds correctly. - name: Run smoketest partition (Windows) if: runner.os == 'Windows' shell: pwsh From f8d681219dd4283b9ef8709bd618d9e21609aa96 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 12:23:38 -0700 Subject: [PATCH 21/31] fix caching for unity builds --- .github/workflows/ci.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f6b7e91b23..d5f1adab2f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -866,6 +866,7 @@ jobs: env: CARGO_TARGET_DIR: ${{ github.workspace }}/target RUST_BACKTRACE: full + UNITY_VERSION: 2022.3.32f1 steps: - name: Checkout repository id: checkout-stdb @@ -873,6 +874,17 @@ jobs: with: ref: ${{ needs.merge_queue_noop.outputs.source_sha }} + # Key the Library by the inputs Unity imports so unrelated PRs share the same + # cache. Keep this before generation/hydration so ignored build outputs do not + # affect hashFiles. + - name: Restore Unity Library + uses: actions/cache@v4 + with: + path: demo/Blackholio/client-unity/Library + key: Unity-v2-${{ runner.os }}-${{ env.UNITY_VERSION }}-${{ hashFiles('demo/Blackholio/client-unity/**', 'sdks/csharp/**', 'crates/bindings-csharp/BSATN.Runtime/**', 'crates/bindings-csharp/Runtime/**', 'crates/bindings-csharp/Directory.Build.props', 'tools/regen/src/csharp.rs', 'global.json') }} + restore-keys: | + Unity-v2-${{ runner.os }}-${{ env.UNITY_VERSION }}- + # Run cheap .NET tests first. If those fail, no need to run expensive Unity tests. - name: Setup dotnet @@ -953,16 +965,10 @@ jobs: yq e -i '.dependencies["com.clockworklabs.spacetimedbsdk"] = "file:../../../../sdks/csharp"' manifest.json cat manifest.json - - uses: actions/cache@v3 - with: - path: demo/Blackholio/client-unity/Library - key: Unity-${{ github.head_ref }} - restore-keys: Unity- - - name: Run Unity tests uses: game-ci/unity-test-runner@v4 with: - unityVersion: 2022.3.32f1 # Adjust Unity version to a valid tag + unityVersion: ${{ env.UNITY_VERSION }} projectPath: demo/Blackholio/client-unity # Path to the Unity project subdirectory githubToken: ${{ secrets.GITHUB_TOKEN }} testMode: playmode From 76d0c246c268643f8ba6134748f9fed62de6386a Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 13:01:50 -0700 Subject: [PATCH 22/31] update native rusty_v8 cache key --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5f1adab2f0..f47cf852db9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -229,7 +229,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ runner.temp }}/spacetimedb-native-cache/rusty-v8/rusty_v8.lib.gz - key: native-v1-rusty-v8-${{ steps.v8-version.outputs.v8 }}-x86_64-pc-windows-msvc-release-pc0-sb0 + key: native-v1-rusty-v8-${{ steps.v8-version.outputs.v8 }} - name: Prepare native V8 library shell: pwsh From 1ba2b3b894c3ac4a705c8688b86f0e78c33c4a8b Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 13:04:46 -0700 Subject: [PATCH 23/31] fix setup-pnpm --- .github/actions/setup-pnpm/action.yml | 13 +++++++++++++ .github/workflows/ci.yml | 26 +++----------------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 2bf9ecdbdec..618f393fa14 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -5,6 +5,10 @@ inputs: description: package.json file used to determine the pnpm version required: false default: package.json + node_version: + description: Node.js version to install with pnpm dependency caching; omitted when Node.js is already configured + required: false + default: "" run_install: description: Run pnpm install after configuring pnpm required: false @@ -20,6 +24,15 @@ runs: with: package_json_file: ${{ inputs.package_json_file }} + # setup-node's pnpm cache requires the pnpm executable to exist first. + - name: Set up Node.js + if: inputs.node_version != '' + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - name: Configure pnpm package age policy shell: bash run: pnpm config set --global minimumReleaseAge 1440 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f47cf852db9..22489798b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,14 +115,8 @@ jobs: - name: Set up pnpm for TypeScript SDK build if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' uses: ./.github/actions/setup-pnpm - - - name: Set up Node.js for TypeScript SDK build - if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' - uses: actions/setup-node@v4 with: - node-version: 22 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + node_version: 22 - name: Build TypeScript SDK if: steps.typescript-sdk-cache.outputs.cache-hit != 'true' @@ -281,17 +275,10 @@ jobs: with: rust-cache-enabled: "false" - # Node.js and pnpm are required for the TypeScript smoketests. - # pnpm must be available before setup-node can locate and cache its store. - name: Set up pnpm uses: ./.github/actions/setup-pnpm - - - name: Set up Node.js - uses: actions/setup-node@v4 with: - node-version: 18 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + node_version: 18 - name: Install TypeScript SDK runtime dependencies run: pnpm --filter spacetimedb install --prod --frozen-lockfile --prefer-offline @@ -325,17 +312,10 @@ jobs: with: rust-cache-enabled: "false" - # Node.js and pnpm are required for the TypeScript smoketests. - # pnpm must be available before setup-node can locate and cache its store. - name: Set up pnpm uses: ./.github/actions/setup-pnpm - - - name: Set up Node.js - uses: actions/setup-node@v4 with: - node-version: 18 - cache: pnpm - cache-dependency-path: pnpm-lock.yaml + node_version: 18 - name: Install TypeScript SDK runtime dependencies run: pnpm --filter spacetimedb install --prod --frozen-lockfile --prefer-offline From 23f0183cc867a5f2d1b487474ef84873d1317542 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 13:11:29 -0700 Subject: [PATCH 24/31] remove sccache --- .github/actions/keynote-bench-setup/action.yml | 9 --------- .github/actions/smoketest-setup/action.yml | 9 --------- .github/workflows/ci.yml | 8 -------- 3 files changed, 26 deletions(-) diff --git a/.github/actions/keynote-bench-setup/action.yml b/.github/actions/keynote-bench-setup/action.yml index 59d900fd7b1..424c1a55a7a 100644 --- a/.github/actions/keynote-bench-setup/action.yml +++ b/.github/actions/keynote-bench-setup/action.yml @@ -9,12 +9,3 @@ runs: - name: Set default rust toolchain shell: bash run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - - name: Set up sccache - uses: mozilla-actions/sccache-action@v0.0.10 - - - name: Enable sccache - shell: bash - run: | - echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" - echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" diff --git a/.github/actions/smoketest-setup/action.yml b/.github/actions/smoketest-setup/action.yml index 475867345ef..02cebf29ede 100644 --- a/.github/actions/smoketest-setup/action.yml +++ b/.github/actions/smoketest-setup/action.yml @@ -24,15 +24,6 @@ runs: shell: bash run: rustup default "$(rustup show active-toolchain | cut -d' ' -f1)" - - name: Set up sccache - uses: mozilla-actions/sccache-action@v0.0.10 - - - name: Enable sccache - shell: bash - run: | - echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" - echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" - - name: Cache Rust dependencies if: inputs.rust-cache-enabled == 'true' uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22489798b21..63c8ffa5344 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -522,14 +522,6 @@ jobs: - name: Set default rust toolchain run: rustup default $(rustup show active-toolchain | cut -d' ' -f1) - - name: Set up sccache - uses: mozilla-actions/sccache-action@v0.0.10 - - - name: Enable sccache - run: | - echo "SCCACHE_GHA_ENABLED=true" >>"$GITHUB_ENV" - echo "RUSTC_WRAPPER=sccache" >>"$GITHUB_ENV" - - name: Run index scan benchmark regression check run: cargo run --release -p spacetimedb-index-scan-gate From dfceff855d7c3229cfd4c4fd2aaa329206a44b2a Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 13:24:09 -0700 Subject: [PATCH 25/31] only 2 partitions for linux --- .../actions/smoketest-run-partition/action.yml | 7 +++++-- .github/workflows/ci.yml | 18 +++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/actions/smoketest-run-partition/action.yml b/.github/actions/smoketest-run-partition/action.yml index ffc22c517da..47600945689 100644 --- a/.github/actions/smoketest-run-partition/action.yml +++ b/.github/actions/smoketest-run-partition/action.yml @@ -8,6 +8,9 @@ inputs: partition: description: One-based hash partition number. required: true + partition-total: + description: Total number of hash partitions. + required: true runs: using: composite @@ -34,7 +37,7 @@ runs: --archive-file smoketest-nextest.tar.zst \ -- \ --test-threads=1 \ - --partition hash:${{ inputs.partition }}/4 + --partition hash:${{ inputs.partition }}/${{ inputs.partition-total }} - name: Run smoketest partition (Windows) if: runner.os == 'Windows' @@ -47,4 +50,4 @@ runs: --archive-file smoketest-nextest.tar.zst ` -- ` --test-threads=1 ` - --partition "hash:${{ inputs.partition }}/4" + --partition "hash:${{ inputs.partition }}/${{ inputs.partition-total }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63c8ffa5344..51bf0e5205a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -254,11 +254,11 @@ jobs: smoketests_linux: needs: [merge_queue_noop, smoketest_build_linux] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} - name: Smoketests (Linux, ${{ matrix.partition }}/4) + name: Smoketests (Linux, ${{ matrix.partition }}/2) strategy: fail-fast: false matrix: - partition: [1, 2, 3, 4] + partition: [1, 2] runs-on: spacetimedb-new-runner-2 timeout-minutes: 120 env: @@ -287,6 +287,7 @@ jobs: with: artifact-name: smoketest-build-Linux partition: ${{ matrix.partition }} + partition-total: 2 smoketests_windows: needs: [merge_queue_noop, smoketest_build_windows] @@ -324,6 +325,7 @@ jobs: with: artifact-name: smoketest-build-Windows partition: ${{ matrix.partition }} + partition-total: 4 # this is a no-op version of the above check with a trivially-passing body. # we can't just let the check be entirely skipped because each matrix target is a required check, @@ -331,26 +333,28 @@ jobs: smoketests_noop: needs: [merge_queue_noop] if: ${{ needs.merge_queue_noop.outputs.skip == 'true' }} - name: Smoketests (${{ matrix.name }}, ${{ matrix.partition }}/4) + name: Smoketests (${{ matrix.name }}, ${{ matrix.partition }}/${{ matrix.partition-total }}) strategy: matrix: include: - name: Linux partition: 1 + partition-total: 2 - name: Linux partition: 2 - - name: Linux - partition: 3 - - name: Linux - partition: 4 + partition-total: 2 - name: Windows partition: 1 + partition-total: 4 - name: Windows partition: 2 + partition-total: 4 - name: Windows partition: 3 + partition-total: 4 - name: Windows partition: 4 + partition-total: 4 runs-on: ubuntu-latest steps: - name: Skip duplicate merge queue smoketest From e389edbf98ccad18aa43ed92c179d359af7dab46 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 13:41:41 -0700 Subject: [PATCH 26/31] remove rust-cache-targets --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51bf0e5205a..6ad0ca43a1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -208,7 +208,6 @@ jobs: with: runtime-dependencies: "false" rust-cache-enabled: "true" - rust-cache-targets: "true" rust-cache-shared-key: smoketest-build-dependencies - name: Resolve V8 version From 51ef1d34596f8894ae59c745befd7606b5001702 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 15:15:47 -0700 Subject: [PATCH 27/31] private compatibility --- .../restore-smoketest-build/action.yml | 21 ++++++++++++++++-- .github/actions/setup-pnpm/action.yml | 22 +++++++++++++++++-- crates/cli/src/subcommands/generate.rs | 9 +++++++- .../smoketests/tests/smoketests/cli/server.rs | 15 +++++++++---- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/.github/actions/restore-smoketest-build/action.yml b/.github/actions/restore-smoketest-build/action.yml index 682383f64a5..f6fe717032c 100644 --- a/.github/actions/restore-smoketest-build/action.yml +++ b/.github/actions/restore-smoketest-build/action.yml @@ -1,10 +1,18 @@ name: Restore smoketest build -description: Restore prebuilt Linux CLI and standalone binaries from a smoketest build artifact. +description: Restore prebuilt Linux smoketest support binaries from a build artifact. inputs: artifact-name: description: Name of the smoketest build artifact to download. required: true + require-standalone: + description: Require the artifact to contain the standalone server. + required: false + default: "true" + schema-extractor-path: + description: Optional schema extractor executable used instead of standalone. + required: false + default: "" runs: using: composite @@ -21,9 +29,18 @@ runs: - name: Configure prebuilt SpacetimeDB binaries shell: bash + env: + REQUIRE_STANDALONE: ${{ inputs.require-standalone }} + SCHEMA_EXTRACTOR_PATH: ${{ inputs.schema-extractor-path }} run: | test -x "${CARGO_TARGET_DIR}/release/spacetimedb-cli" - test -x "${CARGO_TARGET_DIR}/release/spacetimedb-standalone" + if [[ "${REQUIRE_STANDALONE}" == "true" ]]; then + test -x "${CARGO_TARGET_DIR}/release/spacetimedb-standalone" + fi + if [[ -n "${SCHEMA_EXTRACTOR_PATH}" ]]; then + test -x "${SCHEMA_EXTRACTOR_PATH}" + echo "SPACETIMEDB_SCHEMA_EXTRACTOR=${SCHEMA_EXTRACTOR_PATH}" >>"$GITHUB_ENV" + fi ln -sf spacetimedb-cli "${CARGO_TARGET_DIR}/release/spacetime" echo "${CARGO_TARGET_DIR}/release" >>"$GITHUB_PATH" echo "SPACETIME_PREBUILT_BINARIES=1" >>"$GITHUB_ENV" diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 618f393fa14..f7155ca29a3 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -1,6 +1,10 @@ name: Set up pnpm description: Install pnpm and configure repository package-age policy before any install inputs: + version: + description: pnpm version to install. If omitted, package_json_file determines the version. + required: false + default: "" package_json_file: description: package.json file used to determine the pnpm version required: false @@ -9,10 +13,18 @@ inputs: description: Node.js version to install with pnpm dependency caching; omitted when Node.js is already configured required: false default: "" + cache_dependency_path: + description: Lockfile used by setup-node's pnpm store cache + required: false + default: pnpm-lock.yaml run_install: description: Run pnpm install after configuring pnpm required: false default: "false" + install_args: + description: Additional arguments passed to pnpm install + required: false + default: "" working_directory: description: Directory to run pnpm install in required: false @@ -21,6 +33,12 @@ runs: using: composite steps: - uses: pnpm/action-setup@v4 + if: inputs.version != '' + with: + version: ${{ inputs.version }} + + - uses: pnpm/action-setup@v4 + if: inputs.version == '' with: package_json_file: ${{ inputs.package_json_file }} @@ -31,7 +49,7 @@ runs: with: node-version: ${{ inputs.node_version }} cache: pnpm - cache-dependency-path: pnpm-lock.yaml + cache-dependency-path: ${{ inputs.cache_dependency_path }} - name: Configure pnpm package age policy shell: bash @@ -41,4 +59,4 @@ runs: if: inputs.run_install == 'true' shell: bash working-directory: ${{ inputs.working_directory }} - run: pnpm install + run: pnpm install ${{ inputs.install_args }} diff --git a/crates/cli/src/subcommands/generate.rs b/crates/cli/src/subcommands/generate.rs index 69bab330cd2..a909a7f9fb6 100644 --- a/crates/cli/src/subcommands/generate.rs +++ b/crates/cli/src/subcommands/generate.rs @@ -750,7 +750,14 @@ impl Language { pub type ExtractDescriptions = fn(&Path) -> anyhow::Result; pub fn extract_descriptions(wasm_file: &Path) -> anyhow::Result { - let bin_path = resolve_sibling_binary("spacetimedb-standalone")?; + // Internal Tests execute public smoketests against private cloud binaries + // and intentionally do not package standalone. The private CI driver + // exposes the same `extract-schema` interface and provides this override. + // Normal CLI installations continue to use the sibling standalone binary. + let bin_path = std::env::var_os("SPACETIMEDB_SCHEMA_EXTRACTOR") + .map(PathBuf::from) + .map(Ok) + .unwrap_or_else(|| resolve_sibling_binary("spacetimedb-standalone"))?; let child = Command::new(&bin_path) .arg("extract-schema") .arg(wasm_file) diff --git a/crates/smoketests/tests/smoketests/cli/server.rs b/crates/smoketests/tests/smoketests/cli/server.rs index 9aae2e41f83..1dfbdcf2405 100644 --- a/crates/smoketests/tests/smoketests/cli/server.rs +++ b/crates/smoketests/tests/smoketests/cli/server.rs @@ -1,6 +1,7 @@ //! CLI server command tests -use spacetimedb_guard::{ensure_binaries_built, SpacetimeDbGuard}; +use spacetimedb_guard::ensure_binaries_built; +use spacetimedb_smoketests::{require_local_server, Smoketest}; use std::fs; use std::io::Read; use std::net::TcpListener; @@ -98,10 +99,12 @@ fn stop_child(mut child: Child) { } #[test] -fn cli_can_ping_spacetimedb_on_disk() { - let spacetime = SpacetimeDbGuard::spawn_in_temp_data_dir(); +fn cli_can_ping_spacetimedb_server() { + // Internal Tests supply a private cloud URL through + // SPACETIME_REMOTE_SERVER. Normal public smoketests still start standalone. + let spacetime = Smoketest::builder().autopublish(false).build(); let output = cli_cmd() - .args(["server", "ping", &spacetime.host_url.to_string()]) + .args(["server", "ping", &spacetime.server_url]) .output() .expect("failed to execute"); assert!( @@ -113,6 +116,8 @@ fn cli_can_ping_spacetimedb_on_disk() { #[test] fn cli_start_uses_listen_addr_from_cli_toml() { + require_local_server!(); + let root = tempfile::tempdir().expect("failed to create tempdir"); let port = free_local_port(); let listen_addr = format!("127.0.0.1:{port}"); @@ -129,6 +134,8 @@ fn cli_start_uses_listen_addr_from_cli_toml() { #[test] fn cli_start_explicit_listen_addr_overrides_cli_toml() { + require_local_server!(); + let root = tempfile::tempdir().expect("failed to create tempdir"); let config_port = free_local_port(); let mut explicit_port = free_local_port(); From 471db452388d19691f309beeb53bbd6db55114f2 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 16:34:18 -0700 Subject: [PATCH 28/31] use one runner for release dry-run --- .github/workflows/release.yml | 203 +++++++++++++++++++++++++++++----- 1 file changed, 173 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 133c5e0d92f..3b32dfef988 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,9 +58,174 @@ concurrency: cancel-in-progress: true jobs: - # This job runs before all of our release jobs. If there is a release problem we should + release-dry-run: + name: release-dry-run + if: >- + ${{ + inputs.dry_run + && (inputs.release_crates + || inputs.release_csharp + || inputs.release_cpp + || inputs.release_npm + || inputs.release_docker) + }} + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + packages: write + env: + CARGO_TERM_COLOR: always + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + # Build the release tool from the dispatched ref, matching the live release path. + - name: Checkout release tooling + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Rust + uses: dsherret/rust-toolchain-file@v1 + + - name: Set default Rust toolchain + run: rustup default "$(rustup show active-toolchain | cut -d' ' -f1)" + + - name: Install cargo-release + run: cargo install --path tools/release + + # Keep the release sources separate from the tooling checkout so each target + # can be reset without removing the cargo-release sources or executable. + - name: Checkout release sources + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_tag }} + path: release-source + submodules: recursive + + - name: Set up Node.js + if: inputs.release_npm + uses: actions/setup-node@v6 + with: + node-version: '22.x' + + - name: Install NPM release tools + if: inputs.release_npm + run: | + # This is the minimum npm version required to use trusted publishing. + npm install --global npm@11.5.1 + npm install --global pnpm + npm --version + pnpm --version + + - name: Set up .NET + if: inputs.release_csharp + uses: actions/setup-dotnet@v4 + with: + global-json-file: release-source/global.json + + - name: Install NuGet + if: inputs.release_csharp + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y mono-complete + + nuget_bin="$RUNNER_TEMP/release-tools" + mkdir -p "$nuget_bin" + wget -q https://dist.nuget.org/win-x86-commandline/latest/nuget.exe \ + -O "$nuget_bin/nuget-mono.exe" + cat >"$nuget_bin/nuget" <<'EOF' + #!/bin/bash + exec mono "$(dirname "$(readlink -f "$0")")/nuget-mono.exe" "$@" + EOF + chmod +x "$nuget_bin/nuget" + echo "$nuget_bin" >>"$GITHUB_PATH" + + - name: Set up SDK repository SSH keys + if: inputs.release_csharp || inputs.release_cpp + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: | + ${{ inputs.release_csharp && secrets.UNITY_SDK_DEPLOY_KEY || '' }} + ${{ inputs.release_cpp && secrets.CPP_SDK_DEPLOY_KEY || '' }} + + - name: Configure git + if: inputs.release_csharp || inputs.release_cpp + run: | + git config --global user.name "Release Bot" + git config --global user.email "no-reply@clockworklabs.io" + git config --global --unset-all url.https://github.com/.insteadOf || true + git config --global url."git@github.com:".insteadOf "https://github.com/" + + - name: Set up Docker Buildx + if: inputs.release_docker + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Run release dry-runs + working-directory: release-source + env: + RELEASE_TAG: ${{ github.event.inputs.release_tag }} + RELEASE_CRATES: ${{ github.event.inputs.release_crates }} + RELEASE_CSHARP: ${{ github.event.inputs.release_csharp }} + RELEASE_CPP: ${{ github.event.inputs.release_cpp }} + RELEASE_NPM: ${{ github.event.inputs.release_npm }} + RELEASE_DOCKER: ${{ github.event.inputs.release_docker }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + set -uo pipefail + + failures=() + + reset_release_sources() { + git reset --hard "$RELEASE_TAG" + git clean -ffdx + git submodule sync --recursive + git submodule update --init --recursive --force + git submodule foreach --recursive 'git reset --hard HEAD && git clean -ffdx' + } + + run_dry_run() { + local enabled="$1" + local target="$2" + + if [[ "$enabled" != "true" ]]; then + return + fi + + echo "::group::$target dry-run" + if cargo-release release "$target" "$RELEASE_TAG" --dry-run; then + echo "$target: success" >>"$GITHUB_STEP_SUMMARY" + else + failures+=("$target") + echo "$target: failure" >>"$GITHUB_STEP_SUMMARY" + echo "::error title=$target release dry-run failed::See the $target log group for details." + fi + echo "::endgroup::" + + if ! reset_release_sources; then + failures+=("$target workspace reset") + echo "::error title=Release source reset failed::Could not clean the checkout after $target." + fi + } + + run_dry_run "$RELEASE_CRATES" crates + run_dry_run "$RELEASE_CSHARP" csharp + run_dry_run "$RELEASE_CPP" cpp + run_dry_run "$RELEASE_NPM" npm + run_dry_run "$RELEASE_DOCKER" docker + + if (( ${#failures[@]} > 0 )); then + printf 'Release dry-run failures: %s\n' "${failures[*]}" >&2 + exit 1 + fi + + # This job runs before all of our live release jobs. If there is a release problem we should # try to fail during this step to prevent partial releases. build-cargo-release: + if: ${{ !inputs.dry_run }} runs-on: spacetimedb-new-runner-2 steps: - name: Checkout @@ -96,7 +261,7 @@ jobs: release-crates: needs: build-cargo-release runs-on: spacetimedb-new-runner-2 - if: ${{ inputs.release_crates }} + if: ${{ !inputs.dry_run && inputs.release_crates }} env: CARGO_TERM_COLOR: always CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -125,20 +290,13 @@ jobs: chmod +x ./shared-bin/cargo-release echo "$PWD/shared-bin" >> "$GITHUB_PATH" - # TODO: dry-run via publishing to a local registry: https://doc.rust-lang.org/cargo/reference/registries.html - - name: Run release (dry-run) - if: ${{ inputs.dry_run }} - # NOTE: This will print a warning that `cargo-release release crates` dry runs are not supported - run: cargo-release release crates ${{ github.event.inputs.release_tag }} --dry-run - - name: Run release - if: ${{ !inputs.dry_run }} run: cargo-release release crates ${{ github.event.inputs.release_tag }} release-csharp: needs: build-cargo-release runs-on: spacetimedb-new-runner-2 - if: ${{ inputs.release_csharp }} + if: ${{ !inputs.dry_run && inputs.release_csharp }} env: CARGO_TERM_COLOR: always GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -170,7 +328,7 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@v4 with: - global-json-path: global.json + global-json-file: global.json - name: Install NuGet shell: bash @@ -206,18 +364,13 @@ jobs: echo "Git URL rewrite config:" git config --global --get-regexp url - - name: Run C# SDK release (dry-run) - if: ${{ inputs.dry_run }} - run: cargo-release release csharp ${{ github.event.inputs.release_tag }} --dry-run - - name: Run C# SDK release - if: ${{ !inputs.dry_run }} run: cargo-release release csharp ${{ github.event.inputs.release_tag }} release-cpp: needs: build-cargo-release runs-on: spacetimedb-new-runner-2 - if: ${{ inputs.release_cpp }} + if: ${{ !inputs.dry_run && inputs.release_cpp }} env: CARGO_TERM_COLOR: always GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -262,18 +415,13 @@ jobs: echo "Git URL rewrite config:" git config --global --get-regexp url - - name: Run C++ SDK release (dry-run) - if: ${{ inputs.dry_run }} - run: cargo-release release cpp ${{ github.event.inputs.release_tag }} --dry-run - - name: Run C++ SDK release - if: ${{ !inputs.dry_run }} run: cargo-release release cpp ${{ github.event.inputs.release_tag }} release-npm: needs: build-cargo-release runs-on: ubuntu-latest - if: ${{ inputs.release_npm }} + if: ${{ !inputs.dry_run && inputs.release_npm }} env: CARGO_TERM_COLOR: always GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -315,12 +463,7 @@ jobs: - name: Install pnpm run: npm install -g pnpm - - name: Run NPM release (dry-run) - if: ${{ inputs.dry_run }} - run: cargo-release release npm ${{ github.event.inputs.release_tag }} --dry-run - - name: Run NPM release - if: ${{ !inputs.dry_run }} run: cargo-release release npm ${{ github.event.inputs.release_tag }} release-docker: @@ -329,7 +472,7 @@ jobs: env: CARGO_TERM_COLOR: always GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: ${{ inputs.release_docker }} + if: ${{ !inputs.dry_run && inputs.release_docker }} steps: - name: Checkout specific tag @@ -362,7 +505,7 @@ jobs: # Docker Hub access tokens are passed to docker/login-action via the password input. password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Run Docker release - run: cargo-release release docker ${{ github.event.inputs.release_tag }} ${{ inputs.dry_run && '--dry-run' || '' }} + run: cargo-release release docker ${{ github.event.inputs.release_tag }} update-mirror-latest-version: runs-on: ubuntu-latest From a8a952b866235ed956a996c6ac3763d7d08ad012 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 16:41:38 -0700 Subject: [PATCH 29/31] full pnpm install for test suite --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ad0ca43a1e..b6dfbc0415a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -410,8 +410,8 @@ jobs: - uses: ./.github/actions/setup-pnpm - - name: Install TypeScript SDK dependencies - run: pnpm --filter spacetimedb... install --frozen-lockfile + - name: Install pnpm workspace dependencies + run: pnpm install --frozen-lockfile # Install cmake and emscripten for C++ module compilation tests. - name: Install cmake and emscripten From 7607b1581bdf886d5b21d41491b4847173dd28f4 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 17:23:57 -0700 Subject: [PATCH 30/31] fix internal tests --- .github/workflows/internal-tests.yml | 59 +++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/.github/workflows/internal-tests.yml b/.github/workflows/internal-tests.yml index cd100558a69..3425a011e32 100644 --- a/.github/workflows/internal-tests.yml +++ b/.github/workflows/internal-tests.yml @@ -53,15 +53,70 @@ jobs: github-token: ${{ secrets.SPACETIMEDB_PRIVATE_TOKEN }} script: | const workflowId = 'ci.yml'; - const targetRef = 'master'; const targetOwner = process.env.TARGET_OWNER; const targetRepo = process.env.TARGET_REPO; // Use the ref for pull requests because the head sha is brittle (github does some extra dance where it merges in master). const publicRef = (context.eventName === 'pull_request') ? context.payload.pull_request.head.ref : context.sha; const publicPrNumber = context.payload.pull_request?.number; const inputs = { public_ref: publicRef }; + let targetRef = 'master'; + if (publicPrNumber) { - inputs.public_pr_number = String(publicPrNumber); + // GitHub constructs the job graph from the dispatched ref before + // ci-setup can check out related sources. Dispatch the companion + // branch itself so workflow changes in that PR are exercised too. + const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: publicPrNumber, + per_page: 100, + }); + const privatePrNumbers = [ + ...new Set( + timeline + .filter(event => event.event === 'cross-referenced') + .map(event => event.source?.issue) + .filter(issue => + issue?.repository?.full_name === `${targetOwner}/${targetRepo}` + && issue.pull_request + && issue.state === 'open' + ) + .map(issue => issue.number) + ), + ]; + + if (privatePrNumbers.length > 1) { + core.setFailed( + `Multiple open ${targetOwner}/${targetRepo} PRs reference public PR #${publicPrNumber}: ` + + privatePrNumbers.map(number => `#${number}`).join(', ') + ); + return; + } + + if (privatePrNumbers.length === 1) { + const privatePrNumber = privatePrNumbers[0]; + const privatePr = await github.rest.pulls.get({ + owner: targetOwner, + repo: targetRepo, + pull_number: privatePrNumber, + }); + const privateHeadRepo = privatePr.data.head.repo?.full_name; + if (privateHeadRepo !== `${targetOwner}/${targetRepo}`) { + core.setFailed( + `Refusing to run a workflow from related private PR #${privatePrNumber}: ` + + `expected head repository ${targetOwner}/${targetRepo}, got ${privateHeadRepo || 'unknown'}` + ); + return; + } + + targetRef = privatePr.data.head.ref; + inputs.public_pr_number = String(publicPrNumber); + core.info(`Using related private PR #${privatePrNumber} workflow from ${targetRef}.`); + } else { + // No private companion exists, so run the private master workflow + // and let setup use the public PR with the private master sources. + inputs.public_pr_number = String(publicPrNumber); + } } // Dispatch the workflow in the target repository From 4d86687a61540acc5f3a323db643c01e3f016733 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 30 Jul 2026 17:59:15 -0700 Subject: [PATCH 31/31] don't wait for lints --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6dfbc0415a..6a946fc75d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,7 @@ jobs: overwrite: true smoketest_build_linux: - needs: [merge_queue_noop, lints, typescript_sdk_build] + needs: [merge_queue_noop, typescript_sdk_build] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} name: Build smoketests (Linux) runs-on: spacetimedb-new-runner-2 @@ -180,7 +180,7 @@ jobs: artifact-name: smoketest-build-Linux smoketest_build_windows: - needs: [merge_queue_noop, lints, typescript_sdk_build] + needs: [merge_queue_noop, typescript_sdk_build] if: ${{ needs.merge_queue_noop.outputs.skip != 'true' }} name: Build smoketests (Windows) runs-on: spacetimedb-windows-runner