Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
37 changes: 25 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String>,

/// 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,
Expand All @@ -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.
Expand All @@ -82,11 +87,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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)?;

Expand All @@ -95,10 +108,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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);
}
Expand Down
72 changes: 72 additions & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
83 changes: 74 additions & 9 deletions src/versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,29 +94,94 @@ 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=";
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<String>` 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<Vec<String>, Box<dyn std::error::Error>> {
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<String, Box<dyn std::error::Error>> {
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<reqwest::Response, reqwest::Error> {
Expand Down