//! The check stage's contract: a `ReleaseSource` says what a package's //! latest release is and where its releases API lives, and //! `version_from_tag` turns the resulting tag into a version string. Each //! hosting service implements the trait in its own module (`github`, //! `forgejo`) so per-host logic doesn't accumulate here; //! `source::for_package` picks the implementation for a package. use anyhow::Result; /// A hosting service a package's releases are published on. `Debug` so a /// `Box` can sit in a `Result` that tests unwrap. pub trait ReleaseSource: std::fmt::Debug { /// The latest release tag for `repo`. fn latest_release(&self, client: &reqwest::blocking::Client, repo: &str) -> Result; /// The releases API root, which `fetcher` and `verifier` build their /// own paths under. GitHub and Forgejo both serve /// `/repos/{owner}/{repo}/releases/tags/{tag}` there with the same /// `assets[].{name, browser_download_url}` shape, which is why those /// stages need only this and not the source itself. fn api(&self) -> &str; } /// 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) } #[cfg(test)] mod tests { use super::*; #[test] fn version_from_tag_strips_leading_v() { assert_eq!(version_from_tag("v2.62.0"), "2.62.0"); } #[test] fn version_from_tag_leaves_bare_version_unchanged() { assert_eq!(version_from_tag("0.12.15"), "0.12.15"); } }