use super::*;
pub(crate) fn startup_version_check_source(config: &UpdateConfig) -> StartupVersionCheckSource {
resolve_version_check_source(config, codewhale_release::suppression_reason())
}
pub(crate) fn resolve_version_check_source(
config: &UpdateConfig,
suppression: Option<codewhale_release::SuppressionReason>,
) -> StartupVersionCheckSource {
if !config.check_for_updates {
return StartupVersionCheckSource::Disabled;
}
if let Some(reason) = suppression {
tracing::debug!(
variable = reason.variable(),
"skipping startup update check"
);
return StartupVersionCheckSource::Disabled;
}
if let Some(update_uri) = config.update_uri() {
return StartupVersionCheckSource::ConfiguredUrl(update_uri.to_string());
}
StartupVersionCheckSource::ReleaseResolver
}
pub(crate) fn update_check_cache_path() -> Option<PathBuf> {
codewhale_config::codewhale_home()
.ok()
.map(|home| codewhale_release::check::cache_path_in(&home))
}
pub(crate) fn spawn_startup_version_check(
config: UpdateConfig,
) -> Option<tokio::task::JoinHandle<Option<UpdateNotice>>> {
let source = startup_version_check_source(&config);
if source == StartupVersionCheckSource::Disabled {
return None;
}
let current = env!("CARGO_PKG_VERSION").to_string();
let cache_path = update_check_cache_path();
let interval_hours = config.check_interval_hours;
Some(tokio::spawn(async move {
cached_version_hint(source, ¤t, cache_path.as_deref(), interval_hours).await
}))
}
pub(crate) async fn cached_version_hint(
source: StartupVersionCheckSource,
current: &str,
cache_path: Option<&Path>,
interval_hours: u64,
) -> Option<UpdateNotice> {
let now = codewhale_release::check::now_unix();
if let Some(path) = cache_path
&& let Some(entry) = codewhale_release::UpdateCheckCache::load(path)
&& entry.is_fresh(now, interval_hours)
{
return entry
.latest_tag
.as_deref()
.and_then(|tag| version_hint_from_latest_tag(tag, current));
}
let latest_tag = latest_tag_from_startup_source(source).await;
if let Some(path) = cache_path
&& latest_tag.is_some()
&& let Err(err) = codewhale_release::UpdateCheckCache::now(latest_tag.clone()).store(path)
{
tracing::debug!(error = %err, "failed to persist update-check cache");
}
latest_tag
.as_deref()
.and_then(|tag| version_hint_from_latest_tag(tag, current))
}
pub(crate) async fn latest_tag_from_startup_source(
source: StartupVersionCheckSource,
) -> Option<String> {
match source {
StartupVersionCheckSource::Disabled => None,
StartupVersionCheckSource::ConfiguredUrl(url) => {
match latest_tag_from_configured_update_uri(&url).await {
Ok(tag) => tag,
Err(_) => latest_tag_from_release_mirror_env().await,
}
}
StartupVersionCheckSource::ReleaseResolver => {
if release_mirror_env_configured() {
return latest_tag_from_release_mirror_env().await;
}
let body = codewhale_release::fetch_release_json_async(
codewhale_release::LATEST_RELEASE_URL,
"latest release",
)
.await
.ok()?;
let json: serde_json::Value = serde_json::from_str(&body).ok()?;
publishable_release_tag(&json).map(str::to_string)
}
}
}
pub(crate) async fn latest_tag_from_release_mirror_env() -> Option<String> {
if !release_mirror_env_configured() {
return None;
}
codewhale_release::latest_release_tag_async(codewhale_release::ReleaseChannel::Stable)
.await
.ok()
}
pub(crate) fn release_mirror_env_configured() -> bool {
let version = codewhale_release::update_version_from_env()
.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
codewhale_release::release_base_url_from_env(&version).is_some()
}
pub(crate) async fn latest_tag_from_configured_update_uri(
update_uri: &str,
) -> Result<Option<String>> {
let body = codewhale_release::fetch_release_json_async(update_uri, "configured latest release")
.await?;
let json: serde_json::Value = serde_json::from_str(&body).with_context(|| {
format!("failed to parse release JSON from configured URI {update_uri}")
})?;
Ok(custom_release_tag(&json).map(str::to_string))
}
pub(crate) fn publishable_release_tag(json: &serde_json::Value) -> Option<&str> {
if !release_has_required_assets(json) {
return None;
}
json["tag_name"].as_str()
}
pub(crate) fn custom_release_tag(json: &serde_json::Value) -> Option<&str> {
if !release_is_publishable(json) {
return None;
}
if json.get("assets").is_some() && !release_has_required_assets(json) {
return None;
}
json["tag_name"].as_str()
}
#[cfg(test)]
pub(crate) fn version_hint_from_release_json(
json: &serde_json::Value,
current: &str,
) -> Option<UpdateNotice> {
version_hint_from_latest_tag(publishable_release_tag(json)?, current)
}
#[cfg(test)]
pub(crate) fn version_hint_from_custom_release_json(
json: &serde_json::Value,
current: &str,
) -> Option<UpdateNotice> {
version_hint_from_latest_tag(custom_release_tag(json)?, current)
}
pub(crate) fn version_hint_from_latest_tag(tag: &str, current: &str) -> Option<UpdateNotice> {
let latest = tag.trim_start_matches('v');
if !is_newer_version(latest, current) {
return None;
}
Some(UpdateNotice {
current: current.to_string(),
latest: latest.to_string(),
})
}
pub(crate) fn release_has_required_assets(json: &serde_json::Value) -> bool {
if !release_is_publishable(json) {
return false;
}
REQUIRED_RELEASE_ASSETS
.iter()
.all(|required| release_has_uploaded_asset(json, required))
}
pub(crate) fn release_is_publishable(json: &serde_json::Value) -> bool {
!json
.get("draft")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
&& !json
.get("prerelease")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
}
pub(crate) fn release_has_uploaded_asset(json: &serde_json::Value, required: &str) -> bool {
let Some(assets) = json.get("assets").and_then(serde_json::Value::as_array) else {
return false;
};
assets.iter().any(|asset| {
asset.get("name").and_then(serde_json::Value::as_str) == Some(required)
&& asset.get("state").and_then(serde_json::Value::as_str) == Some("uploaded")
})
}
pub(crate) fn is_newer_version(latest: &str, current: &str) -> bool {
match (parse_semver(latest), parse_semver(current)) {
(Some(l), Some(c)) => l > c,
_ => latest != current,
}
}
pub(crate) fn parse_semver(v: &str) -> Option<(u32, u32, u32)> {
let mut parts = v.splitn(3, '.');
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next()?.parse::<u32>().ok()?;
let patch = parts.next().unwrap_or("0").parse::<u32>().ok()?;
Some((major, minor, patch))
}