use regex::Regex;
use serde::Deserialize;
use std::collections::{BTreeMap, HashSet};
#[derive(Debug, Deserialize)]
struct CargoLock {
package: Vec<Package>,
}
#[derive(Debug, Deserialize)]
struct Package {
name: String,
version: String,
source: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PlanToml {
#[serde(rename = "crate")]
pub crates: Vec<Crate>,
}
#[derive(Debug, Deserialize)]
pub struct Crate {
pub name: String,
pub to: String,
pub from: String,
pub publish: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct OrmlToml {
pub workspace: Workspace,
}
#[derive(Deserialize, Debug)]
pub struct Metadata {
orml: Orml,
}
#[derive(Deserialize, Debug)]
pub struct Orml {
#[serde(rename = "crates-version")]
crates_version: String,
}
#[derive(Deserialize, Debug)]
pub struct Workspace {
members: Vec<String>,
metadata: Metadata,
}
#[derive(Deserialize, Debug)]
pub struct TagInfo {
pub name: String,
}
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+)?$";
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)
}
async fn github_query(url: &str) -> Result<reqwest::Response, reqwest::Error> {
let mut builder = reqwest::Client::new()
.get(url)
.header("User-Agent", "reqwest")
.header("Accept", "application/vnd.github.v3+json");
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
builder = builder.header("Authorization", format!("Bearer {}", token))
};
builder.send().await
}
pub async fn get_stable_tag_versions() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut release_tags = vec![];
let tag_regex = Regex::new(POLKADOT_SDK_STABLE_TAGS_REGEX).unwrap();
for page in 1..100 {
let response = github_query(&format!("{}{}", POLKADOT_SDK_TAGS_URL, page)).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",
&format!("{}{}", POLKADOT_SDK_TAGS_GH_CMD_URL, page),
])
.output()?
.stdout,
)?
};
let tag_branches: Vec<TagInfo> = serde_json::from_str(&output)?;
let stable_tag_branches = tag_branches
.iter()
.filter(|b| tag_regex.is_match(&b.name))
.map(|branch| branch.name.to_string());
release_tags = release_tags
.into_iter()
.chain(stable_tag_branches)
.collect();
if tag_branches.len() < 100 {
break;
}
}
Ok(release_tags)
}
pub async fn get_orml_crates_and_version(
base_url: &str,
version: &str,
) -> Result<Option<OrmlToml>, Box<dyn std::error::Error>> {
if get_release_branches_versions(Repository::Orml)
.await?
.contains(&version.to_string())
{
let version_url = format!(
"{}/open-web3-stack/open-runtime-module-library/polkadot-v{}/Cargo.dev.toml",
base_url, version
);
let response = github_query(&version_url).await?;
let content = response.text().await?;
let orml_workspace_members = toml::from_str::<OrmlToml>(&content)
.map_err(|_| "Error Parsing ORML TOML. Required Fields not Found")?;
Ok(Some(orml_workspace_members))
} else {
log::error!(
"No matching ORML release version found for corresponding polkadot-sdk version."
);
Ok(None)
}
}
pub fn include_orml_crates_in_version_mapping(
crates_versions: &mut BTreeMap<String, String>,
orml_crates_version: Option<OrmlToml>,
) {
if let Some(orml_toml) = orml_crates_version {
for crate_name in orml_toml.workspace.members {
crates_versions.insert(
format!("orml-{}", crate_name),
orml_toml.workspace.metadata.orml.crates_version.clone(),
);
}
}
}
pub async fn get_version_mapping_with_fallback(
base_url: &str,
version: &str,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
let result = get_version_mapping(base_url, version, "Plan.toml").await;
match result {
Err(_) => get_version_mapping(base_url, version, "Cargo.lock").await,
Ok(_) => result,
}
}
fn version_to_url(base_url: &str, version: &str, source: &str) -> String {
let stable_tag_regex_patten = Regex::new(POLKADOT_SDK_STABLE_TAGS_REGEX).unwrap();
let version = if version.starts_with("stable") {
format!("polkadot-{}", version)
} else if stable_tag_regex_patten.is_match(version) {
version.into()
} else {
format!("release-crates-io-v{}", version)
};
format!(
"{}/paritytech/polkadot-sdk/{}/{}",
base_url, version, source
)
}
pub async fn get_version_mapping(
base_url: &str,
version: &str,
source: &str,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
let url = version_to_url(base_url, version, source);
let response = github_query(&url).await?;
let content = match response.error_for_status() {
Ok(response) => response.text().await?,
Err(err) => return Err(err.into()),
};
match source {
"Cargo.lock" => get_cargo_packages(&content),
"Plan.toml" => get_plan_packages(&content).await,
_ => panic!("Unknown source: {}", source),
}
}
fn get_cargo_packages(
content: &str,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
let cargo_lock: CargoLock = toml::from_str(content)?;
let cargo_packages: BTreeMap<_, _> = cargo_lock
.package
.into_iter()
.filter(|pkg| pkg.source.is_none())
.map(|pkg| (pkg.name, pkg.version))
.collect();
Ok(cargo_packages)
}
async fn get_plan_packages(
content: &str,
) -> Result<BTreeMap<String, String>, Box<dyn std::error::Error>> {
let plan_toml: PlanToml = toml::from_str(content)?;
let parity_owned_crates = get_parity_crate_owner_crates().await?;
let plan_packages: BTreeMap<_, _> = plan_toml
.crates
.into_iter()
.filter(|pkg| {
pkg.publish.unwrap_or(true) || {
let placeholder = pkg.to == "0.0.0" && pkg.from == "0.0.0";
let public_not_in_release = parity_owned_crates.contains(&pkg.name) && !placeholder;
if public_not_in_release {
log::info!(
"Adding public crate not in release {}: {} -> {}",
pkg.name,
pkg.from,
pkg.to
);
}
public_not_in_release
}
})
.map(|pkg| (pkg.name, pkg.to))
.collect();
Ok(plan_packages)
}
#[derive(serde::Deserialize, Debug)]
struct Branch {
name: String,
}
struct RepositoryInfo {
branches_url: String,
gh_cmd_url: String,
version_filter_string: String,
version_replace_string: String,
}
pub enum Repository {
Orml,
Psdk,
}
fn get_repository_info(repository: &Repository) -> RepositoryInfo {
match repository {
Repository::Orml => RepositoryInfo {
branches_url: "https://api.github.com/repos/open-web3-stack/open-runtime-module-library/branches?per_page=100&page=".into(),
gh_cmd_url: "/repos/open-web3-stack/open-runtime-module-library/branches?per_page=100&page=".into(),
version_filter_string: "polkadot-v1".into(),
version_replace_string: "polkadot-v".into()
},
Repository::Psdk => RepositoryInfo {
branches_url: "https://api.github.com/repos/paritytech/polkadot-sdk/branches?per_page=100&page=".into(),
gh_cmd_url: "/repos/paritytech/polkadot-sdk/branches?per_page=100&page=".into(),
version_filter_string: "release-crates-io-v".into(),
version_replace_string: "release-crates-io-v".into()
},
}
}
pub async fn get_release_branches_versions(
repository: Repository,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut release_branches = vec![];
let repository_info = get_repository_info(&repository);
for page in 1..100 {
let response = github_query(&format!("{}{}", repository_info.branches_url, page)).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",
&format!("{}{}", repository_info.gh_cmd_url, page),
])
.output()?
.stdout,
)?
};
let branches: Vec<Branch> = serde_json::from_str(&output)?;
let version_branches = branches
.iter()
.filter(|b| b.name.starts_with(&repository_info.version_filter_string))
.filter(|b| b.name != "polkadot-v1.0.0") .map(|branch| {
branch
.name
.replace(&repository_info.version_replace_string, "")
});
release_branches = release_branches
.into_iter()
.chain(version_branches)
.collect();
if branches.len() < 100 {
break;
}
}
Ok(release_branches)
}
pub async fn get_parity_crate_owner_crates() -> Result<HashSet<String>, Box<dyn std::error::Error>>
{
let mut parity_crates = HashSet::new();
for page in 1..=10 {
let response = reqwest::Client::new()
.get(format!(
"https://crates.io/api/v1/crates?page={}&per_page=100&user_id=150167", page
))
.header("User-Agent", "reqwest")
.header("Accept", "application/vnd.github.v3+json")
.send()
.await?;
let output = response.text().await?;
let crates_data: serde_json::Value = serde_json::from_str(&output)?;
let crates = crates_data["crates"].as_array().unwrap().iter();
let crates_len = crates.len();
let crate_names = crates
.filter(|crate_data| crate_data["max_version"].as_str().unwrap_or_default() != "0.0.0")
.map(|crate_data| crate_data["id"].as_str().unwrap_or_default().to_string());
parity_crates.extend(crate_names);
if crates_len < 100 {
break;
}
}
Ok(parity_crates)
}