diff --git a/.cargo/config.toml b/.cargo/config.toml index 755c9d30397..5ee0dda9f02 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,7 @@ rustflags = ["--cfg", "tokio_unstable"] [alias] bump-versions = "run -p upgrade-version --" +ci = "run -p ci --" [target.x86_64-pc-windows-msvc] # Use a different linker. Otherwise, the build fails with some obscure linker error that diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a236c42b66..e9539de4342 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,11 @@ jobs: strategy: matrix: include: - - { runner: spacetimedb-runner, smoketest_args: --docker } - - { runner: windows-latest, smoketest_args: --no-build-cli } - runner: [ spacetimedb-runner, windows-latest ] + - runner: spacetimedb-runner + smoketest_args: '--docker -x clear_database replication' + - runner: windows-latest + smoketest_args: '--no-build-cli -x clear_database replication' + runner: [spacetimedb-runner, windows-latest] runs-on: ${{ matrix.runner }} steps: - name: Find Git ref @@ -87,8 +89,8 @@ jobs: - name: Install python deps run: python -m pip install psycopg2-binary xmltodict - name: Run smoketests - # Note: clear_database and replication only work in private - run: python -m smoketests ${{ matrix.smoketest_args }} -x clear_database replication + run: cargo ci smoketests -- ${{ matrix.smoketest_args }} + - name: Stop containers (Linux) if: always() && runner.os == 'Linux' run: docker compose down @@ -129,30 +131,13 @@ jobs: with: run_install: true - - name: Create /stdb dir - run: | - sudo mkdir /stdb - sudo chmod 777 /stdb - - name: Build typescript module sdk working-directory: crates/bindings-typescript run: pnpm build - name: Run cargo test #Note: Unreal tests will be run separately - run: cargo test --all -- --skip unreal - - - name: Check that the test outputs are up-to-date - run: bash tools/check-diff.sh - - - name: Ensure C# autogen bindings are up-to-date - run: | - cargo run -p spacetimedb-codegen --example regen-csharp-moduledef - bash tools/check-diff.sh crates/bindings-csharp - - - name: C# bindings tests - working-directory: crates/bindings-csharp - run: dotnet test -warnaserror + run: cargo ci test lints: name: Lints @@ -168,30 +153,8 @@ jobs: with: global-json-file: global.json - - name: Run cargo fmt - run: cargo fmt --all -- --check - - - name: Run cargo clippy - run: cargo clippy --all --tests --benches -- -D warnings - - - name: Run C# formatting check - working-directory: crates/bindings-csharp - run: | - dotnet tool restore - dotnet csharpier --check . - - - name: Run `cargo doc` for bindings crate - # `bindings` is the only crate we care strongly about documenting, - # since we link to its docs.rs from our website. - # We won't pass `--no-deps`, though, - # since we want everything reachable through it to also work. - # This includes `sats` and `lib`. - working-directory: crates/bindings - env: - # Make `cargo doc` exit with error on warnings, most notably broken links - RUSTDOCFLAGS: '--deny warnings' - run: | - cargo doc + - name: Run ci lint + run: cargo ci lint wasm_bindings: name: Build and test wasm bindings @@ -203,20 +166,7 @@ jobs: - run: echo ::add-matcher::.github/workflows/rust_matcher.json - name: Run bindgen tests - run: cargo test -p spacetimedb-codegen - - # Make sure the `Cargo.lock` file reflects the latest available versions. - # This is what users would end up with on a fresh module, so we want to - # catch any compile errors arising from a different transitive closure - # of dependencies than what is in the workspace lock file. - # - # For context see also: https://github.com/clockworklabs/SpacetimeDB/pull/2714 - - name: Update dependencies - run: cargo update - - - name: Build module-test - run: cargo run -p spacetimedb-cli -- build --project-path modules/module-test - + run: cargo ci wasm-bindings publish_checks: name: Check that packages are publishable @@ -269,19 +219,9 @@ jobs: run: sudo apt install -y libssl-dev - name: Build spacetimedb-update - run: cargo build --features github-token-auth --target ${{ matrix.target }} -p spacetimedb-update - - - name: Run self-install env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - ROOT_DIR="$(mktemp -d)" - # NOTE(bfops): We need the `github-token-auth` feature because we otherwise tend to get ratelimited when we try to fetch `/releases/latest`. - # My best guess is that, on the GitHub runners, the "anonymous" ratelimit is shared by *all* users of that runner (I think this because it - # happens very frequently on the `macos-runner`, but we haven't seen it on any others). - cargo run --features github-token-auth --target ${{ matrix.target }} -p spacetimedb-update -- self-install --root-dir="${ROOT_DIR}" --yes - "${ROOT_DIR}"/spacetime --root-dir="${ROOT_DIR}" help + run: cargo ci update-flow --target=${{ matrix.target }} --github-token-auth unreal_engine_tests: name: Unreal Engine Tests @@ -303,26 +243,26 @@ jobs: # without this (reassigning env vars and stuff), but was unable to get it to work and it felt like an uphill battle. options: --user 0:0 steps: -# Uncomment this before merging so that it will run properly if run manually through the GH actions flow. It was playing weird with rolled back -# commits though. -# - 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" + # Uncomment this before merging so that it will run properly if run manually through the GH actions flow. It was playing weird with rolled back + # commits though. + # - 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: Run Unreal Engine tests + - name: Install unreal engine test dependencies working-directory: sdks/unreal env: UE_ROOT_PATH: /home/ue4/UnrealEngine @@ -331,43 +271,39 @@ jobs: apt-get update apt-get install -y acl curl ca-certificates - REPO="$GITHUB_WORKSPACE" - # Let ue4 read/write the workspace & tool caches without changing ownership - for p in "$REPO" "${RUNNER_TEMP:-/__t}" "${RUNNER_TOOL_CACHE:-/__t}"; do - [ -d "$p" ] && setfacl -R -m u:ue4:rwX -m d:u:ue4:rwX "$p" || true - done + - name: Run Unreal Engine tests + working-directory: sdks/unreal + env: + UE_ROOT_PATH: /home/ue4/UnrealEngine + run: | + cargo ci unreal-tests - # Rust tool caches live under the runner tool cache so they persist - export CARGO_HOME="${RUNNER_TOOL_CACHE:-/__t}/cargo" - export RUSTUP_HOME="${RUNNER_TOOL_CACHE:-/__t}/rustup" - mkdir -p "$CARGO_HOME" "$RUSTUP_HOME" - chown -R ue4:ue4 "$CARGO_HOME" "$RUSTUP_HOME" - - # Make sure the UE build script is executable (and parents traversable) - UE_DIR="${UE_ROOT_PATH:-/home/ue4/UnrealEngine}" - chmod a+rx "$UE_DIR" "$UE_DIR/Engine" "$UE_DIR/Engine/Build" "$UE_DIR/Engine/Build/BatchFiles/Linux" || true - chmod a+rx "$UE_DIR/Engine/Build/BatchFiles/Linux/Build.sh" || true - - # Run the build & tests as ue4 (who owns the UE tree) - sudo -E -H -u ue4 env \ - HOME=/home/ue4 \ - XDG_CONFIG_HOME=/home/ue4/.config \ - CARGO_HOME="$CARGO_HOME" \ - RUSTUP_HOME="$RUSTUP_HOME" \ - PATH="$CARGO_HOME/bin:$PATH" \ - bash -lc ' - set -euxo pipefail - # Install rustup for ue4 if needed (uses the shared caches) - if ! command -v cargo >/dev/null 2>&1; then - curl -sSf https://sh.rustup.rs | sh -s -- -y - fi - rustup show >/dev/null - git config --global --add safe.directory "$GITHUB_WORKSPACE" || true - - cd "$GITHUB_WORKSPACE/sdks/unreal" - cargo --version - cargo test - ' + ci_command_docs: + name: Check CI command docs + runs-on: ubuntu-latest + 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: Check for docs change + run: cargo ci self-docs --check cli_docs: name: Check CLI docs @@ -418,12 +354,4 @@ jobs: - name: Check for docs change run: | - cargo run --features markdown-docs -p spacetimedb-cli > docs/docs/cli-reference.md - pnpm format - git status - if git diff --exit-code HEAD; then - echo "No docs changes detected" - else - echo "It looks like the CLI docs have changed:" - exit 1 - fi + cargo ci cli-docs diff --git a/Cargo.toml b/Cargo.toml index 5bb1d180aed..b534f64acfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "sdks/rust/tests/test-client", "sdks/rust/tests/test-counter", "sdks/rust/tests/connect_disconnect_client", + "tools/ci", "tools/upgrade-version", "tools/license-check", "crates/bindings-typescript/test-app/server", diff --git a/tools/ci/Cargo.toml b/tools/ci/Cargo.toml new file mode 100644 index 00000000000..e907526a42c --- /dev/null +++ b/tools/ci/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ci" +version = "0.1.0" +edition.workspace = true + +[dependencies] +log.workspace = true +anyhow.workspace = true +chrono = { workspace = true, features=["clock"] } +clap.workspace = true +regex.workspace = true +duct.workspace = true diff --git a/tools/ci/README.md b/tools/ci/README.md new file mode 100644 index 00000000000..0a4a83b1264 --- /dev/null +++ b/tools/ci/README.md @@ -0,0 +1,164 @@ +# SpacetimeDB's cargo ci + +## Overview + +This document provides an overview of the `cargo ci` command-line tool, and documentation for each of its subcommands and options. + +## `cargo ci` + +SpacetimeDB CI tasks + +This tool provides several subcommands for automating CI workflows in SpacetimeDB. + +It may be invoked via `cargo ci `, or simply `cargo ci` to run all subcommands in sequence. It is mostly designed to be run in CI environments via the github workflows, but can also be run locally + +**Usage:** +```bash +Usage: cargo ci [OPTIONS] [COMMAND] +``` + +**Options:** + +- `--skip`: Skip specified subcommands when running all + +When no subcommand is specified, all subcommands are run in sequence. This option allows specifying subcommands to skip when running all. For example, to skip the `unreal-tests` subcommand, use `--skip unreal-tests`. + +- `--help`: Print help (see a summary with '-h') + +### `test` + +Runs tests + +Runs rust tests, codegens csharp sdk and runs csharp tests. This does not include Unreal tests. This expects to run in a clean git state. + +**Usage:** +```bash +Usage: test +``` + +**Options:** + +- `--help`: Print help (see a summary with '-h') + +### `lint` + +Lints the codebase + +Runs rustfmt, clippy, csharpier and generates rust docs to ensure there are no warnings. + +**Usage:** +```bash +Usage: lint +``` + +**Options:** + +- `--help`: Print help (see a summary with '-h') + +### `wasm-bindings` + +Tests Wasm bindings + +Runs tests for the codegen crate and builds a test module with the wasm bindings. + +**Usage:** +```bash +Usage: wasm-bindings +``` + +**Options:** + +- `--help`: Print help (see a summary with '-h') + +### `smoketests` + +Runs smoketests + +Executes the smoketests suite with some default exclusions. + +**Usage:** +```bash +Usage: smoketests [ARGS]... +``` + +**Options:** + +- `args`: Additional arguments to pass to the smoketests runner. These are usually set by the CI environment, such as `-- --docker` +- `--help`: Print help (see a summary with '-h') + +### `update-flow` + +Tests the update flow + +Tests the self-update flow by building the spacetimedb-update binary for the specified target, by default the current target, and performing a self-install into a temporary directory. + +**Usage:** +```bash +Usage: update-flow [OPTIONS] +``` + +**Options:** + +- `--target`: Target triple to build for, by default the current target. Used by github workflows to check the update flow on multiple platforms. +- `--github-token-auth`: Whether to enable github token authentication feature when building the update binary. By default this is disabled. +- `--help`: Print help (see a summary with '-h') + +### `unreal-tests` + +Run Unreal Engine related tests + +This assumes the UE4 environment is already set up + +This is designed to run in the github actions environment, but should work locally if the Unreal environment is set up correctly. + +**Usage:** +```bash +Usage: unreal-tests +``` + +**Options:** + +- `--help`: Print help (see a summary with '-h') + +### `cli-docs` + +**Usage:** +```bash +Usage: cli-docs +``` + +**Options:** + +- `--help`: + +### `self-docs` + +**Usage:** +```bash +Usage: self-docs [OPTIONS] +``` + +**Options:** + +- `--check`: Only check for changes, do not generate the docs +- `--help`: Print help (see a summary with '-h') + +### `help` + +**Usage:** +```bash +Usage: help [COMMAND]... +``` + +**Options:** + +- `subcommand`: + + +--- + +This document is auto-generated by running: + +```bash +cargo ci self-docs +``` \ No newline at end of file diff --git a/tools/ci/build.rs b/tools/ci/build.rs new file mode 100644 index 00000000000..b821cf9a10d --- /dev/null +++ b/tools/ci/build.rs @@ -0,0 +1,4 @@ +#[allow(clippy::disallowed_macros)] +fn main() { + println!("cargo:rustc-env=TARGET={}", std::env::var("TARGET").unwrap_or_default()); +} diff --git a/tools/ci/src/ci_docs.rs b/tools/ci/src/ci_docs.rs new file mode 100644 index 00000000000..f6beb2eb8ef --- /dev/null +++ b/tools/ci/src/ci_docs.rs @@ -0,0 +1,68 @@ +use clap::{Command, CommandFactory}; + +use crate::Cli; + +pub fn generate_cli_docs() -> String { + let mut cli = Cli::command(); + let usage = generate_markdown(&mut cli, 2); + + format!( + "\ +# SpacetimeDB's cargo ci + +## Overview + +This document provides an overview of the `cargo ci` command-line tool, and documentation for each of its subcommands and options. + +{} +--- + +This document is auto-generated by running: + +```bash +cargo ci self-docs +```", + usage + ) +} + +fn generate_markdown(cmd: &mut Command, heading_level: usize) -> String { + let mut out = String::new(); + + let heading = "#".repeat(heading_level); + out.push_str(&format!("{} `{}`\n\n", heading, cmd.get_name())); + + if let Some(long_about) = cmd.get_long_about() { + out.push_str(&format!("{}\n\n", long_about)); + } + + out.push_str(&format!("**Usage:**\n```bash\n{}\n```\n\n", cmd.render_usage())); + + let mut options = String::new(); + for arg in cmd.get_arguments() { + let names = arg + .get_long() + .map(|l| format!("--{}", l)) + .or_else(|| arg.get_short().map(|s| format!("-{}", s))) + .unwrap_or_else(|| arg.get_id().to_string()); + let help = arg.get_long_help().unwrap_or_default(); + options.push_str(&format!( + "- `{}`: {}\n{}", + names, + help, + if help.to_string().lines().count() > 1 { "\n" } else { "" } + )); + } + + if !options.is_empty() { + out.push_str("**Options:**\n\n"); + out.push_str(&options); + out.push('\n'); + } + + for sub in cmd.get_subcommands_mut() { + out.push_str(&generate_markdown(sub, heading_level + 1)); + } + + out +} diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs new file mode 100644 index 00000000000..b939b94e5d8 --- /dev/null +++ b/tools/ci/src/main.rs @@ -0,0 +1,251 @@ +use anyhow::{bail, Result}; +use clap::{CommandFactory, Parser, Subcommand}; +use duct::cmd; +use std::collections::HashMap; +use std::path::Path; +use std::{env, fs}; + +const README_PATH: &str = "tools/ci/README.md"; + +mod ci_docs; + +/// SpacetimeDB CI tasks +/// +/// This tool provides several subcommands for automating CI workflows in SpacetimeDB. +/// +/// It may be invoked via `cargo ci `, or simply `cargo ci` to run all subcommands in +/// sequence. It is mostly designed to be run in CI environments via the github workflows, but can +/// also be run locally +#[derive(Parser)] +#[command(name = "cargo ci", subcommand_required = false, arg_required_else_help = false)] +struct Cli { + #[command(subcommand)] + cmd: Option, + + /// Skip specified subcommands when running all + /// + /// When no subcommand is specified, all subcommands are run in sequence. This option allows + /// specifying subcommands to skip when running all. For example, to skip the `unreal-tests` + /// subcommand, use `--skip unreal-tests`. + #[arg(long)] + skip: Vec, +} + +#[derive(Subcommand)] +enum CiCmd { + /// Runs tests + /// + /// Runs rust tests, codegens csharp sdk and runs csharp tests. + /// This does not include Unreal tests. + /// This expects to run in a clean git state. + Test, + /// Lints the codebase + /// + /// Runs rustfmt, clippy, csharpier and generates rust docs to ensure there are no warnings. + Lint, + /// Tests Wasm bindings + /// + /// Runs tests for the codegen crate and builds a test module with the wasm bindings. + WasmBindings, + /// Runs smoketests + /// + /// Executes the smoketests suite with some default exclusions. + Smoketests { + #[arg( + trailing_var_arg = true, + long_help = "Additional arguments to pass to the smoketests runner. These are usually set by the CI environment, such as `-- --docker`" + )] + args: Vec, + }, + /// Tests the update flow + /// + /// Tests the self-update flow by building the spacetimedb-update binary for the specified + /// target, by default the current target, and performing a self-install into a temporary + /// directory. + UpdateFlow { + #[arg( + long, + long_help = "Target triple to build for, by default the current target. Used by github workflows to check the update flow on multiple platforms." + )] + target: Option, + #[arg( + long, + default_value = "false", + long_help = "Whether to enable github token authentication feature when building the update binary. By default this is disabled." + )] + github_token_auth: bool, + }, + /// Run Unreal Engine related tests + /// + /// This assumes the UE4 environment is already set up + /// + /// This is designed to run in the github actions environment, but should work locally if the + /// Unreal environment is set up correctly. + UnrealTests, + /// Generates CLI documentation and checks for changes + CliDocs, + SelfDocs { + #[arg( + long, + default_value_t = false, + long_help = "Only check for changes, do not generate the docs" + )] + check: bool, + }, +} + +macro_rules! run { + ($cmdline:expr) => { + run_command($cmdline, &Vec::new()) + }; + ($cmdline:expr, $envs:expr) => { + run_command($cmdline, $envs) + }; +} + +fn run_all_clap_subcommands(skips: &[String]) -> Result<()> { + let subcmds = Cli::command() + .get_subcommands() + .map(|sc| sc.get_name().to_string()) + .collect::>(); + + for subcmd in subcmds { + if skips.contains(&subcmd) { + log::info!("skipping {subcmd} as requested"); + continue; + } + log::info!("executing cargo ci {subcmd}"); + run!(&format!("cargo ci {subcmd}"))?; + } + + Ok(()) +} + +fn run_command(cmdline: &str, additional_env: &[(&str, &str)]) -> Result<()> { + let mut env = env::vars().collect::>(); + env.extend(additional_env.iter().map(|(k, v)| (k.to_string(), v.to_string()))); + log::debug!("$ {cmdline}"); + let status = cmd!("bash", "-lc", cmdline).full_env(env).run()?; + if !status.status.success() { + let e = anyhow::anyhow!("command failed: {cmdline}"); + log::error!("{e}"); + return Err(e); + } + Ok(()) +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.cmd { + Some(CiCmd::Test) => { + run!("cargo test --all -- --skip unreal")?; + run!("bash tools/check-diff.sh")?; + run!("cargo run -p spacetimedb-codegen --example regen-csharp-moduledef && bash tools/check-diff.sh crates/bindings-csharp")?; + run!("(cd crates/bindings-csharp && dotnet test -warnaserror)")?; + } + + Some(CiCmd::Lint) => { + run!("cargo fmt --all -- --check")?; + run!("cargo clippy --all --tests --benches -- -D warnings")?; + run!("(cd crates/bindings-csharp && dotnet tool restore && dotnet csharpier --check .)")?; + run!( + "cd crates/bindings && cargo doc", + &[("RUSTDOCFLAGS", "--deny warnings")] + )?; + } + + Some(CiCmd::WasmBindings) => { + run!("cargo test -p spacetimedb-codegen")?; + run!("cargo update")?; + run!("cargo run -p spacetimedb-cli -- build --project-path modules/module-test")?; + } + + Some(CiCmd::Smoketests { mut args }) => { + let default_args = ["-x", "clear_database", "replication"]; + if args.is_empty() { + args = default_args.iter().map(ToString::to_string).collect(); + } + // Note: clear_database and replication only work in private + run!(&format!("python -m smoketests {}", args.join(" ")))?; + } + + Some(CiCmd::UpdateFlow { + target, + github_token_auth, + }) => { + let target = target.map(|t| format!("--target {t}")).unwrap_or_default(); + let github_token_auth_flag = if github_token_auth { + "--features github-token-auth " + } else { + "" + }; + + run!(&format!("echo 'checking update flow for target: {target}'"))?; + run!(&format!( + "cargo build {github_token_auth_flag}{target} -p spacetimedb-update" + ))?; + // NOTE(bfops): We need the `github-token-auth` feature because we otherwise tend to get ratelimited when we try to fetch `/releases/latest`. + // My best guess is that, on the GitHub runners, the "anonymous" ratelimit is shared by *all* users of that runner (I think this because it + // happens very frequently on the `macos-runner`, but we haven't seen it on any others). + run!(&format!( + r#" +ROOT_DIR="$(mktemp -d)" +cargo run {github_token_auth_flag}--target {target} -p spacetimedb-update -- self-install --root-dir="${{ROOT_DIR}}" --yes +"${{ROOT_DIR}}"/spacetime --root-dir="${{ROOT_DIR}}" help + "# + ))?; + } + + Some(CiCmd::UnrealTests) => { + run!("for p in \"$GITHUB_WORKSPACE\" \"${RUNNER_TEMP:-/__t}\" \"${RUNNER_TOOL_CACHE:-/__t}\"; do [ -d \"$p\" ] && setfacl -R -m u:ue4:rwX -m d:u:ue4:rwX \"$p\" || true; done")?; + + run!("export CARGO_HOME=\"${RUNNER_TOOL_CACHE:-/__t}/cargo\"")?; + run!("export RUSTUP_HOME=\"${RUNNER_TOOL_CACHE:-/__t}/rustup\"")?; + run!("mkdir -p \"$CARGO_HOME\" \"$RUSTUP_HOME\"")?; + + run!("chmod a+rx \"$UE_ROOT_PATH\" \"$UE_ROOT_PATH/Engine\" \"$UE_ROOT_PATH/Engine/Build\" \"$UE_ROOT_PATH/Engine/Build/BatchFiles/Linux\" || true")?; + run!("chmod a+rx \"$UE_ROOT_PATH/Engine/Build/BatchFiles/Linux/Build.sh\" || true")?; + + run!("sudo -E -H -u ue4 env HOME=/home/ue4 CARGO_HOME=\"$CARGO_HOME\" RUSTUP_HOME=\"$RUSTUP_HOME\" PATH=\"$CARGO_HOME/bin:$PATH\" bash -lc 'set -euxo pipefail; if ! command -v cargo >/dev/null 2>&1; then curl -sSf https://sh.rustup.rs | sh -s -- -y; fi; rustup show >/dev/null; git config --global --add safe.directory \"$GITHUB_WORKSPACE\" || true; cd \"$GITHUB_WORKSPACE/sdks/unreal\"; cargo --version; cargo test'")?; + } + + Some(CiCmd::CliDocs) => { + run!("pnpm install --recursive")?; + run!("cargo run --features markdown-docs -p spacetimedb-cli > docs/docs/cli-reference.md")?; + run!("pnpm format")?; + run!("git status")?; + run!( + r#" +if git diff --exit-code HEAD; then + echo "No docs changes detected" +else + echo "It looks like the CLI docs have changed:" + exit 1 +fi + "# + )?; + } + + Some(CiCmd::SelfDocs { check }) => { + let readme_content = ci_docs::generate_cli_docs(); + let path = Path::new(README_PATH); + + if check { + let existing = fs::read_to_string(path).unwrap_or_default(); + if existing != readme_content { + bail!("README.md is out of date. Please run `cargo ci self-docs` to update it."); + } else { + log::info!("README.md is up to date."); + } + } else { + fs::write(path, readme_content)?; + log::info!("Wrote CLI docs to {}", path.display()); + } + } + + None => run_all_clap_subcommands(&cli.skip)?, + } + + Ok(()) +}