diff --git a/crates/tower-cmd/src/deploy.rs b/crates/tower-cmd/src/deploy.rs index 15baadf7..433cfe30 100644 --- a/crates/tower-cmd/src/deploy.rs +++ b/crates/tower-cmd/src/deploy.rs @@ -24,6 +24,20 @@ pub fn deploy_cmd() -> Command { .help("Automatically force creation of the app if it doesn't already exist") .action(clap::ArgAction::SetTrue), ) + .arg( + Arg::new("environment") + .long("environment") + .short('e') + .help("The environment to deploy to") + .conflicts_with("all"), + ) + .arg( + Arg::new("all") + .long("all") + .help("Deploy to all environments") + .action(clap::ArgAction::SetTrue) + .conflicts_with("environment"), + ) .about("Deploy your latest code to Tower") } @@ -35,10 +49,30 @@ fn resolve_path(args: &ArgMatches) -> PathBuf { } } +/// Resolves the target environment from CLI args. +/// +/// - `--all` → `DeployTarget::All` +/// - `--environment ` → `DeployTarget::Environment(name)` +/// - neither → `DeployTarget::Default` +#[derive(Debug, Clone)] +pub enum DeployTarget { + Environment(String), + All, +} + pub async fn do_deploy(config: Config, args: &ArgMatches) { let dir = resolve_path(args); let create_app = args.get_flag("create"); - if let Err(err) = deploy_from_dir(config, dir, create_app).await { + + let target = if args.get_flag("all") { + DeployTarget::All + } else if let Some(env) = args.get_one::("environment") { + DeployTarget::Environment(env.clone()) + } else { + DeployTarget::Environment("default".to_string()) + }; + + if let Err(err) = deploy_from_dir(config, dir, create_app, target).await { match err { crate::Error::ApiDeployError { source } => { output::tower_error_and_die(source, "Deploying app failed") @@ -66,6 +100,7 @@ pub async fn deploy_from_dir( config: Config, dir: PathBuf, create_app: bool, + target: DeployTarget, ) -> Result<(), crate::Error> { debug!("Building package from directory: {:?}", dir); @@ -96,23 +131,145 @@ pub async fn deploy_from_dir( }; spinner.success(); - do_deploy_package(api_config, package, &towerfile).await + do_deploy_package(api_config, package, &towerfile, target).await } async fn do_deploy_package( api_config: Configuration, package: Package, towerfile: &Towerfile, + target: DeployTarget, ) -> Result<(), crate::Error> { - let res = util::deploy::deploy_app_package(&api_config, &towerfile.app.name, package).await; + let (environment, all_environments) = match &target { + DeployTarget::All => (None, true), + DeployTarget::Environment(env) => (Some(env.as_str()), false), + }; + + let res = util::deploy::deploy_app_package( + &api_config, + &towerfile.app.name, + package, + environment, + all_environments, + ) + .await; match res { Ok(resp) => { let version = resp.version; - let line = format!("Version `{}` has been deployed to Tower!", version.version); + let line = match &target { + DeployTarget::All => format!( + "Version `{}` has been deployed to all environments!", + version.version + ), + DeployTarget::Environment(env) => format!( + "Version `{}` has been deployed to environment '{}'!", + version.version, env + ), + }; output::success(&line); Ok(()) } Err(err) => Err(crate::Error::ApiDeployError { source: err }), } } + +#[cfg(test)] +mod tests { + use super::deploy_cmd; + + fn parse(args: &[&str]) -> Result { + let mut full = vec!["deploy"]; + full.extend_from_slice(args); + deploy_cmd().try_get_matches_from(full) + } + + #[test] + fn no_args_uses_defaults() { + let m = parse(&[]).unwrap(); + assert_eq!(m.get_one::("environment"), None); + assert!(!m.get_flag("all")); + } + + #[test] + fn environment_flag_long() { + let m = parse(&["--environment", "production"]).unwrap(); + assert_eq!( + m.get_one::("environment").map(|s| s.as_str()), + Some("production") + ); + } + + #[test] + fn environment_flag_short() { + let m = parse(&["-e", "staging"]).unwrap(); + assert_eq!( + m.get_one::("environment").map(|s| s.as_str()), + Some("staging") + ); + } + + #[test] + fn environment_flag_equals_syntax() { + let m = parse(&["--environment=production"]).unwrap(); + assert_eq!( + m.get_one::("environment").map(|s| s.as_str()), + Some("production") + ); + } + + #[test] + fn all_flag() { + let m = parse(&["--all"]).unwrap(); + assert!(m.get_flag("all")); + assert_eq!(m.get_one::("environment"), None); + } + + #[test] + fn environment_and_all_conflict() { + let err = parse(&["--environment", "production", "--all"]).unwrap_err(); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + } + + #[test] + fn all_and_environment_conflict() { + let err = parse(&["--all", "--environment", "staging"]).unwrap_err(); + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + } + + #[test] + fn create_flag_with_environment() { + let m = parse(&["--create", "--environment", "production"]).unwrap(); + assert!(m.get_flag("create")); + assert_eq!( + m.get_one::("environment").map(|s| s.as_str()), + Some("production") + ); + } + + #[test] + fn create_flag_with_all() { + let m = parse(&["--create", "--all"]).unwrap(); + assert!(m.get_flag("create")); + assert!(m.get_flag("all")); + } + + #[test] + fn dir_with_environment() { + let m = parse(&["-d", "/tmp/myapp", "-e", "production"]).unwrap(); + assert_eq!( + m.get_one::("dir").map(|s| s.as_str()), + Some("/tmp/myapp") + ); + assert_eq!( + m.get_one::("environment").map(|s| s.as_str()), + Some("production") + ); + } + + #[test] + fn help_flag_shows_help() { + let err = parse(&["--help"]).unwrap_err(); + assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp); + } +} diff --git a/crates/tower-cmd/src/mcp.rs b/crates/tower-cmd/src/mcp.rs index 49dd7beb..478a0558 100644 --- a/crates/tower-cmd/src/mcp.rs +++ b/crates/tower-cmd/src/mcp.rs @@ -708,8 +708,9 @@ impl TowerService { Parameters(request): Parameters, ) -> Result { let working_dir = Self::resolve_working_directory(&request.common); + let deploy_target = deploy::DeployTarget::Environment("default".to_string()); - match deploy::deploy_from_dir(self.config.clone(), working_dir, true).await { + match deploy::deploy_from_dir(self.config.clone(), working_dir, true, deploy_target).await { Ok(_) => Self::text_success("Deploy completed successfully".to_string()), Err(e) => Self::error_result("Deploy failed", e), } diff --git a/crates/tower-cmd/src/util/deploy.rs b/crates/tower-cmd/src/util/deploy.rs index ad995887..62e01184 100644 --- a/crates/tower-cmd/src/util/deploy.rs +++ b/crates/tower-cmd/src/util/deploy.rs @@ -8,6 +8,7 @@ use tower_package::{compute_sha256_file, Package}; use tower_telemetry::debug; use tower_api::apis::configuration::Configuration; +use tower_api::apis::urlencode; use tower_api::apis::default_api::DeployAppError; use tower_api::apis::Error; use tower_api::apis::ResponseContent; @@ -96,6 +97,8 @@ pub async fn deploy_app_package( api_config: &tower_api::apis::configuration::Configuration, app_name: &str, package: Package, + environment: Option<&str>, + all_environments: bool, ) -> Result> { let progress_bar = Arc::new(Mutex::new(output::progress_bar("Deploying to Tower..."))); @@ -116,7 +119,14 @@ pub async fn deploy_app_package( // Create the URL for the API endpoint let base_url = &api_config.base_path; - let url = format!("{}/apps/{}/deploy", base_url, app_name); + let url = if all_environments { + format!("{}/apps/{}/deploy?all_environments=true", base_url, app_name) + } else if let Some(env) = environment { + let encoded_environment = urlencode(env); + format!("{}/apps/{}/deploy?environment={}", base_url, app_name, encoded_environment) + } else { + format!("{}/apps/{}/deploy", base_url, app_name) + }; // Upload the package let response = upload_file_with_progress(