Skip to content

Commit

Permalink
feat(new): ux improvements (#191)
Browse files Browse the repository at this point in the history
* feat(new): add warning if parachain template repo has no releases

* feat(new): add SupportedVersions to templates. Defaults to all versions

* feat(new): add IsAudited template property.

* feat(new): display template license

* test(templates): fix failing unit tests by adding new test templates
  • Loading branch information
peterwht authored May 30, 2024
1 parent 506dfe2 commit 7135ef5
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 13 deletions.
56 changes: 43 additions & 13 deletions crates/pop-cli/src/commands/new/parachain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,7 @@ async fn guide_user_to_generate_parachain() -> Result<NewParachainCommand> {
let provider = prompt.interact()?;
let template = display_select_options(provider)?;

let url = url::Url::parse(&template.repository_url()?).expect("valid repository url");
// Get only the latest 3 releases
let latest_3_releases: Vec<Release> = get_latest_3_releases(url).await?;

let mut release_name = None;
if latest_3_releases.len() > 0 {
release_name = Some(display_release_versions_to_user(latest_3_releases)?);
}
let release_name = choose_release(template).await?;

let name: String = input("Where should your project be created?")
.placeholder("./my-parachain")
Expand Down Expand Up @@ -200,10 +193,12 @@ fn generate_parachain_from_template(
.unwrap_or_default()
))?;

// warn about audit status and licensing
warning(format!("NOTE: the resulting parachain is not guaranteed to be audited or reviewed for security vulnerabilities.\n{}",
style(format!("Please consult the source repository at {} to assess production suitability and licensing restrictions.", template.repository_url()?))
.dim()))?;
if !template.is_audited() {
// warn about audit status and licensing
warning(format!("NOTE: the resulting parachain is not guaranteed to be audited or reviewed for security vulnerabilities.\n{}",
style(format!("Please consult the source repository at {} to assess production suitability and licensing restrictions.", template.repository_url()?))
.dim()))?;
}

// add next steps
let mut next_steps = vec![
Expand Down Expand Up @@ -290,15 +285,50 @@ fn check_destination_path(name_template: &String) -> Result<&Path> {
Ok(destination_path)
}

async fn get_latest_3_releases(url: url::Url) -> Result<Vec<Release>> {
/// Gets the latest 3 releases. Prompts the user to choose if releases exist.
/// Otherwise, the default release is used.
///
/// return: `Option<String>` - The release name selected by the user or None if no releases found.
async fn choose_release(template: &Template) -> Result<Option<String>> {
let url = url::Url::parse(&template.repository_url()?).expect("valid repository url");
let repo = GitHub::parse(url.as_str())?;

let license = repo.get_repo_license().await?;
log::info(format!("Template {}: {}", style("License").bold(), license))?;

// Get only the latest 3 releases that are supported by the template (default is all)
let latest_3_releases: Vec<Release> = get_latest_3_releases(&repo)
.await?
.into_iter()
.filter(|r| template.is_supported_version(&r.tag_name))
.collect();

let mut release_name = None;
if latest_3_releases.len() > 0 {
release_name = Some(display_release_versions_to_user(latest_3_releases)?);
} else {
// If supported_versions exists and no other releases are found,
// then the default branch is not supported and an error is returned
let _ = template.supported_versions().is_some()
&& Err(anyhow::anyhow!(
"No supported versions found for this template. Please open an issue here: https://github.com/r0gue-io/pop-cli/issues "
))?;

warning("No releases found for this template. Will use the default branch")?;
}

Ok(release_name)
}

async fn get_latest_3_releases(repo: &GitHub) -> Result<Vec<Release>> {
let mut latest_3_releases: Vec<Release> = repo
.get_latest_releases()
.await?
.into_iter()
.filter(|r| !r.prerelease)
.take(3)
.collect();
repo.get_repo_license().await?;
// Get the commit sha for the releases
for release in latest_3_releases.iter_mut() {
let commit = repo.get_commit_sha_from_release(&release.tag_name).await?;
Expand Down
77 changes: 77 additions & 0 deletions crates/pop-parachains/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ pub enum Template {
)
)]
ParityFPT,

// templates for unit tests below
#[cfg(test)]
#[strum(
serialize = "test_01",
message = "Test_01",
detailed_message = "Test template only compiled in test mode.",
props(
Provider = "Test",
Repository = "",
Network = "",
SupportedVersions = "v1.0.0,v2.0.0",
IsAudited = "true"
)
)]
TestTemplate01,
#[cfg(test)]
#[strum(
serialize = "test_02",
message = "Test_02",
detailed_message = "Test template only compiled in test mode.",
props(Provider = "Test", Repository = "", Network = "",)
)]
TestTemplate02,
}

impl Template {
Expand Down Expand Up @@ -190,6 +214,19 @@ impl Template {
pub fn network_config(&self) -> Option<&str> {
self.get_str("Network")
}

pub fn supported_versions(&self) -> Option<Vec<&str>> {
self.get_str("SupportedVersions").map(|s| s.split(',').collect())
}

pub fn is_supported_version(&self, version: &str) -> bool {
// if `SupportedVersion` is None, then all versions are supported. Otherwise, ensure version is present.
self.supported_versions().map_or(true, |versions| versions.contains(&version))
}

pub fn is_audited(&self) -> bool {
self.get_str("IsAudited").map_or(false, |s| s == "true")
}
}

#[derive(Error, Debug)]
Expand All @@ -213,6 +250,8 @@ mod tests {
("evm".to_string(), Template::EVM),
("cpt".to_string(), Template::ParityContracts),
("fpt".to_string(), Template::ParityFPT),
("test_01".to_string(), Template::TestTemplate01),
("test_02".to_string(), Template::TestTemplate02),
])
}

