use std::env;
use self_update::cargo_crate_version;
const REPO_OWNER: &str = "ndaal";
const REPO_NAME: &str = "archmeld";
const BIN_NAME: &str = "archmeld";
pub const NO_SELF_UPDATE_ENV: &str = "ARCHMELD_NO_SELF_UPDATE";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpdateOutcome {
UpToDate {
current: String,
},
Available {
current: String,
latest: String,
},
Updated {
from: String,
to: String,
},
Unreachable {
reason: String,
},
DisabledByPolicy,
}
impl UpdateOutcome {
#[must_use]
pub const fn exit_code(&self) -> u8 {
match *self {
Self::UpToDate { .. } | Self::Updated { .. } | Self::Unreachable { .. } => 0,
Self::Available { .. } => 10,
Self::DisabledByPolicy => 3,
}
}
#[must_use]
pub fn message(&self) -> String {
match *self {
Self::UpToDate { ref current } => {
format!("archmeld {current} is up to date")
},
Self::Available {
ref current,
ref latest,
} => format!(
"archmeld {latest} is available (running {current}); run `archmeld --self-update`"
),
Self::Updated { ref from, ref to } => {
format!("archmeld updated {from} -> {to}")
},
Self::Unreachable { ref reason } => {
format!("update check skipped: update host unreachable ({reason})")
},
Self::DisabledByPolicy => format!(
"self-update is disabled by policy (--no-self-update / {NO_SELF_UPDATE_ENV})"
),
}
}
}
#[must_use]
pub fn flag_value_is_on(raw: &str) -> bool {
matches!(
raw.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
#[must_use]
pub fn env_flag_on(name: &str) -> bool {
#[allow(clippy::disallowed_methods)]
env::var(name).is_ok_and(|raw| flag_value_is_on(&raw))
}
#[must_use]
pub fn is_disabled(flag: bool) -> bool {
flag || env_flag_on(NO_SELF_UPDATE_ENV)
}
fn build_updater(
no_confirm: bool,
) -> Result<self_update::backends::github::Update, self_update::Error> {
self_update::backends::github::Update::configure()
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.bin_name(BIN_NAME)
.current_version(cargo_crate_version!())
.show_download_progress(true)
.show_output(false)
.no_confirm(no_confirm)
.build()
}
#[must_use]
pub fn check_update() -> UpdateOutcome {
let current = cargo_crate_version!().to_owned();
let updater = match build_updater(true) {
Ok(u) => u,
Err(e) => {
return UpdateOutcome::Unreachable {
reason: e.to_string(),
};
},
};
match updater.get_latest_release() {
Ok(releases) => match releases.is_update_available() {
Ok(true) => releases.latest().map_or_else(
|| UpdateOutcome::UpToDate {
current: current.clone(),
},
|release| UpdateOutcome::Available {
current: current.clone(),
latest: release.version().to_owned(),
},
),
Ok(false) => UpdateOutcome::UpToDate { current },
Err(e) => UpdateOutcome::Unreachable {
reason: e.to_string(),
},
},
Err(e) => UpdateOutcome::Unreachable {
reason: e.to_string(),
},
}
}
pub fn self_update(disabled: bool, interactive: bool) -> Result<UpdateOutcome, self_update::Error> {
if disabled {
return Ok(UpdateOutcome::DisabledByPolicy);
}
let from = cargo_crate_version!().to_owned();
let updater = build_updater(!interactive)?;
let status = updater.update()?;
let to = status.version().to_owned();
if to == from {
Ok(UpdateOutcome::UpToDate { current: from })
} else {
Ok(UpdateOutcome::Updated { from, to })
}
}
#[cfg(test)]
mod tests {
use super::{NO_SELF_UPDATE_ENV, UpdateOutcome, env_flag_on, flag_value_is_on, is_disabled};
#[test]
fn exit_codes_match_the_documented_contract() {
assert_eq!(
UpdateOutcome::UpToDate {
current: "1.2.3".to_owned()
}
.exit_code(),
0
);
assert_eq!(
UpdateOutcome::Available {
current: "1.2.3".to_owned(),
latest: "1.3.0".to_owned(),
}
.exit_code(),
10
);
assert_eq!(
UpdateOutcome::Updated {
from: "1.2.3".to_owned(),
to: "1.3.0".to_owned(),
}
.exit_code(),
0
);
assert_eq!(UpdateOutcome::DisabledByPolicy.exit_code(), 3);
}
#[test]
fn unreachable_is_not_a_failure() {
let outcome = UpdateOutcome::Unreachable {
reason: "dns error".to_owned(),
};
assert_eq!(outcome.exit_code(), 0);
assert!(outcome.message().contains("unreachable"));
}
#[test]
fn flag_value_accepts_only_the_documented_truthy_values() {
for truthy in ["1", "true", "TRUE", "yes", "On", " on "] {
assert!(flag_value_is_on(truthy), "{truthy:?} should be true");
}
for falsy in ["0", "false", "no", "off", "", "maybe", "2"] {
assert!(!flag_value_is_on(falsy), "{falsy:?} should be false");
}
}
#[test]
fn cli_flag_wins_over_environment() {
assert!(is_disabled(true), "--no-self-update must always disable");
}
#[test]
fn env_name_is_the_documented_one() {
assert_eq!(NO_SELF_UPDATE_ENV, "ARCHMELD_NO_SELF_UPDATE");
assert!(!env_flag_on("ARCHMELD_DEFINITELY_NOT_SET_XYZ"));
}
}