From dea48f4bb4a67146ca2a829eda332b344bdebfc2 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Wed, 24 Sep 2025 15:22:40 -0400 Subject: [PATCH 1/7] feat(ci): add cargo setup ci command and GitHub Actions workflow - Add --ci flag to setup tool for CI builds without installation - Create CI-specific build functions for Rust, TypeScript, and Swift - Add comprehensive GitHub Actions workflow with caching - Support cross-platform builds (Ubuntu and macOS) - Include conditional test running for all languages - Optimize for CI with npm ci and production webpack builds Co-authored-by: Claude --- .github/workflows/ci.yml | 77 ++++++++++++++++++++ setup/src/main.rs | 148 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..1eb3ff41 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + symposium/mcp-server/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: symposium/vscode-extension/package-lock.json + + - name: Cache Node.js dependencies + uses: actions/cache@v4 + with: + path: symposium/vscode-extension/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('symposium/vscode-extension/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Run CI build + run: cargo setup --ci + + - name: Run Rust tests + run: cargo test --workspace + + - name: Run TypeScript tests (if they exist) + run: | + cd symposium/vscode-extension + if [ -f "package.json" ] && grep -q '"test"' package.json; then + npm test + else + echo "No TypeScript tests found, skipping" + fi + + - name: Run Swift tests (if they exist) + if: runner.os == 'macOS' + run: | + cd symposium/macos-app + if [ -d "Tests" ]; then + swift test + else + echo "No Swift tests found, skipping" + fi diff --git a/setup/src/main.rs b/setup/src/main.rs index 42f30131..bdd5bcad 100644 --- a/setup/src/main.rs +++ b/setup/src/main.rs @@ -18,6 +18,7 @@ Build Symposium components and set up for development with AI assistants Examples: cargo setup # Show help and usage + cargo setup --ci # CI mode: build all components without installation cargo setup --all # Build everything and setup for development cargo setup --vscode # Build/install VSCode extension only cargo setup --mcp # Build/install MCP server only @@ -29,11 +30,15 @@ Examples: Prerequisites: - Rust and Cargo (https://rustup.rs/) - Node.js and npm (for VSCode extension) - - VSCode with 'code' command available - - Q CLI or Claude Code (for MCP server) + - VSCode with 'code' command available (for development setup) + - Q CLI or Claude Code (for MCP server setup) "# )] struct Args { + /// CI mode: build all components for continuous integration + #[arg(long)] + ci: bool, + /// Build all components (VSCode extension, MCP server, and macOS app) #[arg(long)] all: bool, @@ -62,7 +67,12 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); - // Validate flag combinations first + // Handle CI mode first + if args.ci { + return run_ci_mode(); + } + + // Validate flag combinations for regular mode if args.open && !args.app && !args.all { return Err(anyhow!("āŒ --open requires --app")); } @@ -133,6 +143,7 @@ fn show_help() { println!("Usage: cargo setup [OPTIONS]"); println!(); println!("Options:"); + println!(" --ci CI mode: build all components for continuous integration"); println!( " --all Build all components (VSCode extension, MCP server, and macOS app)" ); @@ -144,6 +155,7 @@ fn show_help() { println!(" --help Show this help message"); println!(); println!("Examples:"); + println!(" cargo setup --ci # CI build (all components)"); println!(" cargo setup --all # Build everything"); println!(" cargo setup --vscode # Build VSCode extension only"); println!(" cargo setup --mcp --restart # Build MCP server and restart daemon"); @@ -151,6 +163,30 @@ fn show_help() { println!(" cargo setup --vscode --mcp --app # Build all components"); } +/// CI mode: build all components without installation or setup +fn run_ci_mode() -> Result<()> { + println!("šŸ¤– Symposium CI Build"); + println!("{}", "=".repeat(25)); + + // Check basic prerequisites for CI + check_rust()?; + check_node_ci()?; + + // Build all components in CI mode + build_rust_server_ci()?; + build_extension_ci()?; + + // Only build macOS app on macOS + if cfg!(target_os = "macos") { + build_macos_app_ci()?; + } else { + println!("ā­ļø Skipping macOS app build (not on macOS)"); + } + + println!("\nāœ… All components built successfully!"); + Ok(()) +} + fn check_rust() -> Result<()> { if which::which("cargo").is_err() { return Err(anyhow!( @@ -160,6 +196,112 @@ fn check_rust() -> Result<()> { Ok(()) } +fn check_node_ci() -> Result<()> { + if which::which("npm").is_err() { + return Err(anyhow!( + "āŒ Error: npm not found. Please install Node.js first.\n Visit: https://nodejs.org/" + )); + } + Ok(()) +} + +/// Build Rust MCP server in CI mode (no installation) +fn build_rust_server_ci() -> Result<()> { + let repo_root = get_repo_root()?; + let server_dir = repo_root.join("symposium/mcp-server"); + + println!("šŸ¦€ Building Rust MCP server..."); + println!(" Building in: {}", server_dir.display()); + + let output = Command::new("cargo") + .args(["build", "--release"]) + .current_dir(&server_dir) + .output() + .context("Failed to execute cargo build")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Failed to build Rust server:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… Rust server built successfully!"); + Ok(()) +} + +/// Build VSCode extension in CI mode (no installation) +fn build_extension_ci() -> Result<()> { + let repo_root = get_repo_root()?; + let extension_dir = repo_root.join("symposium/vscode-extension"); + + println!("\nšŸ“¦ Building VSCode extension..."); + + // Install dependencies + println!("šŸ“„ Installing extension dependencies..."); + let output = Command::new("npm") + .args(["ci"]) // Use npm ci for faster, reproducible builds in CI + .current_dir(&extension_dir) + .output() + .context("Failed to execute npm ci")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Failed to install extension dependencies:\n Error: {}", + stderr.trim() + )); + } + + // Build extension for production + println!("šŸ”Ø Building extension..."); + let output = Command::new("npm") + .args(["run", "webpack"]) + .current_dir(&extension_dir) + .output() + .context("Failed to execute npm run webpack")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Failed to build extension:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… VSCode extension built successfully!"); + Ok(()) +} + +/// Build macOS app in CI mode +fn build_macos_app_ci() -> Result<()> { + let repo_root = get_repo_root()?; + let app_dir = repo_root.join("symposium").join("macos-app"); + + println!("\nšŸŽ Building macOS application..."); + println!(" Building in: {}", app_dir.display()); + + let output = Command::new("swift") + .args(["build", "--configuration", "release"]) + .current_dir(&app_dir) + .output() + .context("Failed to execute swift build")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(anyhow!( + "āŒ Failed to build macOS app:\n stdout: {}\n stderr: {}", + stdout.trim(), + stderr.trim() + )); + } + + println!("āœ… macOS application built successfully!"); + Ok(()) +} + fn check_node() -> Result<()> { if which::which("npm").is_err() { return Err(anyhow!( From a967704f22b80433aba980e002ac1c205e8e0b2d Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Wed, 24 Sep 2025 15:44:06 -0400 Subject: [PATCH 2/7] refactor(setup): change from --ci flag to ci subcommand - Replace --ci flag with proper `cargo setup ci` subcommand - Add Commands enum with Ci variant for better CLI structure - Update help text to show subcommands section - Update GitHub Actions workflow to use new subcommand syntax - Improves usability by making ci a distinct mode rather than conflicting flag Co-authored-by: Claude --- .github/workflows/ci.yml | 2 +- setup/src/main.rs | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eb3ff41..c6da9d1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: ${{ runner.os }}-node- - name: Run CI build - run: cargo setup --ci + run: cargo setup ci - name: Run Rust tests run: cargo test --workspace diff --git a/setup/src/main.rs b/setup/src/main.rs index bdd5bcad..901896a5 100644 --- a/setup/src/main.rs +++ b/setup/src/main.rs @@ -18,7 +18,7 @@ Build Symposium components and set up for development with AI assistants Examples: cargo setup # Show help and usage - cargo setup --ci # CI mode: build all components without installation + cargo setup ci # CI mode: build all components without installation cargo setup --all # Build everything and setup for development cargo setup --vscode # Build/install VSCode extension only cargo setup --mcp # Build/install MCP server only @@ -35,9 +35,8 @@ Prerequisites: "# )] struct Args { - /// CI mode: build all components for continuous integration - #[arg(long)] - ci: bool, + #[command(subcommand)] + command: Option, /// Build all components (VSCode extension, MCP server, and macOS app) #[arg(long)] @@ -64,11 +63,17 @@ struct Args { restart: bool, } +#[derive(Parser)] +enum Commands { + /// CI mode: build all components for continuous integration + Ci, +} + fn main() -> Result<()> { let args = Args::parse(); - // Handle CI mode first - if args.ci { + // Handle CI subcommand first + if let Some(Commands::Ci) = args.command { return run_ci_mode(); } @@ -143,10 +148,7 @@ fn show_help() { println!("Usage: cargo setup [OPTIONS]"); println!(); println!("Options:"); - println!(" --ci CI mode: build all components for continuous integration"); - println!( - " --all Build all components (VSCode extension, MCP server, and macOS app)" - ); + println!(" --all Build all components (VSCode extension, MCP server, and macOS app)"); println!(" --vscode Build/install VSCode extension"); println!(" --mcp Build/install MCP server"); println!(" --app Build the Symposium macOS app"); @@ -154,8 +156,11 @@ fn show_help() { println!(" --restart Restart MCP daemon after building (requires --mcp)"); println!(" --help Show this help message"); println!(); + println!("Subcommands:"); + println!(" ci CI mode: build all components for continuous integration"); + println!(); println!("Examples:"); - println!(" cargo setup --ci # CI build (all components)"); + println!(" cargo setup ci # CI build (all components)"); println!(" cargo setup --all # Build everything"); println!(" cargo setup --vscode # Build VSCode extension only"); println!(" cargo setup --mcp --restart # Build MCP server and restart daemon"); From 3e2522c2f95385a5e178242b75914361836756ee Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Wed, 24 Sep 2025 16:12:50 -0400 Subject: [PATCH 3/7] perf(ci): use cargo check instead of cargo build for faster CI - Replace `cargo build --release` with `cargo check --release` in CI mode - Cargo check only does type checking without code generation - Significantly faster while still catching all compilation errors - Update function name and comments to reflect checking vs building Co-authored-by: Claude --- setup/src/main.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/setup/src/main.rs b/setup/src/main.rs index 901896a5..4aadad62 100644 --- a/setup/src/main.rs +++ b/setup/src/main.rs @@ -210,29 +210,29 @@ fn check_node_ci() -> Result<()> { Ok(()) } -/// Build Rust MCP server in CI mode (no installation) +/// Build Rust MCP server in CI mode (compilation check only) fn build_rust_server_ci() -> Result<()> { let repo_root = get_repo_root()?; let server_dir = repo_root.join("symposium/mcp-server"); - println!("šŸ¦€ Building Rust MCP server..."); - println!(" Building in: {}", server_dir.display()); + println!("šŸ¦€ Checking Rust MCP server..."); + println!(" Checking in: {}", server_dir.display()); let output = Command::new("cargo") - .args(["build", "--release"]) + .args(["check", "--release"]) .current_dir(&server_dir) .output() - .context("Failed to execute cargo build")?; + .context("Failed to execute cargo check")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow!( - "āŒ Failed to build Rust server:\n Error: {}", + "āŒ Failed to check Rust server:\n Error: {}", stderr.trim() )); } - println!("āœ… Rust server built successfully!"); + println!("āœ… Rust server check passed!"); Ok(()) } From 55a257e9fff8bf0ac12f6dd083c05dbdf7048388 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 25 Sep 2025 06:16:59 -0400 Subject: [PATCH 4/7] feat(ci): add separate check and test subcommands - Add CiCommands enum with Check and Test variants - Implement cargo setup ci check for compilation verification - Implement cargo setup ci test for running all tests - Add dedicated test functions for Rust, TypeScript, and Swift - Update GitHub Actions to use both check and test commands - Maintain backward compatibility with bare 'ci' defaulting to check Co-authored-by: Claude --- .github/workflows/ci.yml | 27 +----- setup/src/main.rs | 172 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6da9d1e..590b4b31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,27 +51,8 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - name: Run CI build - run: cargo setup ci + - name: Run CI check + run: cargo setup ci check - - name: Run Rust tests - run: cargo test --workspace - - - name: Run TypeScript tests (if they exist) - run: | - cd symposium/vscode-extension - if [ -f "package.json" ] && grep -q '"test"' package.json; then - npm test - else - echo "No TypeScript tests found, skipping" - fi - - - name: Run Swift tests (if they exist) - if: runner.os == 'macOS' - run: | - cd symposium/macos-app - if [ -d "Tests" ]; then - swift test - else - echo "No Swift tests found, skipping" - fi + - name: Run CI tests + run: cargo setup ci test diff --git a/setup/src/main.rs b/setup/src/main.rs index 4aadad62..3ac8fd5a 100644 --- a/setup/src/main.rs +++ b/setup/src/main.rs @@ -18,7 +18,9 @@ Build Symposium components and set up for development with AI assistants Examples: cargo setup # Show help and usage - cargo setup ci # CI mode: build all components without installation + cargo setup ci # CI mode: check all components compile (default) + cargo setup ci check # CI mode: check all components compile + cargo setup ci test # CI mode: run all tests cargo setup --all # Build everything and setup for development cargo setup --vscode # Build/install VSCode extension only cargo setup --mcp # Build/install MCP server only @@ -66,15 +68,30 @@ struct Args { #[derive(Parser)] enum Commands { /// CI mode: build all components for continuous integration - Ci, + Ci { + #[command(subcommand)] + command: Option, + }, +} + +#[derive(Parser)] +enum CiCommands { + /// Check that all components compile + Check, + /// Run all tests + Test, } fn main() -> Result<()> { let args = Args::parse(); // Handle CI subcommand first - if let Some(Commands::Ci) = args.command { - return run_ci_mode(); + if let Some(Commands::Ci { command }) = args.command { + return match command { + Some(CiCommands::Check) => run_ci_check(), + Some(CiCommands::Test) => run_ci_test(), + None => run_ci_check(), // Default to check if no subcommand + }; } // Validate flag combinations for regular mode @@ -157,10 +174,14 @@ fn show_help() { println!(" --help Show this help message"); println!(); println!("Subcommands:"); - println!(" ci CI mode: build all components for continuous integration"); + println!(" ci CI mode: check compilation and run tests"); + println!(" ci check Check that all components compile"); + println!(" ci test Run all tests"); println!(); println!("Examples:"); - println!(" cargo setup ci # CI build (all components)"); + println!(" cargo setup ci # CI check (default)"); + println!(" cargo setup ci check # Check compilation"); + println!(" cargo setup ci test # Run all tests"); println!(" cargo setup --all # Build everything"); println!(" cargo setup --vscode # Build VSCode extension only"); println!(" cargo setup --mcp --restart # Build MCP server and restart daemon"); @@ -168,16 +189,16 @@ fn show_help() { println!(" cargo setup --vscode --mcp --app # Build all components"); } -/// CI mode: build all components without installation or setup -fn run_ci_mode() -> Result<()> { - println!("šŸ¤– Symposium CI Build"); - println!("{}", "=".repeat(25)); +/// CI check mode: verify all components compile +fn run_ci_check() -> Result<()> { + println!("šŸ¤– Symposium CI Check"); + println!("{}", "=".repeat(26)); // Check basic prerequisites for CI check_rust()?; check_node_ci()?; - // Build all components in CI mode + // Check all components compile build_rust_server_ci()?; build_extension_ci()?; @@ -188,7 +209,33 @@ fn run_ci_mode() -> Result<()> { println!("ā­ļø Skipping macOS app build (not on macOS)"); } - println!("\nāœ… All components built successfully!"); + println!("\nāœ… All components check passed!"); + Ok(()) +} + +/// CI test mode: run all tests +fn run_ci_test() -> Result<()> { + println!("šŸ¤– Symposium CI Test"); + println!("{}", "=".repeat(25)); + + // Check basic prerequisites for CI + check_rust()?; + check_node_ci()?; + + // Run Rust tests + run_rust_tests()?; + + // Run TypeScript tests if they exist + run_typescript_tests()?; + + // Run Swift tests if they exist (macOS only) + if cfg!(target_os = "macos") { + run_swift_tests()?; + } else { + println!("ā­ļø Skipping Swift tests (not on macOS)"); + } + + println!("\nāœ… All tests completed!"); Ok(()) } @@ -307,6 +354,107 @@ fn build_macos_app_ci() -> Result<()> { Ok(()) } +/// Run Rust tests +fn run_rust_tests() -> Result<()> { + let repo_root = get_repo_root()?; + + println!("šŸ¦€ Running Rust tests..."); + println!(" Testing workspace in: {}", repo_root.display()); + + let output = Command::new("cargo") + .args(["test", "--workspace"]) + .current_dir(&repo_root) + .output() + .context("Failed to execute cargo test")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Rust tests failed:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… Rust tests passed!"); + Ok(()) +} + +/// Run TypeScript tests if they exist +fn run_typescript_tests() -> Result<()> { + let repo_root = get_repo_root()?; + let extension_dir = repo_root.join("symposium/vscode-extension"); + + println!("\nšŸ“¦ Checking for TypeScript tests..."); + + // Check if package.json has a test script + let package_json_path = extension_dir.join("package.json"); + if !package_json_path.exists() { + println!("ā­ļø No package.json found, skipping TypeScript tests"); + return Ok(()); + } + + let package_json = std::fs::read_to_string(&package_json_path) + .context("Failed to read package.json")?; + + if !package_json.contains("\"test\"") { + println!("ā­ļø No test script found in package.json, skipping TypeScript tests"); + return Ok(()); + } + + println!("šŸ”Ø Running TypeScript tests..."); + let output = Command::new("npm") + .args(["test"]) + .current_dir(&extension_dir) + .output() + .context("Failed to execute npm test")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ TypeScript tests failed:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… TypeScript tests passed!"); + Ok(()) +} + +/// Run Swift tests if they exist +fn run_swift_tests() -> Result<()> { + let repo_root = get_repo_root()?; + let app_dir = repo_root.join("symposium").join("macos-app"); + + println!("\nšŸŽ Checking for Swift tests..."); + + // Check if Tests directory exists + let tests_dir = app_dir.join("Tests"); + if !tests_dir.exists() { + println!("ā­ļø No Tests directory found, skipping Swift tests"); + return Ok(()); + } + + println!("šŸ”Ø Running Swift tests..."); + let output = Command::new("swift") + .args(["test"]) + .current_dir(&app_dir) + .output() + .context("Failed to execute swift test")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(anyhow!( + "āŒ Swift tests failed:\n stdout: {}\n stderr: {}", + stdout.trim(), + stderr.trim() + )); + } + + println!("āœ… Swift tests passed!"); + Ok(()) +} + fn check_node() -> Result<()> { if which::which("npm").is_err() { return Err(anyhow!( From e8fa5c40cecdb16f538c2ce4bea90800ff3372cd Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 25 Sep 2025 06:55:48 -0400 Subject: [PATCH 5/7] refactor(ci): extract CI functionality into dedicated crate with cargo alias - Create new `ci` crate in its own directory with dedicated CI logic - Add `cargo ci` alias in .cargo/config.toml for cleaner command interface - Move all CI functionality from setup tool to dedicated ci tool - Support `cargo ci check` and `cargo ci test` subcommands - Update GitHub Actions workflow to use new `cargo ci` commands - Clean up setup tool by removing CI-related code and subcommands - Maintain separation of concerns: setup for development, ci for automation Co-authored-by: Claude --- .cargo/config.toml | 3 +- .github/workflows/ci.yml | 4 +- Cargo.lock | 9 ++ Cargo.toml | 2 +- ci/Cargo.toml | 17 ++ ci/src/main.rs | 329 +++++++++++++++++++++++++++++++++++++++ setup/src/main.rs | 301 +---------------------------------- 7 files changed, 364 insertions(+), 301 deletions(-) create mode 100644 ci/Cargo.toml create mode 100644 ci/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 0ac7a79a..09d70a1c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,3 @@ [alias] -setup = "run -p setup --" \ No newline at end of file +setup = "run -p setup --" +ci = "run -p ci --" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 590b4b31..75097b88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: ${{ runner.os }}-node- - name: Run CI check - run: cargo setup ci check + run: cargo ci check - name: Run CI tests - run: cargo setup ci test + run: cargo ci test diff --git a/Cargo.lock b/Cargo.lock index 312494ed..ea0ec570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "ci" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "which", +] + [[package]] name = "clap" version = "4.5.46" diff --git a/Cargo.toml b/Cargo.toml index 802b81f8..930eb742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["symposium/mcp-server", "setup", "md-rfd-preprocessor"] +members = ["symposium/mcp-server", "setup", "md-rfd-preprocessor", "ci"] resolver = "2" [workspace.dependencies] diff --git a/ci/Cargo.toml b/ci/Cargo.toml new file mode 100644 index 00000000..54814d8e --- /dev/null +++ b/ci/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ci" +version = "0.1.0" +edition = "2021" +authors = ["Niko Matsakis"] +description = "Symposium CI tool for building and testing all components" + +[[bin]] +name = "ci" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } + +# Additional dependencies for CI functionality +which = { workspace = true } diff --git a/ci/src/main.rs b/ci/src/main.rs new file mode 100644 index 00000000..a20fa0fd --- /dev/null +++ b/ci/src/main.rs @@ -0,0 +1,329 @@ +#!/usr/bin/env cargo +//! Symposium CI Tool +//! +//! Builds and tests all Symposium components for continuous integration + +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Parser)] +#[command( + name = "ci", + about = "Symposium CI tool for building and testing all components", + long_about = r#" +Symposium CI tool for building and testing all components + +Examples: + cargo ci # Check compilation (default) + cargo ci check # Check that all components compile + cargo ci test # Run all tests + +Components: + - Rust MCP server (cargo check) + - TypeScript VSCode extension (npm ci + webpack) + - Swift macOS app (swift build, macOS only) +"# +)] +struct Args { + #[command(subcommand)] + command: Option, +} + +#[derive(Parser)] +enum Commands { + /// Check that all components compile + Check, + /// Run all tests + Test, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + match args.command { + Some(Commands::Check) => run_check(), + Some(Commands::Test) => run_test(), + None => run_check(), // Default to check + } +} + +/// Check that all components compile +fn run_check() -> Result<()> { + println!("šŸ¤– Symposium CI Check"); + println!("{}", "=".repeat(26)); + + // Check basic prerequisites + check_rust()?; + check_node()?; + + // Check all components compile + check_rust_server()?; + build_extension()?; + + // Only build macOS app on macOS + if cfg!(target_os = "macos") { + build_macos_app()?; + } else { + println!("ā­ļø Skipping macOS app build (not on macOS)"); + } + + println!("\nāœ… All components check passed!"); + Ok(()) +} + +/// Run all tests +fn run_test() -> Result<()> { + println!("šŸ¤– Symposium CI Test"); + println!("{}", "=".repeat(25)); + + // Check basic prerequisites + check_rust()?; + check_node()?; + + // Run tests for all components + run_rust_tests()?; + run_typescript_tests()?; + + // Run Swift tests if they exist (macOS only) + if cfg!(target_os = "macos") { + run_swift_tests()?; + } else { + println!("ā­ļø Skipping Swift tests (not on macOS)"); + } + + println!("\nāœ… All tests completed!"); + Ok(()) +} + +fn check_rust() -> Result<()> { + if which::which("cargo").is_err() { + return Err(anyhow!( + "āŒ Error: Cargo not found. Please install Rust first.\n Visit: https://rustup.rs/" + )); + } + Ok(()) +} + +fn check_node() -> Result<()> { + if which::which("npm").is_err() { + return Err(anyhow!( + "āŒ Error: npm not found. Please install Node.js first.\n Visit: https://nodejs.org/" + )); + } + Ok(()) +} + +fn get_repo_root() -> Result { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").context( + "āŒ CI tool must be run via cargo. CARGO_MANIFEST_DIR not found.", + )?; + + let manifest_path = PathBuf::from(manifest_dir); + // If we're in the ci/ directory, go up to workspace root + if manifest_path.file_name() == Some(std::ffi::OsStr::new("ci")) { + if let Some(parent) = manifest_path.parent() { + return Ok(parent.to_path_buf()); + } + } + Ok(manifest_path) +} + +/// Check Rust MCP server compilation +fn check_rust_server() -> Result<()> { + let repo_root = get_repo_root()?; + let server_dir = repo_root.join("symposium/mcp-server"); + + println!("šŸ¦€ Checking Rust MCP server..."); + println!(" Checking in: {}", server_dir.display()); + + let output = Command::new("cargo") + .args(["check", "--release"]) + .current_dir(&server_dir) + .output() + .context("Failed to execute cargo check")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Failed to check Rust server:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… Rust server check passed!"); + Ok(()) +} + +/// Build VSCode extension +fn build_extension() -> Result<()> { + let repo_root = get_repo_root()?; + let extension_dir = repo_root.join("symposium/vscode-extension"); + + println!("\nšŸ“¦ Building VSCode extension..."); + + // Install dependencies + println!("šŸ“„ Installing extension dependencies..."); + let output = Command::new("npm") + .args(["ci"]) + .current_dir(&extension_dir) + .output() + .context("Failed to execute npm ci")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Failed to install extension dependencies:\n Error: {}", + stderr.trim() + )); + } + + // Build extension for production + println!("šŸ”Ø Building extension..."); + let output = Command::new("npm") + .args(["run", "webpack"]) + .current_dir(&extension_dir) + .output() + .context("Failed to execute npm run webpack")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Failed to build extension:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… VSCode extension built successfully!"); + Ok(()) +} + +/// Build macOS app +fn build_macos_app() -> Result<()> { + let repo_root = get_repo_root()?; + let app_dir = repo_root.join("symposium").join("macos-app"); + + println!("\nšŸŽ Building macOS application..."); + println!(" Building in: {}", app_dir.display()); + + let output = Command::new("swift") + .args(["build", "--configuration", "release"]) + .current_dir(&app_dir) + .output() + .context("Failed to execute swift build")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(anyhow!( + "āŒ Failed to build macOS app:\n stdout: {}\n stderr: {}", + stdout.trim(), + stderr.trim() + )); + } + + println!("āœ… macOS application built successfully!"); + Ok(()) +} + +/// Run Rust tests +fn run_rust_tests() -> Result<()> { + let repo_root = get_repo_root()?; + + println!("šŸ¦€ Running Rust tests..."); + println!(" Testing workspace in: {}", repo_root.display()); + + let output = Command::new("cargo") + .args(["test", "--workspace"]) + .current_dir(&repo_root) + .output() + .context("Failed to execute cargo test")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ Rust tests failed:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… Rust tests passed!"); + Ok(()) +} + +/// Run TypeScript tests if they exist +fn run_typescript_tests() -> Result<()> { + let repo_root = get_repo_root()?; + let extension_dir = repo_root.join("symposium/vscode-extension"); + + println!("\nšŸ“¦ Checking for TypeScript tests..."); + + // Check if package.json has a test script + let package_json_path = extension_dir.join("package.json"); + if !package_json_path.exists() { + println!("ā­ļø No package.json found, skipping TypeScript tests"); + return Ok(()); + } + + let package_json = std::fs::read_to_string(&package_json_path) + .context("Failed to read package.json")?; + + if !package_json.contains("\"test\"") { + println!("ā­ļø No test script found in package.json, skipping TypeScript tests"); + return Ok(()); + } + + println!("šŸ”Ø Running TypeScript tests..."); + let output = Command::new("npm") + .args(["test"]) + .current_dir(&extension_dir) + .output() + .context("Failed to execute npm test")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow!( + "āŒ TypeScript tests failed:\n Error: {}", + stderr.trim() + )); + } + + println!("āœ… TypeScript tests passed!"); + Ok(()) +} + +/// Run Swift tests if they exist +fn run_swift_tests() -> Result<()> { + let repo_root = get_repo_root()?; + let app_dir = repo_root.join("symposium").join("macos-app"); + + println!("\nšŸŽ Checking for Swift tests..."); + + // Check if Tests directory exists + let tests_dir = app_dir.join("Tests"); + if !tests_dir.exists() { + println!("ā­ļø No Tests directory found, skipping Swift tests"); + return Ok(()); + } + + println!("šŸ”Ø Running Swift tests..."); + let output = Command::new("swift") + .args(["test"]) + .current_dir(&app_dir) + .output() + .context("Failed to execute swift test")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + return Err(anyhow!( + "āŒ Swift tests failed:\n stdout: {}\n stderr: {}", + stdout.trim(), + stderr.trim() + )); + } + + println!("āœ… Swift tests passed!"); + Ok(()) +} diff --git a/setup/src/main.rs b/setup/src/main.rs index 3ac8fd5a..f867cfe1 100644 --- a/setup/src/main.rs +++ b/setup/src/main.rs @@ -18,9 +18,6 @@ Build Symposium components and set up for development with AI assistants Examples: cargo setup # Show help and usage - cargo setup ci # CI mode: check all components compile (default) - cargo setup ci check # CI mode: check all components compile - cargo setup ci test # CI mode: run all tests cargo setup --all # Build everything and setup for development cargo setup --vscode # Build/install VSCode extension only cargo setup --mcp # Build/install MCP server only @@ -29,6 +26,8 @@ Examples: cargo setup --app --open # Build macOS app and launch it cargo setup --vscode --mcp --app # Build all components (same as --all) +For CI builds, use: cargo ci check / cargo ci test + Prerequisites: - Rust and Cargo (https://rustup.rs/) - Node.js and npm (for VSCode extension) @@ -37,9 +36,6 @@ Prerequisites: "# )] struct Args { - #[command(subcommand)] - command: Option, - /// Build all components (VSCode extension, MCP server, and macOS app) #[arg(long)] all: bool, @@ -65,36 +61,10 @@ struct Args { restart: bool, } -#[derive(Parser)] -enum Commands { - /// CI mode: build all components for continuous integration - Ci { - #[command(subcommand)] - command: Option, - }, -} - -#[derive(Parser)] -enum CiCommands { - /// Check that all components compile - Check, - /// Run all tests - Test, -} - fn main() -> Result<()> { let args = Args::parse(); - // Handle CI subcommand first - if let Some(Commands::Ci { command }) = args.command { - return match command { - Some(CiCommands::Check) => run_ci_check(), - Some(CiCommands::Test) => run_ci_test(), - None => run_ci_check(), // Default to check if no subcommand - }; - } - - // Validate flag combinations for regular mode + // Validate flag combinations if args.open && !args.app && !args.all { return Err(anyhow!("āŒ --open requires --app")); } @@ -173,15 +143,9 @@ fn show_help() { println!(" --restart Restart MCP daemon after building (requires --mcp)"); println!(" --help Show this help message"); println!(); - println!("Subcommands:"); - println!(" ci CI mode: check compilation and run tests"); - println!(" ci check Check that all components compile"); - println!(" ci test Run all tests"); + println!("For CI builds, use: cargo ci check / cargo ci test"); println!(); println!("Examples:"); - println!(" cargo setup ci # CI check (default)"); - println!(" cargo setup ci check # Check compilation"); - println!(" cargo setup ci test # Run all tests"); println!(" cargo setup --all # Build everything"); println!(" cargo setup --vscode # Build VSCode extension only"); println!(" cargo setup --mcp --restart # Build MCP server and restart daemon"); @@ -189,56 +153,6 @@ fn show_help() { println!(" cargo setup --vscode --mcp --app # Build all components"); } -/// CI check mode: verify all components compile -fn run_ci_check() -> Result<()> { - println!("šŸ¤– Symposium CI Check"); - println!("{}", "=".repeat(26)); - - // Check basic prerequisites for CI - check_rust()?; - check_node_ci()?; - - // Check all components compile - build_rust_server_ci()?; - build_extension_ci()?; - - // Only build macOS app on macOS - if cfg!(target_os = "macos") { - build_macos_app_ci()?; - } else { - println!("ā­ļø Skipping macOS app build (not on macOS)"); - } - - println!("\nāœ… All components check passed!"); - Ok(()) -} - -/// CI test mode: run all tests -fn run_ci_test() -> Result<()> { - println!("šŸ¤– Symposium CI Test"); - println!("{}", "=".repeat(25)); - - // Check basic prerequisites for CI - check_rust()?; - check_node_ci()?; - - // Run Rust tests - run_rust_tests()?; - - // Run TypeScript tests if they exist - run_typescript_tests()?; - - // Run Swift tests if they exist (macOS only) - if cfg!(target_os = "macos") { - run_swift_tests()?; - } else { - println!("ā­ļø Skipping Swift tests (not on macOS)"); - } - - println!("\nāœ… All tests completed!"); - Ok(()) -} - fn check_rust() -> Result<()> { if which::which("cargo").is_err() { return Err(anyhow!( @@ -248,213 +162,6 @@ fn check_rust() -> Result<()> { Ok(()) } -fn check_node_ci() -> Result<()> { - if which::which("npm").is_err() { - return Err(anyhow!( - "āŒ Error: npm not found. Please install Node.js first.\n Visit: https://nodejs.org/" - )); - } - Ok(()) -} - -/// Build Rust MCP server in CI mode (compilation check only) -fn build_rust_server_ci() -> Result<()> { - let repo_root = get_repo_root()?; - let server_dir = repo_root.join("symposium/mcp-server"); - - println!("šŸ¦€ Checking Rust MCP server..."); - println!(" Checking in: {}", server_dir.display()); - - let output = Command::new("cargo") - .args(["check", "--release"]) - .current_dir(&server_dir) - .output() - .context("Failed to execute cargo check")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!( - "āŒ Failed to check Rust server:\n Error: {}", - stderr.trim() - )); - } - - println!("āœ… Rust server check passed!"); - Ok(()) -} - -/// Build VSCode extension in CI mode (no installation) -fn build_extension_ci() -> Result<()> { - let repo_root = get_repo_root()?; - let extension_dir = repo_root.join("symposium/vscode-extension"); - - println!("\nšŸ“¦ Building VSCode extension..."); - - // Install dependencies - println!("šŸ“„ Installing extension dependencies..."); - let output = Command::new("npm") - .args(["ci"]) // Use npm ci for faster, reproducible builds in CI - .current_dir(&extension_dir) - .output() - .context("Failed to execute npm ci")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!( - "āŒ Failed to install extension dependencies:\n Error: {}", - stderr.trim() - )); - } - - // Build extension for production - println!("šŸ”Ø Building extension..."); - let output = Command::new("npm") - .args(["run", "webpack"]) - .current_dir(&extension_dir) - .output() - .context("Failed to execute npm run webpack")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!( - "āŒ Failed to build extension:\n Error: {}", - stderr.trim() - )); - } - - println!("āœ… VSCode extension built successfully!"); - Ok(()) -} - -/// Build macOS app in CI mode -fn build_macos_app_ci() -> Result<()> { - let repo_root = get_repo_root()?; - let app_dir = repo_root.join("symposium").join("macos-app"); - - println!("\nšŸŽ Building macOS application..."); - println!(" Building in: {}", app_dir.display()); - - let output = Command::new("swift") - .args(["build", "--configuration", "release"]) - .current_dir(&app_dir) - .output() - .context("Failed to execute swift build")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - return Err(anyhow!( - "āŒ Failed to build macOS app:\n stdout: {}\n stderr: {}", - stdout.trim(), - stderr.trim() - )); - } - - println!("āœ… macOS application built successfully!"); - Ok(()) -} - -/// Run Rust tests -fn run_rust_tests() -> Result<()> { - let repo_root = get_repo_root()?; - - println!("šŸ¦€ Running Rust tests..."); - println!(" Testing workspace in: {}", repo_root.display()); - - let output = Command::new("cargo") - .args(["test", "--workspace"]) - .current_dir(&repo_root) - .output() - .context("Failed to execute cargo test")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!( - "āŒ Rust tests failed:\n Error: {}", - stderr.trim() - )); - } - - println!("āœ… Rust tests passed!"); - Ok(()) -} - -/// Run TypeScript tests if they exist -fn run_typescript_tests() -> Result<()> { - let repo_root = get_repo_root()?; - let extension_dir = repo_root.join("symposium/vscode-extension"); - - println!("\nšŸ“¦ Checking for TypeScript tests..."); - - // Check if package.json has a test script - let package_json_path = extension_dir.join("package.json"); - if !package_json_path.exists() { - println!("ā­ļø No package.json found, skipping TypeScript tests"); - return Ok(()); - } - - let package_json = std::fs::read_to_string(&package_json_path) - .context("Failed to read package.json")?; - - if !package_json.contains("\"test\"") { - println!("ā­ļø No test script found in package.json, skipping TypeScript tests"); - return Ok(()); - } - - println!("šŸ”Ø Running TypeScript tests..."); - let output = Command::new("npm") - .args(["test"]) - .current_dir(&extension_dir) - .output() - .context("Failed to execute npm test")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(anyhow!( - "āŒ TypeScript tests failed:\n Error: {}", - stderr.trim() - )); - } - - println!("āœ… TypeScript tests passed!"); - Ok(()) -} - -/// Run Swift tests if they exist -fn run_swift_tests() -> Result<()> { - let repo_root = get_repo_root()?; - let app_dir = repo_root.join("symposium").join("macos-app"); - - println!("\nšŸŽ Checking for Swift tests..."); - - // Check if Tests directory exists - let tests_dir = app_dir.join("Tests"); - if !tests_dir.exists() { - println!("ā­ļø No Tests directory found, skipping Swift tests"); - return Ok(()); - } - - println!("šŸ”Ø Running Swift tests..."); - let output = Command::new("swift") - .args(["test"]) - .current_dir(&app_dir) - .output() - .context("Failed to execute swift test")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - return Err(anyhow!( - "āŒ Swift tests failed:\n stdout: {}\n stderr: {}", - stdout.trim(), - stderr.trim() - )); - } - - println!("āœ… Swift tests passed!"); - Ok(()) -} - fn check_node() -> Result<()> { if which::which("npm").is_err() { return Err(anyhow!( From 735645d0e721c9af2d619a44729200ff5c456405 Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 25 Sep 2025 08:39:10 -0400 Subject: [PATCH 6/7] chore: upgrade remaining crates to Rust 2024 edition - Update setup, ci, and test-utils crates to use edition = "2024" - All crates now consistently use Rust 2024 edition - Verified all components still build correctly Co-authored-by: Claude --- ci/Cargo.toml | 2 +- setup/Cargo.toml | 2 +- symposium/mcp-server/test-utils/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/Cargo.toml b/ci/Cargo.toml index 54814d8e..8ddb7d3c 100644 --- a/ci/Cargo.toml +++ b/ci/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ci" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["Niko Matsakis"] description = "Symposium CI tool for building and testing all components" diff --git a/setup/Cargo.toml b/setup/Cargo.toml index a4e8738f..9f769a74 100644 --- a/setup/Cargo.toml +++ b/setup/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "setup" version = "0.1.0" -edition = "2021" +edition = "2024" authors = ["Niko Matsakis"] description = "Dialectic development setup tool" diff --git a/symposium/mcp-server/test-utils/Cargo.toml b/symposium/mcp-server/test-utils/Cargo.toml index 9cb5e381..772feb08 100644 --- a/symposium/mcp-server/test-utils/Cargo.toml +++ b/symposium/mcp-server/test-utils/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "test-utils" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] git2 = "0.18" From dfc466897f28357c431467c9b7a7df3a0866714d Mon Sep 17 00:00:00 2001 From: Niko Matsakis Date: Thu, 25 Sep 2025 08:44:35 -0400 Subject: [PATCH 7/7] ci: run check and test jobs in parallel - Split single build job into separate check and test jobs - Both jobs run in parallel across ubuntu-latest and macos-latest - Improves CI speed by running compilation checks and tests concurrently - Each job maintains same caching strategy for optimal performance Co-authored-by: Claude --- .github/workflows/ci.yml | 45 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75097b88..fa298fc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,8 @@ env: CARGO_TERM_COLOR: always jobs: - build: + check: + name: Check compilation strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -53,6 +54,48 @@ jobs: - name: Run CI check run: cargo ci check + + test: + name: Run tests + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + symposium/mcp-server/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: symposium/vscode-extension/package-lock.json + + - name: Cache Node.js dependencies + uses: actions/cache@v4 + with: + path: symposium/vscode-extension/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('symposium/vscode-extension/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Run CI tests run: cargo ci test