diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 748cd6296..e1742cb3c 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -53,24 +53,6 @@ jobs: - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 - - name: Install libpq (macOS and Linux) - if: runner.os != 'Windows' - run: | - if [[ "${{ runner.os }}" == "macOS" ]]; then - brew install libpq - echo "PKG_CONFIG_PATH=$(brew --prefix libpq)/lib/pkgconfig" >> $GITHUB_ENV - echo "LIBRARY_PATH=$(brew --prefix libpq)/lib" >> $GITHUB_ENV - echo "LD_LIBRARY_PATH=$(brew --prefix libpq)/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - # For macOS, we might need to explicitly tell rustc where to find the library. - # Adding common libpq paths to rustflags - echo "RUSTFLAGS=-L $(brew --prefix libpq)/lib" >> $GITHUB_ENV - elif [[ "${{ runner.os }}" == "Linux" ]]; then - sudo apt-get update -y - sudo apt-get install -y libpq-dev - fi - env: - HOMEBREW_NO_INSTALL_CLEANUP: 1 # Recommended for CI to speed up - - name: Configure Cargo for optimized build run: | mkdir -p .cargo @@ -115,6 +97,9 @@ jobs: release: needs: build runs-on: ubuntu-latest + outputs: # Outputs for downstream jobs + cli_version: ${{ steps.get_version.outputs.version }} + cli_tag_name: ${{ steps.create_release.outputs.tag_name }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -131,6 +116,7 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION" - name: Create Release + id: create_release uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.get_version.outputs.version }} diff --git a/cli/cli/src/commands/run.rs b/cli/cli/src/commands/run.rs index 6a71d00d0..0f1a80680 100644 --- a/cli/cli/src/commands/run.rs +++ b/cli/cli/src/commands/run.rs @@ -1,12 +1,12 @@ +use crate::error::BusterError; +use dirs; +use indicatif::{ProgressBar, ProgressStyle}; +use rust_embed::RustEmbed; use std::fs; use std::io::{self, Write}; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use crate::error::BusterError; -use indicatif::{ProgressBar, ProgressStyle}; +use std::path::PathBuf; +use std::process::Command; use std::time::Duration; -use rust_embed::RustEmbed; -use dirs; #[derive(RustEmbed)] #[folder = "../../"] @@ -21,8 +21,11 @@ use dirs; struct StaticAssets; async fn setup_persistent_app_environment() -> Result { - let home_dir = dirs::home_dir() - .ok_or_else(|| BusterError::CommandError("Failed to get home directory. Cannot set up persistent app path.".to_string()))?; + let home_dir = dirs::home_dir().ok_or_else(|| { + BusterError::CommandError( + "Failed to get home directory. Cannot set up persistent app path.".to_string(), + ) + })?; let app_base_dir = home_dir.join(".buster"); fs::create_dir_all(&app_base_dir).map_err(|e| { @@ -35,8 +38,9 @@ async fn setup_persistent_app_environment() -> Result { for filename_cow in StaticAssets::iter() { let filename = filename_cow.as_ref(); - let asset = StaticAssets::get(filename) - .ok_or_else(|| BusterError::CommandError(format!("Failed to get embedded asset: {}", filename)))?; + let asset = StaticAssets::get(filename).ok_or_else(|| { + BusterError::CommandError(format!("Failed to get embedded asset: {}", filename)) + })?; let target_file_path = app_base_dir.join(filename); if let Some(parent) = target_file_path.parent() { @@ -60,15 +64,24 @@ async fn setup_persistent_app_environment() -> Result { } let supabase_volumes_functions_path = app_base_dir.join("supabase/volumes/functions"); - fs::create_dir_all(supabase_volumes_functions_path).map_err(|e| BusterError::CommandError(format!("Failed to create supabase/volumes/functions in persistent app dir: {}", e)))?; - + fs::create_dir_all(supabase_volumes_functions_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to create supabase/volumes/functions in persistent app dir: {}", + e + )) + })?; + let target_dotenv_path = app_base_dir.join(".env"); // Always use .env.example from embedded assets let example_env_filename = "supabase/.env.example"; - let asset = StaticAssets::get(example_env_filename) - .ok_or_else(|| BusterError::CommandError(format!("Failed to get embedded asset: {}", example_env_filename)))?; - + let asset = StaticAssets::get(example_env_filename).ok_or_else(|| { + BusterError::CommandError(format!( + "Failed to get embedded asset: {}", + example_env_filename + )) + })?; + fs::write(&target_dotenv_path, asset.data).map_err(|e| { BusterError::CommandError(format!( "Failed to write {} to {}: {}", @@ -92,16 +105,29 @@ async fn setup_persistent_app_environment() -> Result { Ok(app_base_dir) } -async fn run_docker_compose_command(args: &[&str], operation_name: &str) -> Result<(), BusterError> { +async fn run_docker_compose_command( + args: &[&str], + operation_name: &str, +) -> Result<(), BusterError> { let persistent_app_dir = setup_persistent_app_environment().await?; let data_db_path = persistent_app_dir.join("supabase/volumes/db/data"); - fs::create_dir_all(&data_db_path) - .map_err(|e| BusterError::CommandError(format!("Failed to create persistent data directory at {}: {}", data_db_path.display(), e)))?; + fs::create_dir_all(&data_db_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to create persistent data directory at {}: {}", + data_db_path.display(), + e + )) + })?; let data_storage_path = persistent_app_dir.join("supabase/volumes/storage"); - fs::create_dir_all(&data_storage_path) - .map_err(|e| BusterError::CommandError(format!("Failed to create persistent data directory at {}: {}", data_storage_path.display(), e)))?; + fs::create_dir_all(&data_storage_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to create persistent data directory at {}: {}", + data_storage_path.display(), + e + )) + })?; let pb = ProgressBar::new_spinner(); pb.enable_steady_tick(Duration::from_millis(120)); @@ -112,7 +138,10 @@ async fn run_docker_compose_command(args: &[&str], operation_name: &str) -> Resu .expect("Failed to set progress bar style"), ); if operation_name == "Starting" { - pb.set_message(format!("{} Buster services... (this may take a few minutes)", operation_name)); + pb.set_message(format!( + "{} Buster services... (this may take a few minutes)", + operation_name + )); } else { pb.set_message(format!("{} Buster services...", operation_name)); } @@ -127,7 +156,11 @@ async fn run_docker_compose_command(args: &[&str], operation_name: &str) -> Resu .args(args); let output = cmd.output().map_err(|e| { - BusterError::CommandError(format!("Failed to execute docker compose {}: {}", args.join(" "), e)) + BusterError::CommandError(format!( + "Failed to execute docker compose {}: {}", + args.join(" "), + e + )) })?; if output.status.success() { @@ -145,7 +178,10 @@ async fn run_docker_compose_command(args: &[&str], operation_name: &str) -> Resu String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); - pb.abandon_with_message(format!("Error: docker compose {} failed. See console for details.", args.join(" "))); + pb.abandon_with_message(format!( + "Error: docker compose {} failed. See console for details.", + args.join(" ") + )); println!("\nDocker Compose Error Details:\n{}", err_msg); Err(BusterError::CommandError(err_msg)) } @@ -161,15 +197,21 @@ pub async fn stop() -> Result<(), BusterError> { pub async fn reset() -> Result<(), BusterError> { println!("WARNING: This command will stop all Buster services, attempt to remove their current images, and then restart them from scratch."); - println!("This can lead to a complete wipe of the Buster database and any other local service data."); + println!( + "This can lead to a complete wipe of the Buster database and any other local service data." + ); println!("This action is irreversible."); - print!("Are you sure you want to proceed with resetting? (yes/No): "); - io::stdout().flush().map_err(|e| BusterError::CommandError(format!("Failed to flush stdout: {}", e)))?; + print!("Are you sure you want to proceed with resetting? (y/n): "); + io::stdout() + .flush() + .map_err(|e| BusterError::CommandError(format!("Failed to flush stdout: {}", e)))?; let mut confirmation = String::new(); - io::stdin().read_line(&mut confirmation).map_err(|e| BusterError::CommandError(format!("Failed to read user input: {}", e)))?; + io::stdin() + .read_line(&mut confirmation) + .map_err(|e| BusterError::CommandError(format!("Failed to read user input: {}", e)))?; - if confirmation.trim().to_lowercase() != "yes" { + if confirmation.trim().to_lowercase() != "y" { println!("Reset cancelled by user."); return Ok(()); } @@ -185,10 +227,12 @@ pub async fn reset() -> Result<(), BusterError> { .expect("Failed to set progress bar style"), ); + // Step 1: Stop services pb.set_message("Resetting Buster services (step 1/4): Stopping services..."); let mut down_cmd = Command::new("docker"); - down_cmd.current_dir(&persistent_app_dir) + down_cmd + .current_dir(&persistent_app_dir) .arg("compose") .arg("-p") .arg("buster") @@ -196,7 +240,9 @@ pub async fn reset() -> Result<(), BusterError> { .arg("docker-compose.yml") .arg("down"); - let down_output = down_cmd.output().map_err(|e| BusterError::CommandError(format!("Failed to execute docker compose down: {}", e)))?; + let down_output = down_cmd.output().map_err(|e| { + BusterError::CommandError(format!("Failed to execute docker compose down: {}", e)) + })?; if !down_output.status.success() { let err_msg = format!( "docker compose down failed (status: {}). Logs: @@ -215,9 +261,52 @@ Stderr: return Err(BusterError::CommandError(err_msg)); } - pb.set_message("Resetting Buster services (step 2/4): Identifying service images..."); + // Step 2: Clear persistent data volumes + pb.set_message("Resetting Buster services (step 2/4): Clearing persistent data volumes..."); + let db_volume_path = persistent_app_dir.join("supabase/volumes/db/data"); + let storage_volume_path = persistent_app_dir.join("supabase/volumes/storage"); + + if db_volume_path.exists() { + fs::remove_dir_all(&db_volume_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to remove db volume at {}: {}", + db_volume_path.display(), + e + )) + })?; + } + fs::create_dir_all(&db_volume_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to recreate db volume at {}: {}", + db_volume_path.display(), + e + )) + })?; + pb.println(format!("Successfully cleared and recreated database volume: {}", db_volume_path.display())); + + if storage_volume_path.exists() { + fs::remove_dir_all(&storage_volume_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to remove storage volume at {}: {}", + storage_volume_path.display(), + e + )) + })?; + } + fs::create_dir_all(&storage_volume_path).map_err(|e| { + BusterError::CommandError(format!( + "Failed to recreate storage volume at {}: {}", + storage_volume_path.display(), + e + )) + })?; + pb.println(format!("Successfully cleared and recreated storage volume: {}", storage_volume_path.display())); + + // Step 3: Identify service images + pb.set_message("Resetting Buster services (step 3/4): Identifying service images..."); let mut config_images_cmd = Command::new("docker"); - config_images_cmd.current_dir(&persistent_app_dir) + config_images_cmd + .current_dir(&persistent_app_dir) .arg("compose") .arg("-p") .arg("buster") @@ -226,7 +315,12 @@ Stderr: .arg("config") .arg("--images"); - let config_images_output = config_images_cmd.output().map_err(|e| BusterError::CommandError(format!("Failed to execute docker compose config --images: {}", e)))?; + let config_images_output = config_images_cmd.output().map_err(|e| { + BusterError::CommandError(format!( + "Failed to execute docker compose config --images: {}", + e + )) + })?; if !config_images_output.status.success() { let err_msg = format!( "docker compose config --images failed (status: {}). Logs: @@ -240,25 +334,39 @@ Stderr: String::from_utf8_lossy(&config_images_output.stdout), String::from_utf8_lossy(&config_images_output.stderr) ); - pb.abandon_with_message("Error: Failed to identify service images. See console for details."); - println!("\nDocker Compose Config --images Error Details:\n{}", err_msg); + pb.abandon_with_message( + "Error: Failed to identify service images. See console for details.", + ); + println!( + "\nDocker Compose Config --images Error Details:\n{}", + err_msg + ); return Err(BusterError::CommandError(err_msg)); } let image_list_str = String::from_utf8_lossy(&config_images_output.stdout); - let image_names: Vec<&str> = image_list_str.lines().filter(|line| !line.trim().is_empty()).collect(); + let image_names: Vec<&str> = image_list_str + .lines() + .filter(|line| !line.trim().is_empty()) + .collect(); + // Step 4: Remove service images if image_names.is_empty() { - pb.println("No images identified by docker-compose config --images. Skipping image removal."); + pb.println( + "No images identified by docker-compose config --images. Skipping image removal.", + ); } else { - pb.set_message(format!("Resetting Buster services (step 3/4): Removing {} service image(s)...", image_names.len())); + pb.set_message(format!( + "Resetting Buster services (step 4/4): Removing {} service image(s)...", + image_names.len() + )); for (index, image_name) in image_names.iter().enumerate() { let current_image_name = image_name.trim(); if current_image_name.is_empty() { continue; } pb.set_message(format!( - "Resetting Buster services (step 3/4): Removing image {}/{} ('{}')...", + "Resetting Buster services (step 4/4): Removing image {}/{} ('{}')...", index + 1, image_names.len(), current_image_name @@ -266,48 +374,24 @@ Stderr: let mut rmi_cmd = Command::new("docker"); rmi_cmd.arg("image").arg("rm").arg(current_image_name); - let rmi_output = rmi_cmd.output().map_err(|e| BusterError::CommandError(format!("Failed to execute docker image rm {}: {}", current_image_name, e)))?; - + let rmi_output = rmi_cmd.output().map_err(|e| { + BusterError::CommandError(format!( + "Failed to execute docker image rm {}: {}", + current_image_name, e + )) + })?; + // Log warning on failure but continue, as image might not exist or be in use by other non-project containers if !rmi_output.status.success() { let rmi_stderr = String::from_utf8_lossy(&rmi_output.stderr); - if !rmi_stderr.trim().is_empty() && !rmi_stderr.contains("No such image") { // Don't warn if image was already gone - pb.println(format!("Warning: Could not remove image '{}'. It might be in use or already removed. Stderr: {}", current_image_name, rmi_stderr.trim())); + if !rmi_stderr.trim().is_empty() && !rmi_stderr.contains("No such image") { + // Don't warn if image was already gone + pb.println(format!("Warning: Could not remove image '{}'. It might be in use or already removed. Stderr: {}", current_image_name, rmi_stderr.trim())); } } } } - pb.set_message("Resetting Buster services (step 4/4): Starting services (pulling images if needed)..."); - let mut up_cmd = Command::new("docker"); - up_cmd.current_dir(&persistent_app_dir) - .arg("compose") - .arg("-p") - .arg("buster") - .arg("-f") - .arg("docker-compose.yml") - .arg("up") - .arg("-d") - .arg("--pull") - .arg("always") - .arg("--force-recreate") - .arg("--remove-orphans"); - - let up_output = up_cmd.output().map_err(|e| BusterError::CommandError(format!("Failed to execute docker compose up: {}", e)))?; - - if up_output.status.success() { - pb.finish_with_message("Buster services reset and started successfully."); - Ok(()) - } else { - let err_msg = format!( - "docker compose up failed after image purge (status: {}). Logs:\nWorking directory: {}\nStdout:\n{}\nStderr:\n{}", - up_output.status, - persistent_app_dir.display(), - String::from_utf8_lossy(&up_output.stdout), - String::from_utf8_lossy(&up_output.stderr) - ); - pb.abandon_with_message("Error: docker compose up failed after image purge. See console for details."); - println!("\nDocker Compose Up Error Details:\n{}", err_msg); - Err(BusterError::CommandError(err_msg)) - } -} \ No newline at end of file + pb.finish_with_message("Buster services stopped, volumes cleared, and images removed successfully."); + Ok(()) +}