use anyhow::{Result, bail}; use regex::Regex; /// Resolves the latest release tag for `repo` via its public Atom feed. /// /// Deliberately not a full XML parse: the feed lists entries newest-first, /// and each `"/>` is matched /// in document order. Revisit with a real XML parser if GitHub's feed /// shape ever changes. /// /// The feed can list a tag newer than any tag with a real Release object /// behind it — observed on scaleway/scaleway-cli, which pushes a /// `vX.Y.Z-dbg1` tag (no corresponding Release; `releases/tags/` /// 404s) right after each real release, and that tag sorts newest in the /// feed. So each candidate is confirmed against the releases API in feed /// order, returning the first that actually resolves. pub fn latest_github_release(client: &reqwest::blocking::Client, repo: &str) -> Result { let url = format!("https://github.com/{repo}/releases.atom"); let body = client.get(&url).send()?.error_for_status()?.text()?; let re = Regex::new(r#"releases/tag/([^"]+)""#)?; let mut candidates = re .captures_iter(&body) .map(|caps| caps[1].to_string()) .peekable(); if candidates.peek().is_none() { bail!("no release tag found in {url}"); } for tag in candidates { let release_url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}"); if client.get(&release_url).send()?.status().is_success() { return Ok(tag); } } bail!("no release tag in {url} resolved to a real release via the API") } /// Strips a leading `v` from a release tag, e.g. `v2.62.0` -> `2.62.0`. /// /// Some projects (uv) tag releases with the bare version and use it /// verbatim in asset filenames; others (scaleway-cli) tag `vX.Y.Z` but /// still use the bare version in filenames. This is the version string /// substituted into `{version}` placeholders in `asset_pattern` / /// `checksum_asset_pattern`, not the tag used for API/attestation calls. pub fn version_from_tag(tag: &str) -> &str { tag.strip_prefix('v').unwrap_or(tag) }