Expand All @@ -224,6 +263,8 @@ mod tests {
("evm".to_string(), "https://github.com/r0gue-io/evm-parachain"),
("cpt".to_string(), "https://github.com/paritytech/substrate-contracts-node"),
("fpt".to_string(), "https://github.com/paritytech/frontier-parachain-template"),
("test_01".to_string(), ""),
("test_02".to_string(), ""),
])
}

Expand All @@ -235,6 +276,8 @@ mod tests {
(Template::EVM, Some("./network.toml")),
(Template::ParityContracts, Some("./zombienet.toml")),
(Template::ParityFPT, Some("./zombienet-config.toml")),
(Template::TestTemplate01, Some("")),
(Template::TestTemplate02, Some("")),
]
.into()
}
Expand Down Expand Up @@ -314,4 +357,38 @@ mod tests {
assert_eq!(Provider::from_str("").unwrap_or_default(), Provider::Pop);
assert_eq!(Provider::from_str("Parity").unwrap(), Provider::Parity);
}

#[test]
fn supported_versions_have_no_whitespace() {
for template in Template::VARIANTS {
if let Some(versions) = template.supported_versions() {
for version in versions {
assert!(!version.contains(' '));
}
}
}
}

#[test]
fn test_supported_versions_works() {
let template = Template::TestTemplate01;
assert_eq!(template.supported_versions(), Some(vec!["v1.0.0", "v2.0.0"]));
assert_eq!(template.is_supported_version("v1.0.0"), true);
assert_eq!(template.is_supported_version("v2.0.0"), true);
assert_eq!(template.is_supported_version("v3.0.0"), false);

let template = Template::TestTemplate02;
assert_eq!(template.supported_versions(), None);
// will be true because an empty SupportedVersions defaults to all
assert_eq!(template.is_supported_version("v1.0.0"), true);
}

#[test]
fn test_is_audited() {
let template = Template::TestTemplate01;
assert_eq!(template.is_audited(), true);

let template = Template::TestTemplate02;
assert_eq!(template.is_audited(), false);
}
}
60 changes: 60 additions & 0 deletions crates/pop-parachains/src/utils/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ impl GitHub {
Ok(commit)
}

pub async fn get_repo_license(&self) -> Result<String> {
static APP_USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
let client = reqwest::ClientBuilder::new().user_agent(APP_USER_AGENT).build()?;
let url = self.api_license_url();
let response = client.get(url).send().await?;
let value = response.json::<serde_json::Value>().await?;
let license = value
.get("license")
.and_then(|v| v.get("spdx_id"))
.and_then(|v| v.as_str())
.map(|v| v.to_owned())
.ok_or(Error::Git("Unable to find license for GitHub repo".to_string()))?;
Ok(license)
}

fn api_releases_url(&self) -> String {
format!("{}/repos/{}/{}/releases", self.api, self.org, self.name)
}
Expand All @@ -221,6 +237,10 @@ impl GitHub {
format!("{}/repos/{}/{}/git/ref/tags/{}", self.api, self.org, self.name, tag_name)
}

fn api_license_url(&self) -> String {
format!("{}/repos/{}/{}/license", self.api, self.org, self.name)
}

fn org(repo: &Url) -> Result<&str> {
let path_segments = repo
.path_segments()
Expand Down Expand Up @@ -287,6 +307,16 @@ mod tests {
.await
}

async fn license_mock(mock_server: &mut Server, repo: &GitHub, payload: &str) -> Mock {
mock_server
.mock("GET", format!("/repos/{}/{}/license", repo.org, repo.name).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(payload)
.create_async()
.await
}

#[tokio::test]
async fn test_get_latest_releases() -> Result<(), Box<dyn std::error::Error>> {
let mut mock_server = Server::new_async().await;
Expand Down Expand Up @@ -334,6 +364,27 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn get_repo_license() -> Result<(), Box<dyn std::error::Error>> {
let mut mock_server = Server::new_async().await;

let expected_payload = r#"{
"license": {
"key":"unlicense",
"name":"The Unlicense",
"spdx_id":"Unlicense",
"url":"https://api.github.com/licenses/unlicense",
"node_id":"MDc6TGljZW5zZTE1"
}
}"#;
let repo = GitHub::parse(BASE_PARACHAIN)?.with_api(&mock_server.url());
let mock = license_mock(&mut mock_server, &repo, expected_payload).await;
let license = repo.get_repo_license().await?;
assert_eq!(license, "Unlicense".to_string());
mock.assert_async().await;
Ok(())
}

#[test]
fn test_get_releases_api_url() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
Expand All @@ -352,6 +403,15 @@ mod tests {
Ok(())
}

#[test]
fn test_api_license_url() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
GitHub::parse(POLKADOT_SDK)?.api_license_url(),
"https://api.github.com/repos/paritytech/polkadot-sdk/license"
);
Ok(())
}

#[test]
fn test_parse_org() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(GitHub::parse(BASE_PARACHAIN)?.org, "r0gue-io");
Expand Down

0 comments on commit 7135ef5

Please sign in to comment.