lets test the cli release

This commit is contained in:
dal 2025-05-08 00:03:15 -06:00
parent 7519e066f9
commit 2d1ded6643
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
2 changed files with 164 additions and 94 deletions

View File

@ -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 }}

View File

@ -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<PathBuf, BusterError> {
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<PathBuf, BusterError> {
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<PathBuf, BusterError> {
}
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<PathBuf, BusterError> {
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))
}
}
pb.finish_with_message("Buster services stopped, volumes cleared, and images removed successfully.");
Ok(())
}