use crate::terminal::{color_primary, color_uri};
use crate::{fmt_log, fmt_warn, CommandGlobalOpts};
use clap::crate_version;
use colorful::Colorful;
use miette::{miette, Error, IntoDiagnostic, Result, WrapErr};
use ockam_core::env::get_env_with_default;
use serde::Deserialize;
use std::env;
use tracing::{debug, warn};
use url::Url;
const RELEASE_TAG_NAME_PREFIX: &str = "ockam_v";
fn upgrade_check_is_disabled() -> bool {
get_env_with_default("OCKAM_DISABLE_UPGRADE_CHECK", false).unwrap_or(false)
}
#[derive(Deserialize, Debug)]
struct ReleaseJson {
tag_name: String,
update_url: String,
}
impl ReleaseJson {
fn version(&self) -> Result<String> {
self.tag_name
.split_once(RELEASE_TAG_NAME_PREFIX)
.ok_or(miette!("Unknown release version: {}", self.tag_name))
.map(|(_, version)| version.to_string())
}
fn update_url(&self) -> Result<Url> {
Url::options()
.base_url(Some(&Url::parse("https://github.com").into_diagnostic()?))
.parse(&self.update_url)
.into_diagnostic()
.wrap_err(format!("Invalid download URL: {}", self.update_url))
}
}
struct Release {
version: String,
download_url: Url,
}
impl TryFrom<ReleaseJson> for Release {
type Error = Error;
fn try_from(json: ReleaseJson) -> Result<Self> {
Ok(Self {
version: json.version()?,
download_url: json.update_url()?,
})
}
}
pub fn check_if_an_upgrade_is_available(options: &CommandGlobalOpts) -> Result<()> {
if upgrade_check_is_disabled() || options.global_args.test_argument_parser {
debug!("Upgrade check is disabled");
return Ok(());
}
let latest_release = get_release_data()?;
let current_version =
semver::Version::parse(crate_version!()).map_err(|_| miette!("Invalid version"))?;
let latest_version =
semver::Version::parse(&latest_release.version).map_err(|_| miette!("Invalid version"))?;
if current_version < latest_version {
warn!(
"A new version of the Ockam Command is now available: {}",
latest_release.version
);
options.terminal.write_line(fmt_warn!(
"A new version of the Ockam Command is now available: {}",
color_primary(format!("v{}", latest_release.version))
))?;
options.terminal.write_line(fmt_log!(
"You can download it at: {}",
color_uri(latest_release.download_url.as_ref())
))?;
options.terminal.write_line(fmt_log!(
"Or run the following command to upgrade it: {}\n",
color_primary("brew install build-trust/ockam/ockam")
))?;
}
Ok(())
}
fn get_release_data() -> Result<Release> {
let client = reqwest::blocking::Client::builder()
.user_agent("ockam")
.default_headers({
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/json"),
);
headers
})
.build()
.into_diagnostic()
.wrap_err("Failed to create a HTTP client")?;
let parsed = client
.get("https://github.com/build-trust/ockam/releases/latest")
.send()
.and_then(|resp| resp.json::<ReleaseJson>())
.into_diagnostic()
.wrap_err("Failed to parse JSON response")?;
Release::try_from(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn parse_release_from_json_response() {
let raw_response = r#"
{"id":136375799,"tag_name":"ockam_v0.116.0",
"update_url":"/build-trust/ockam/releases/tag/ockam_v0.116.0",
"update_authenticity_token":"token",
"delete_url":"/build-trust/ockam/releases/tag/ockam_v0.116.0",
"delete_authenticity_token":"token",
"edit_url":"/build-trust/ockam/releases/edit/ockam_v0.116.0"}
"#;
let json: ReleaseJson = serde_json::from_str(raw_response).unwrap();
let release = Release::try_from(json).unwrap();
assert_eq!(release.version, "0.116.0");
assert_eq!(
release.download_url,
Url::parse("https://github.com/build-trust/ockam/releases/tag/ockam_v0.116.0").unwrap()
);
}
#[test]
fn parse_release_from_json_struct() {
let crate_version = crate_version!().to_string();
let json = ReleaseJson {
tag_name: "ockam_v0.116.0".to_string(),
update_url: "/build-trust/ockam/releases/tag/ockam_v0.116.0".to_string(),
};
let release = Release::try_from(json).unwrap();
assert_eq!(release.version, "0.116.0");
assert_eq!(
release.download_url,
Url::parse("https://github.com/build-trust/ockam/releases/tag/ockam_v0.116.0").unwrap()
);
let json = ReleaseJson {
tag_name: format!("{RELEASE_TAG_NAME_PREFIX}{crate_version}"),
update_url: "/build-trust/ockam/releases/tag/ockam_v0.116.0".to_string(),
};
let release = Release::try_from(json).unwrap();
assert_eq!(&release.version, &crate_version);
assert_eq!(
release.download_url,
Url::parse("https://github.com/build-trust/ockam/releases/tag/ockam_v0.116.0").unwrap()
);
let json = ReleaseJson {
tag_name: "unknown_v0.0.1".to_string(),
update_url: "/build-trust/ockam/releases/tag/ockam_v0.116.0".to_string(),
};
assert!(Release::try_from(json).is_err());
}
#[test]
fn get_and_parse_release_data_from_github() {
let mut is_ok = false;
for _ in 0..5 {
if get_release_data().is_ok() {
is_ok = true;
break;
}
sleep(Duration::from_secs(2));
}
assert!(is_ok);
}
}