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)
}