diff --git a/Cargo.toml b/Cargo.toml index e5ee82bf4..0ab976d35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,13 @@ dirs = "5.0.1" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" reqwest = { version = "0.12.8", default-features = false, features = [ - "rustls-tls", "json" + "rustls-tls", + "json", ] } -chrono = { version = "0.4.38", features = ["serde"], default-features = false } +chrono = { version = "0.4.38", features = [ + "serde", + "clock", +], default-features = false } graphql_client = { version = "0.14.0", features = ["reqwest-rustls"] } paste = "1.0.15" tokio = { version = "1.40.0", features = ["full"] } diff --git a/src/commands/check_updates.rs b/src/commands/check_updates.rs new file mode 100644 index 000000000..bed1f503d --- /dev/null +++ b/src/commands/check_updates.rs @@ -0,0 +1,34 @@ +use crate::util::check_update::check_update; + +use super::*; +use serde_json::json; + +/// Test the update check +#[derive(Parser)] +pub struct Args {} + +pub async fn command(_args: Args, json: bool) -> Result<()> { + let mut configs = Configs::new()?; + + if json { + let result = configs.check_update(true).await; + + let json = json!({ + "latest_version": result.ok().flatten().as_ref(), + "current_version": env!("CARGO_PKG_VERSION"), + }); + + println!("{}", serde_json::to_string_pretty(&json)?); + + return Ok(()); + } + + let is_latest = check_update(&mut configs, true).await?; + if is_latest { + println!( + "You are on the latest version of the CLI, v{}", + env!("CARGO_PKG_VERSION") + ); + } + Ok(()) +} diff --git a/src/commands/init.rs b/src/commands/init.rs index 43646951d..0b1686a2f 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use crate::util::prompt::prompt_select; +use crate::util::{check_update::check_update_command, prompt::prompt_select}; use super::{queries::user_projects::UserProjectsMeTeamsEdgesNode, *}; @@ -15,6 +15,9 @@ pub struct Args { pub async fn command(args: Args, _json: bool) -> Result<()> { let mut configs = Configs::new()?; + + check_update_command(&mut configs).await?; + let client = GQLClient::new_authorized(&configs)?; let vars = queries::user_projects::Variables {}; diff --git a/src/commands/link.rs b/src/commands/link.rs index 48ba2536f..5c2ec265b 100644 --- a/src/commands/link.rs +++ b/src/commands/link.rs @@ -3,7 +3,10 @@ use std::fmt::Display; use crate::{ errors::RailwayError, - util::prompt::{fake_select, prompt_options, prompt_options_skippable}, + util::{ + check_update::check_update_command, + prompt::{fake_select, prompt_options, prompt_options_skippable}, + }, }; use super::{ @@ -37,6 +40,9 @@ pub struct Args { pub async fn command(args: Args, _json: bool) -> Result<()> { let mut configs = Configs::new()?; + + check_update_command(&mut configs).await?; + let client = GQLClient::new_authorized(&configs)?; let me = post_graphql::( &client, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6aa5dc440..080097c0f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -29,3 +29,5 @@ pub mod up; pub mod variables; pub mod volume; pub mod whoami; + +pub mod check_updates; diff --git a/src/commands/run.rs b/src/commands/run.rs index 41ed06922..852d3f8a3 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -8,7 +8,10 @@ use crate::{ variables::get_service_variables, }, errors::RailwayError, - util::prompt::{prompt_select, PromptService}, + util::{ + check_update::check_update_command, + prompt::{prompt_select, PromptService}, + }, }; use super::{queries::project::ProjectProject, *}; @@ -73,7 +76,11 @@ async fn get_service( } pub async fn command(args: Args, _json: bool) -> Result<()> { - let configs = Configs::new()?; + // only needs to be mutable for the update check + let mut configs = Configs::new()?; + check_update_command(&mut configs).await?; + let configs = configs; // so we make it immutable again + let client = GQLClient::new_authorized(&configs)?; let linked_project = configs.get_linked_project().await?; diff --git a/src/config.rs b/src/config.rs index 7e8b67d2c..b1fcf3ba7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,8 +7,10 @@ use std::{ }; use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; use colored::Colorize; use inquire::ui::{Attributes, RenderConfig, StyleSheet, Styled}; +use is_terminal::IsTerminal; use serde::{Deserialize, Serialize}; use crate::{ @@ -42,6 +44,7 @@ pub struct RailwayUser { pub struct RailwayConfig { pub projects: BTreeMap, pub user: RailwayUser, + pub last_update_check: Option>, } #[derive(Debug)] @@ -57,6 +60,13 @@ pub enum Environment { Dev, } +#[derive(Deserialize)] +struct GithubApiRelease { + tag_name: String, +} + +const GITHUB_API_RELEASE_URL: &str = "https://api.github.com/repos/railwayapp/cli/releases/latest"; + impl Configs { pub fn new() -> Result { let environment = Self::get_environment_id(); @@ -79,6 +89,7 @@ impl Configs { RailwayConfig { projects: BTreeMap::new(), user: RailwayUser { token: None }, + last_update_check: None, } }); @@ -95,6 +106,7 @@ impl Configs { root_config: RailwayConfig { projects: BTreeMap::new(), user: RailwayUser { token: None }, + last_update_check: None, }, }) } @@ -103,6 +115,7 @@ impl Configs { self.root_config = RailwayConfig { projects: BTreeMap::new(), user: RailwayUser { token: None }, + last_update_check: None, }; Ok(()) } @@ -313,4 +326,43 @@ impl Configs { Ok(()) } + + pub async fn check_update(&mut self, force: bool) -> anyhow::Result> { + // outputting would break json output on CI + if !std::io::stdout().is_terminal() && !force { + return Ok(None); + } + + let should_update = if let Some(last_update_check) = self.root_config.last_update_check { + Utc::now().date_naive() != last_update_check.date_naive() || force + } else { + true + }; + + if !should_update { + return Ok(None); + } + + let client = reqwest::Client::new(); + let response = client + .get(GITHUB_API_RELEASE_URL) + .header("User-Agent", "railwayapp") + .send() + .await?; + + self.root_config.last_update_check = Some(Utc::now()); + self.write() + .context("Failed to save time since last update check")?; + + let response = response.json::().await?; + let latest_version = response.tag_name.trim_start_matches('v'); + + let current_version = env!("CARGO_PKG_VERSION"); + + if latest_version == current_version { + return Ok(None); + } + + Ok(Some(latest_version.to_owned())) + } } diff --git a/src/main.rs b/src/main.rs index 0a7cde75d..118401b0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; mod commands; use commands::*; +use util::check_update::check_update_command; mod client; mod config; @@ -21,6 +22,7 @@ mod macros; #[derive(Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] +// #[clap(author, about, long_about = None)] pub struct Args { #[clap(subcommand)] command: Commands, @@ -58,11 +60,24 @@ commands_enum!( variables, whoami, volume, - redeploy + redeploy, + check_updates ); #[tokio::main] async fn main() -> Result<()> { + // intercept the args + { + let args: Vec = std::env::args().collect(); + let flags = ["--version", "-V", "-h", "--help", "help"]; + let check_version = args.into_iter().any(|arg| flags.contains(&arg.as_str())); + + if check_version { + let mut configs = Configs::new()?; + check_update_command(&mut configs).await?; + } + } + let cli = Args::parse(); match Commands::exec(cli).await { diff --git a/src/util/check_update.rs b/src/util/check_update.rs new file mode 100644 index 000000000..da1bd95e8 --- /dev/null +++ b/src/util/check_update.rs @@ -0,0 +1,21 @@ +use colored::Colorize; + +pub async fn check_update(configs: &mut crate::Configs, force: bool) -> anyhow::Result { + let result = configs.check_update(force).await; + if let Ok(Some(latest_version)) = result { + println!( + "{} v{} visit {} for more info", + "New version available:".green().bold(), + latest_version.yellow(), + "https://docs.railway.com/guides/cli".purple(), + ); + Ok(false) + } else { + Ok(true) + } +} + +pub async fn check_update_command(configs: &mut crate::Configs) -> anyhow::Result<()> { + check_update(configs, false).await?; + Ok(()) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 4c1a3567f..487cc046b 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,2 +1,3 @@ pub mod logs; pub mod prompt; +pub mod check_update;