diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7045850..2897b93 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,8 +26,12 @@ jobs: - name: Run tests run: cargo test --locked + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Polkadot Runtimes Work + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | cargo install --path . --locked git clone --depth 1 --branch main https://github.com/polkadot-fellows/runtimes diff --git a/src/lib.rs b/src/lib.rs index 1206676..f530675 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,8 +29,9 @@ use std::{ use toml_edit::DocumentMut; pub use versions::{ - get_orml_crates_and_version, get_polkadot_sdk_versions, get_release_branches_versions, - get_version_mapping_with_fallback, include_orml_crates_in_version_mapping, Repository, + get_latest_polkadot_sdk_version, get_orml_crates_and_version, get_polkadot_sdk_versions, + get_release_branches_versions, get_version_mapping_with_fallback, + include_orml_crates_in_version_mapping, Repository, }; pub const DEFAULT_GIT_SERVER: &str = "https://raw.githubusercontent.com"; diff --git a/src/main.rs b/src/main.rs index a2e2495..0876871 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,10 @@ use clap::Parser; use env_logger::Env; use psvm::{ - get_orml_crates_and_version, get_polkadot_sdk_versions, get_release_branches_versions, - get_version_mapping_with_fallback, include_orml_crates_in_version_mapping, update_dependencies, - validate_workspace_path, Repository, DEFAULT_GIT_SERVER, + get_latest_polkadot_sdk_version, get_orml_crates_and_version, get_polkadot_sdk_versions, + get_release_branches_versions, get_version_mapping_with_fallback, + include_orml_crates_in_version_mapping, update_dependencies, validate_workspace_path, + Repository, DEFAULT_GIT_SERVER, }; use std::collections::BTreeMap; use std::path::PathBuf; @@ -37,11 +38,15 @@ struct Command { #[clap( short, long, - required_unless_present_any = ["list", "git_ref"], - conflicts_with = "git_ref" + required_unless_present_any = ["list", "git_ref", "latest"], + conflicts_with_all = ["git_ref", "latest"] )] version: Option, + /// Use the latest Polkadot SDK release version. + #[clap(short = 'L', long, conflicts_with_all = ["list", "git_ref"])] + latest: bool, + /// Overwrite local dependencies (using path) with same name as the ones in the Polkadot SDK. #[clap(short, long)] overwrite: bool, @@ -55,7 +60,7 @@ struct Command { check: bool, /// To either list available ORML versions or update the Cargo.toml file with corresponding ORML versions. - #[clap(short('O'), long, requires = "version")] + #[clap(short('O'), long)] orml: bool, /// Explicit git reference (branch or tag) to fetch release metadata from. @@ -82,11 +87,19 @@ async fn main() -> Result<(), Box> { return Ok(()); } - let version_or_ref = cmd - .version + let latest_version = if cmd.latest { + let v = get_latest_polkadot_sdk_version().await?; + println!("Using latest version: {}", v); + Some(v) + } else { + None + }; + + let version_or_ref = latest_version .as_deref() + .or(cmd.version.as_deref()) .or(cmd.git_ref.as_deref()) - .expect("clap enforces presence of either version or git_ref"); + .expect("clap enforces presence of version, latest, or git_ref"); let cargo_toml_path = validate_workspace_path(cmd.path)?; @@ -95,10 +108,10 @@ async fn main() -> Result<(), Box> { get_version_mapping_with_fallback(DEFAULT_GIT_SERVER, version_or_ref).await?; if cmd.orml { - let version = cmd - .version + let version = latest_version .as_deref() - .expect("ORML lookups require a version"); + .or(cmd.version.as_deref()) + .ok_or("ORML lookups require a version (use --version or --latest)")?; let orml_crates = get_orml_crates_and_version(DEFAULT_GIT_SERVER, version).await?; include_orml_crates_in_version_mapping(&mut crates_versions, orml_crates); } diff --git a/src/tests.rs b/src/tests.rs index 7ff9e59..8505977 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -329,6 +329,78 @@ to = "0.2.0" assert_eq!(mapping.get("package_minor"), Some(&"0.2.0".to_string())); } + #[tokio::test] + // Fetches the latest release version and verifies it returns a non-empty string + // that can be used to fetch a valid version mapping. + async fn test_get_latest_version() { + let latest = crate::versions::get_latest_polkadot_sdk_version() + .await + .unwrap(); + + assert!(!latest.is_empty(), "Latest version should not be empty"); + + // The latest version should work with the version mapping + let crates_versions = get_version_mapping_with_fallback(crate::DEFAULT_GIT_SERVER, &latest) + .await + .unwrap(); + + assert!( + crates_versions.len() > 0, + "No crate versions found for latest version: {}", + latest + ); + } + + #[tokio::test] + // Verifies that get_polkadot_sdk_versions returns versions sorted newest-first. + // Stable tags should appear before crates-io versions, and within stable tags, + // higher base versions and patch numbers should come first. + async fn test_versions_list_is_sorted_newest_first() { + let versions = crate::versions::get_polkadot_sdk_versions().await.unwrap(); + + assert!(!versions.is_empty()); + + // Find all stable tags and verify they are sorted correctly + let stable_tags: Vec<&str> = versions + .iter() + .filter(|v| v.starts_with("polkadot-stable")) + .map(|v| v.as_str()) + .collect(); + + fn parse_stable_tag(tag: &str) -> (u32, u32) { + let rest = tag.strip_prefix("polkadot-stable").unwrap_or(""); + match rest.split_once('-') { + Some((base, patch)) => (base.parse().unwrap_or(0), patch.parse().unwrap_or(0)), + None => (rest.parse().unwrap_or(0), 0), + } + } + + for window in stable_tags.windows(2) { + let (a_base, a_patch) = parse_stable_tag(window[0]); + let (b_base, b_patch) = parse_stable_tag(window[1]); + assert!( + (a_base, a_patch) >= (b_base, b_patch), + "Stable tags not sorted newest-first: {} should come before {}", + window[0], + window[1] + ); + } + + // Stable tags should appear before crates-io versions + let first_crates_io = versions + .iter() + .position(|v| !v.starts_with("polkadot-stable")); + let last_stable = versions + .iter() + .rposition(|v| v.starts_with("polkadot-stable")); + if let (Some(first_cio), Some(last_st)) = (first_crates_io, last_stable) { + assert!( + last_st < first_cio, + "All stable tags should appear before crates-io versions" + ); + } + } + #[tokio::test] // This test will fetch all available versions, update a generic parachain Cargo.toml file // and assert that the Cargo.toml file has been updated (modified) diff --git a/src/versions.rs b/src/versions.rs index 7bd2ab4..12ecad3 100644 --- a/src/versions.rs +++ b/src/versions.rs @@ -94,6 +94,15 @@ pub struct TagInfo { pub name: String, } +/// Represents a GitHub release, used to deserialize the latest release API response. +#[derive(Deserialize, Debug)] +struct Release { + /// The tag name of the release. + tag_name: String, +} + +const POLKADOT_SDK_LATEST_RELEASE_URL: &str = + "https://api.github.com/repos/paritytech/polkadot-sdk/releases/latest"; const POLKADOT_SDK_TAGS_URL: &str = "https://api.github.com/repos/paritytech/polkadot-sdk/tags?per_page=100&page="; const POLKADOT_SDK_TAGS_GH_CMD_URL: &str = "/repos/paritytech/polkadot-sdk/tags?per_page=100&page="; @@ -101,22 +110,78 @@ const POLKADOT_SDK_STABLE_TAGS_REGEX: &str = r"^polkadot-stable\d+(-\d+)?$"; /// Fetches a combined list of Polkadot SDK release versions and stable tag releases. /// -/// This function first retrieves release branch versions from the Polkadot SDK and -/// then fetches stable tag releases versions. It combines these two lists into a -/// single list of version strings. +/// Returns versions sorted newest-first: stable tags followed by crates-io releases. /// /// # Returns /// A `Result` containing either a `Vec` of combined version names on success, /// or an `Error` if any part of the process fails. -/// -/// # Errors -/// This function can return an error if either the fetching of release branches versions -/// or the fetching of stable tag versions encounters an issue. pub async fn get_polkadot_sdk_versions() -> Result, Box> { let mut crates_io_releases = get_release_branches_versions(Repository::Psdk).await?; let mut stable_tag_versions = get_stable_tag_versions().await?; - crates_io_releases.append(&mut stable_tag_versions); - Ok(crates_io_releases) + + // Sort stable tags by (base_version DESC, patch DESC) so newest appears first. + stable_tag_versions.sort_by(|a, b| { + fn parse_stable_tag(tag: &str) -> (u32, u32) { + let rest = tag.strip_prefix("polkadot-stable").unwrap_or(""); + match rest.split_once('-') { + Some((base, patch)) => (base.parse().unwrap_or(0), patch.parse().unwrap_or(0)), + None => (rest.parse().unwrap_or(0), 0), + } + } + parse_stable_tag(b).cmp(&parse_stable_tag(a)) + }); + + // Crates-io releases come oldest-first; reverse so newest is first. + crates_io_releases.reverse(); + + let mut all_versions = stable_tag_versions; + all_versions.append(&mut crates_io_releases); + Ok(all_versions) +} + +/// Fetches the latest Polkadot SDK release version from GitHub. +/// +/// This function queries the GitHub releases API for the latest release of the +/// Polkadot SDK and normalizes the tag name into a version string compatible +/// with `version_to_url()`. +/// +/// # Returns +/// A `Result` containing the normalized version string on success, or an `Error` +/// if the API request or response parsing fails. +pub async fn get_latest_polkadot_sdk_version() -> Result> { + let response = github_query(POLKADOT_SDK_LATEST_RELEASE_URL).await?; + let output = if response.status().is_success() { + response.text().await? + } else { + String::from_utf8( + std::process::Command::new("gh") + .args([ + "api", + "-H", + "Accept: application/vnd.github+json", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + "/repos/paritytech/polkadot-sdk/releases/latest", + ]) + .output()? + .stdout, + )? + }; + + let release: Release = serde_json::from_str(&output)?; + let tag = release.tag_name; + + let stable_tag_regex = Regex::new(POLKADOT_SDK_STABLE_TAGS_REGEX).unwrap(); + + let version = if let Some(stripped) = tag.strip_prefix("release-crates-io-v") { + stripped.to_string() + } else if stable_tag_regex.is_match(&tag) { + tag + } else { + tag + }; + + Ok(version) } async fn github_query(url: &str) -> Result {