mod check;
mod download;
mod install;
mod lock;
mod types;
pub(crate) mod verify;
pub use check::{check_for_updates, spawn_update_check_if_needed};
pub use download::download_release;
pub use install::{install_update, rollback};
pub use types::{Channel, UpdateCheck};
use crate::commands;
use crate::ui::OutputContext;
use anyhow::Result;
use types::UpdateError;
#[allow(clippy::too_many_arguments)]
pub async fn run_update(
ctx: &OutputContext,
check_only: bool,
version: Option<String>,
channel: Channel,
fleet: bool,
do_rollback: bool,
verify_only: bool,
dry_run: bool,
yes: bool,
skip_verify: bool,
no_restart: bool,
drain_timeout: u64,
show_changelog: bool,
) -> Result<()> {
if do_rollback {
return rollback(ctx, dry_run, None)
.await
.map_err(|e| anyhow::anyhow!("{}", e));
}
if verify_only {
return verify_installation(ctx).await;
}
let update_info = check_for_updates(channel, version.clone(), false).await?;
if check_only {
display_update_check(ctx, &update_info, show_changelog);
return Ok(());
}
if !update_info.update_available {
if !ctx.is_json() {
println!("Already up to date ({})", update_info.current_version);
} else {
println!(
"{}",
serde_json::json!({
"update_available": false,
"current_version": update_info.current_version.to_string(),
})
);
}
return Ok(());
}
if !ctx.is_json() && !yes {
println!(
"Update available: {} -> {}",
update_info.current_version, update_info.latest_version
);
if show_changelog && let Some(ref notes) = update_info.release_notes {
println!("\nRelease notes:\n{}", notes);
}
if !dry_run {
println!("\nProceed with update? [y/N]");
if !yes {
println!("Use --yes to confirm update");
return Ok(());
}
}
}
if dry_run {
println!("Dry run: would update to {}", update_info.latest_version);
return Ok(());
}
let download = download_release(ctx, &update_info, skip_verify).await?;
if !download.checksum_verified {
let asset = download
.archive_path
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
if skip_verify {
if !ctx.is_json() {
println!(
"Warning: proceeding without checksum verification for {} (--skip-verify)",
asset
);
}
} else {
return Err(UpdateError::ChecksumMissing { asset }.into());
}
}
let result = install_update(ctx, &download, !no_restart, drain_timeout).await?;
if !ctx.is_json() {
println!(
"Successfully updated to {} (backup: {})",
update_info.latest_version,
result.backup_path.display()
);
}
if fleet {
update_fleet(ctx, &update_info, dry_run).await?;
}
Ok(())
}
fn display_update_check(ctx: &OutputContext, info: &UpdateCheck, show_changelog: bool) {
if ctx.is_json() {
println!("{}", serde_json::to_string_pretty(info).unwrap());
return;
}
if info.update_available {
println!(
"Update available: {} -> {}",
info.current_version, info.latest_version
);
println!("Release URL: {}", info.release_url);
if show_changelog {
if let Some(ref diff) = info.changelog_diff {
println!("\nChanges since {}:\n{}", info.current_version, diff);
} else if let Some(ref notes) = info.release_notes {
println!("\nRelease notes:\n{}", notes);
}
}
println!("\nRun 'rch update' to update.");
} else {
println!("Already up to date ({})", info.current_version);
}
}
async fn verify_installation(ctx: &OutputContext) -> Result<()> {
if !ctx.is_json() {
println!("Verifying installation...");
}
let rch_version = std::process::Command::new(std::env::current_exe()?)
.arg("--version")
.output()?;
if rch_version.status.success() {
let version = String::from_utf8_lossy(&rch_version.stdout);
if !ctx.is_json() {
println!("rch: {}", version.trim());
}
}
if !ctx.is_json() {
println!("Installation verified.");
}
Ok(())
}
async fn update_fleet(ctx: &OutputContext, _info: &UpdateCheck, dry_run: bool) -> Result<()> {
if !ctx.is_json() {
println!("Updating fleet...");
}
if dry_run {
if !ctx.is_json() {
println!("Dry run: would update all configured workers");
}
commands::workers_deploy_binary(None, true, false, true, ctx).await?;
return Ok(());
}
commands::workers_deploy_binary(None, true, false, false, ctx).await?;
if !ctx.is_json() {
println!("Fleet update complete.");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::set_test_config_dir_override;
use crate::ui::{OutputConfig, OutputMode};
use types::Version;
#[test]
fn test_channel_default() {
assert_eq!(Channel::default(), Channel::Stable);
}
#[test]
fn test_channel_variants() {
let stable = Channel::Stable;
let beta = Channel::Beta;
let nightly = Channel::Nightly;
assert_ne!(stable, beta);
assert_ne!(beta, nightly);
assert_ne!(stable, nightly);
}
fn create_test_output_context(json: bool) -> OutputContext {
let config = OutputConfig {
force_mode: Some(if json {
OutputMode::Json
} else {
OutputMode::Plain
}),
json,
..Default::default()
};
OutputContext::new(config)
}
fn create_test_update_check(update_available: bool) -> UpdateCheck {
UpdateCheck {
current_version: Version::parse("1.0.0").unwrap(),
latest_version: if update_available {
Version::parse("2.0.0").unwrap()
} else {
Version::parse("1.0.0").unwrap()
},
update_available,
release_url: "https://github.com/test/releases/v2.0.0".to_string(),
release_notes: Some("Test release notes".to_string()),
changelog_diff: None,
assets: vec![],
}
}
#[test]
fn test_update_check_creation_no_update() {
let check = create_test_update_check(false);
assert!(!check.update_available);
assert_eq!(check.current_version, check.latest_version);
}
#[test]
fn test_update_check_creation_with_update() {
let check = create_test_update_check(true);
assert!(check.update_available);
assert!(check.latest_version > check.current_version);
}
#[test]
fn test_update_check_has_release_notes() {
let check = create_test_update_check(true);
assert!(check.release_notes.is_some());
assert!(check.release_notes.as_ref().unwrap().contains("Test"));
}
#[test]
fn test_update_check_release_url() {
let check = create_test_update_check(true);
assert!(!check.release_url.is_empty());
assert!(check.release_url.contains("github"));
}
#[test]
fn test_display_update_check_json_mode() {
let ctx = create_test_output_context(true);
let info = create_test_update_check(true);
display_update_check(&ctx, &info, false);
}
#[test]
fn test_display_update_check_no_update() {
let ctx = create_test_output_context(false);
let info = create_test_update_check(false);
display_update_check(&ctx, &info, false);
}
#[test]
fn test_display_update_check_with_update() {
let ctx = create_test_output_context(false);
let info = create_test_update_check(true);
display_update_check(&ctx, &info, false);
}
#[test]
fn test_display_update_check_with_changelog() {
let ctx = create_test_output_context(false);
let info = create_test_update_check(true);
display_update_check(&ctx, &info, true);
}
#[test]
fn test_update_check_empty_assets() {
let check = create_test_update_check(true);
assert!(check.assets.is_empty());
}
#[test]
fn test_update_check_with_changelog_diff() {
let mut check = create_test_update_check(true);
check.changelog_diff = Some("## Changes\n- Feature A\n- Bug fix B".to_string());
assert!(check.changelog_diff.is_some());
}
#[tokio::test]
async fn test_verify_installation_plain_mode() {
let ctx = create_test_output_context(false);
let _ = verify_installation(&ctx).await;
}
#[tokio::test]
async fn test_verify_installation_json_mode() {
let ctx = create_test_output_context(true);
let _ = verify_installation(&ctx).await;
}
#[tokio::test]
async fn test_update_fleet_dry_run() {
let temp_dir = std::env::temp_dir().join("rch_test_update_fleet_dry_run");
let _ = std::fs::create_dir_all(&temp_dir);
set_test_config_dir_override(Some(temp_dir.clone()));
let ctx = create_test_output_context(false);
let info = create_test_update_check(true);
let result = update_fleet(&ctx, &info, true).await;
assert!(result.is_ok());
set_test_config_dir_override(None);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_update_fleet_json_mode() {
let temp_dir = std::env::temp_dir().join("rch_test_update_fleet_json_mode");
let _ = std::fs::create_dir_all(&temp_dir);
set_test_config_dir_override(Some(temp_dir.clone()));
let ctx = create_test_output_context(true);
let info = create_test_update_check(true);
let result = update_fleet(&ctx, &info, false).await;
assert!(result.is_ok());
set_test_config_dir_override(None);
let _ = std::fs::remove_dir_all(&temp_dir);
}
}