diff --git a/src/commands/deployment.rs b/src/commands/deployment.rs new file mode 100644 index 0000000..c5a799b --- /dev/null +++ b/src/commands/deployment.rs @@ -0,0 +1,199 @@ +use super::*; +use crate::client::post_graphql; +use crate::controllers::environment::get_matched_environment; +use crate::controllers::project::{ensure_project_and_environment_exist, get_project}; +use crate::gql::queries::deployments::{DeploymentStatus, ResponseData, Variables}; +use chrono::{DateTime, Local, Utc}; +use serde::Serialize; + +/// Manage deployments +#[derive(Parser)] +pub struct Args { + #[clap(subcommand)] + command: Commands, +} + +// Aliasing some of our root commands that should be "deployment" +// subcommands. This allows us to deprecate the root commands without +// breaking existing workflows. +#[derive(Parser)] +enum Commands { + /// List deployments for a service with IDs, statuses and other metadata + #[clap(alias = "ls")] + List(ListArgs), + + /// Upload and deploy project from the current directory + Up(crate::commands::up::Args), + + /// Redeploy the latest deployment of a service + Redeploy(crate::commands::redeploy::Args), +} + +#[derive(Parser)] +struct ListArgs { + /// Service name or ID to list deployments for (defaults to linked service) + #[clap(short, long)] + service: Option, + + /// Environment to list deployments from (defaults to linked environment) + #[clap(short, long)] + environment: Option, + + /// Maximum number of deployments to show (default: 20, max: 1000) + #[clap(long, default_value = "20")] + limit: i64, + + /// Output in JSON format + #[clap(long)] + json: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct DeploymentOutput { + id: String, + status: String, + created_at: DateTime, + meta: Option, +} + +pub async fn command(args: Args) -> Result<()> { + match args.command { + Commands::List(list_args) => { + list_deployments( + list_args.service, + list_args.environment, + list_args.limit, + list_args.json, + ) + .await + } + Commands::Up(deploy_args) => { + // Call the existing up command implementation + crate::commands::up::command(deploy_args).await + } + Commands::Redeploy(redeploy_args) => { + // Call the existing redeploy command implementation + crate::commands::redeploy::command(redeploy_args).await + } + } +} + +async fn list_deployments( + service: Option, + environment: Option, + limit: i64, + json: bool, +) -> Result<()> { + let configs = Configs::new()?; + let client = GQLClient::new_authorized(&configs)?; + let linked_project = configs.get_linked_project().await?; + + ensure_project_and_environment_exist(&client, &configs, &linked_project).await?; + + let limit = if limit > 1000 { + eprintln!("Warning: limit cannot exceed 1000, using 1000 instead"); + 1000 + } else if limit < 1 { + eprintln!("Warning: limit must be at least 1, using 1 instead"); + 1 + } else { + limit + }; + + let project = get_project(&client, &configs, linked_project.project.clone()).await?; + let environment = environment.unwrap_or(linked_project.environment.clone()); + let environment_id = get_matched_environment(&project, environment)?.id; + + let service_id = if let Some(service_name_or_id) = service { + let service = project + .services + .edges + .iter() + .find(|s| { + s.node.name.to_lowercase() == service_name_or_id.to_lowercase() + || s.node.id == service_name_or_id + }) + .ok_or_else(|| anyhow::anyhow!("Service '{}' not found", service_name_or_id))?; + service.node.id.clone() + } else if let Some(linked_service_id) = linked_project.service { + linked_service_id + } else { + bail!("No service specified and no service linked. Use 'railway link' to link a service or specify one with the service argument."); + }; + + let variables = Variables { + input: crate::gql::queries::deployments::DeploymentListInput { + service_id: Some(service_id.clone()), + environment_id: Some(environment_id), + project_id: None, + status: None, + include_deleted: None, + }, + first: Some(limit), + }; + + let response: ResponseData = post_graphql::( + &client, + configs.get_backboard(), + variables, + ) + .await?; + + let deployments = response + .deployments + .edges + .into_iter() + .map(|edge| edge.node) + .collect::>(); + + if json { + let output: Vec = deployments + .into_iter() + .map(|d| DeploymentOutput { + id: d.id, + status: format!("{:?}", d.status), + created_at: d.created_at, + meta: d.meta, + }) + .collect(); + println!("{}", serde_json::to_string_pretty(&output)?); + } else { + if deployments.is_empty() { + println!("No deployments found"); + return Ok(()); + } + + println!("{}", "Recent Deployments".bold()); + + for deployment in deployments { + let status_colored = match deployment.status { + DeploymentStatus::SUCCESS => format!("{:?}", deployment.status).green(), + DeploymentStatus::FAILED | DeploymentStatus::CRASHED => { + format!("{:?}", deployment.status).red() + } + DeploymentStatus::BUILDING + | DeploymentStatus::DEPLOYING + | DeploymentStatus::INITIALIZING + | DeploymentStatus::WAITING + | DeploymentStatus::QUEUED => format!("{:?}", deployment.status).blue(), + DeploymentStatus::REMOVED | DeploymentStatus::REMOVING => { + format!("{:?}", deployment.status).dimmed() + } + _ => format!("{:?}", deployment.status).white(), + }; + + // Convert UTC time to local timezone + let local_time: DateTime = DateTime::from(deployment.created_at); + let created_at = local_time.format("%Y-%m-%d %H:%M:%S %Z"); + println!( + " {} | {} | {}", + deployment.id, + status_colored, + created_at.to_string().dimmed() + ); + } + } + + Ok(()) +} diff --git a/src/commands/down.rs b/src/commands/down.rs index 0743f38..6c63804 100644 --- a/src/commands/down.rs +++ b/src/commands/down.rs @@ -67,6 +67,7 @@ pub async fn command(args: Args) -> Result<()> { include_deleted: None, status: None, }, + first: None, }; let linked_project_name = linked_project diff --git a/src/commands/logs.rs b/src/commands/logs.rs index 05ae4e1..a8eec68 100644 --- a/src/commands/logs.rs +++ b/src/commands/logs.rs @@ -107,6 +107,7 @@ pub async fn command(args: Args) -> Result<()> { include_deleted: None, status: None, }, + first: None, }; let deployments = post_graphql::(&client, configs.get_backboard(), vars) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index df053a5..6c8bebb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,5 +1,5 @@ pub(super) use crate::{client::*, config::*, gql::*}; -pub(super) use anyhow::{Context, Result}; +pub(super) use anyhow::{bail, Context, Result}; pub(super) use clap::{Parser, Subcommand}; pub(super) use colored::Colorize; @@ -12,6 +12,7 @@ pub mod add; pub mod completion; pub mod connect; pub mod deploy; +pub mod deployment; pub mod docs; pub mod domain; pub mod down; diff --git a/src/gql/queries/strings/Deployments.graphql b/src/gql/queries/strings/Deployments.graphql index 6e5885c..f5ea483 100644 --- a/src/gql/queries/strings/Deployments.graphql +++ b/src/gql/queries/strings/Deployments.graphql @@ -1,11 +1,12 @@ -query Deployments($input: DeploymentListInput!) { - deployments(input: $input) { - edges { - node { - id - createdAt - status - } +query Deployments($input: DeploymentListInput!, $first: Int) { + deployments(input: $input, first: $first) { + edges { + node { + id + createdAt + status + meta } } -} \ No newline at end of file + } +} diff --git a/src/main.rs b/src/main.rs index dc037eb..dab510d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ commands!( completion, connect, deploy, + deployment, domain, docs, down,