diff --git a/Cargo.lock b/Cargo.lock index 863abecadff..ab420c26af3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2018,6 +2018,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -5688,6 +5694,7 @@ dependencies = [ "futures", "hex", "http", + "humantime", "hyper", "indexmap 1.9.3", "indicatif", diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index f000e756315..d4f73c82d30 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -115,6 +115,7 @@ opener = "0.6.1" hyper = { version = "0.14.27", features = ["server"] } http = "0.2.9" futures = "0.3.29" +humantime = "2.1.0" # NOTE: Must use different features for clap because the "color" feature does not # work on wasi due to the anstream dependency not compiling. diff --git a/lib/cli/src/commands/publish.rs b/lib/cli/src/commands/publish.rs index 46de87fb12f..d2d83eecd51 100644 --- a/lib/cli/src/commands/publish.rs +++ b/lib/cli/src/commands/publish.rs @@ -27,6 +27,15 @@ pub struct Publish { /// Defaults to current working directory. #[clap(name = "PACKAGE_PATH")] pub package_path: Option, + /// Wait for package to be available on the registry before exiting. + #[clap(long)] + pub wait: bool, + /// Timeout (in seconds) for the publish query to the registry. + /// + /// Note that this is not the timeout for the entire publish process, but + /// for each individual query to the registry during the publish flow. + #[clap(long, default_value = "30s")] + pub timeout: humantime::Duration, } impl Publish { @@ -46,6 +55,8 @@ impl Publish { token, no_validate: self.no_validate, package_path: self.package_path.clone(), + wait: self.wait, + timeout: self.timeout.into(), }; publish.execute().map_err(on_error)?; diff --git a/lib/registry/graphql/mutations/publish_package_chunked.graphql b/lib/registry/graphql/mutations/publish_package_chunked.graphql index 76a1c67a7c0..398b4fbbaf4 100644 --- a/lib/registry/graphql/mutations/publish_package_chunked.graphql +++ b/lib/registry/graphql/mutations/publish_package_chunked.graphql @@ -12,6 +12,7 @@ mutation PublishPackageMutationChunked( $signature: InputSignature $signedUrl: String $private: Boolean + $wait: Boolean ) { publishPackage( input: { @@ -29,6 +30,7 @@ mutation PublishPackageMutationChunked( signature: $signature clientMutationId: "" private: $private + wait: $wait } ) { success diff --git a/lib/registry/src/package/builder.rs b/lib/registry/src/package/builder.rs index 544b403e2e6..5c8ab7074b1 100644 --- a/lib/registry/src/package/builder.rs +++ b/lib/registry/src/package/builder.rs @@ -1,5 +1,6 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; +use std::time::Duration; use std::{fs, io::IsTerminal}; use anyhow::{anyhow, bail, Context}; @@ -38,6 +39,10 @@ pub struct Publish { pub no_validate: bool, /// Directory containing the `wasmer.toml` (defaults to current root dir) pub package_path: Option, + /// Wait for package to be available on the registry before exiting + pub wait: bool, + /// Timeout (in seconds) for the publish query to the registry + pub timeout: Duration, } #[derive(Debug, Error)] @@ -186,6 +191,8 @@ impl Publish { &maybe_signature_data, archived_data_size, self.quiet, + self.wait, + self.timeout, ) } diff --git a/lib/registry/src/publish.rs b/lib/registry/src/publish.rs index 1f69ef0741f..126604e201e 100644 --- a/lib/registry/src/publish.rs +++ b/lib/registry/src/publish.rs @@ -1,14 +1,14 @@ -use std::collections::BTreeMap; -use std::fmt::Write; -use std::io::BufRead; -use std::path::PathBuf; - +use anyhow::Context; use console::{style, Emoji}; use graphql_client::GraphQLQuery; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; +use std::fmt::Write; +use std::io::BufRead; +use std::path::PathBuf; +use std::{collections::BTreeMap, time::Duration}; +use crate::graphql::queries::get_signed_url::GetSignedUrlUrl; use crate::graphql::{ - execute_query_modifier_inner, mutations::{publish_package_mutation_chunked, PublishPackageMutationChunked}, queries::{get_signed_url, GetSignedUrl}, }; @@ -39,7 +39,61 @@ pub fn try_chunked_uploading( maybe_signature_data: &SignArchiveResult, archived_data_size: u64, quiet: bool, + wait: bool, + timeout: Duration, ) -> Result<(), anyhow::Error> { + let (registry, token) = initialize_registry_and_token(registry, token)?; + + let maybe_signature_data = sign_package(maybe_signature_data); + + // fetch this before showing the `Uploading...` message + // because there is a chance that the registry may not return a signed url. + // This usually happens if the package version already exists in the registry. + let signed_url = google_signed_url(®istry, &token, package, timeout)?; + + if !quiet { + println!("{} {} Uploading...", style("[1/2]").bold().dim(), UPLOAD); + } + + upload_package(&signed_url.url, archive_path, archived_data_size, timeout)?; + + if !quiet { + println!("{} {}Publishing...", style("[2/2]").bold().dim(), PACKAGE); + } + + let q = + PublishPackageMutationChunked::build_query(publish_package_mutation_chunked::Variables { + name: package.name.to_string(), + version: package.version.to_string(), + description: package.description.clone(), + manifest: manifest_string.to_string(), + license: package.license.clone(), + license_file: license_file.to_owned(), + readme: readme.to_owned(), + repository: package.repository.clone(), + homepage: package.homepage.clone(), + file_name: Some(archive_name.to_string()), + signature: maybe_signature_data, + signed_url: Some(signed_url.url), + private: Some(package.private), + wait: Some(wait), + }); + + let _response: publish_package_mutation_chunked::ResponseData = + crate::graphql::execute_query_with_timeout(®istry, &token, timeout, &q)?; + + println!( + "Successfully published package `{}@{}`", + package.name, package.version + ); + + Ok(()) +} + +fn initialize_registry_and_token( + registry: Option, + token: Option, +) -> Result<(String, String), anyhow::Error> { let registry = match registry.as_ref() { Some(s) => format_graphql(s), None => { @@ -71,7 +125,13 @@ pub fn try_chunked_uploading( } }; - let maybe_signature_data = match maybe_signature_data { + Ok((registry, token)) +} + +fn sign_package( + maybe_signature_data: &SignArchiveResult, +) -> Option { + match maybe_signature_data { SignArchiveResult::Ok { public_key_id, signature, @@ -90,20 +150,27 @@ pub fn try_chunked_uploading( //warn!("Publishing package without a verifying signature. Consider registering a key pair with wasmer"); None } - }; - - if !quiet { - println!("{} {} Uploading...", style("[1/2]").bold().dim(), UPLOAD); } +} +fn google_signed_url( + registry: &str, + token: &str, + package: &wasmer_toml::Package, + timeout: Duration, +) -> Result { let get_google_signed_url = GetSignedUrl::build_query(get_signed_url::Variables { name: package.name.to_string(), version: package.version.to_string(), expires_after_seconds: Some(60 * 30), }); - let _response: get_signed_url::ResponseData = - execute_query_modifier_inner(®istry, &token, &get_google_signed_url, None, |f| f)?; + let _response: get_signed_url::ResponseData = crate::graphql::execute_query_with_timeout( + registry, + token, + timeout, + &get_google_signed_url, + )?; let url = _response.url.ok_or_else(|| { anyhow::anyhow!( @@ -112,11 +179,19 @@ pub fn try_chunked_uploading( package.version ) })?; + Ok(url) +} - let signed_url = url.url; - let url = url::Url::parse(&signed_url).unwrap(); +fn upload_package( + signed_url: &str, + archive_path: &PathBuf, + archived_data_size: u64, + timeout: Duration, +) -> Result<(), anyhow::Error> { + let url = url::Url::parse(signed_url).context("cannot parse signed url")?; let client = reqwest::blocking::Client::builder() .default_headers(reqwest::header::HeaderMap::default()) + .timeout(timeout) .build() .unwrap(); @@ -212,35 +287,5 @@ pub fn try_chunked_uploading( } pb.finish_and_clear(); - - if !quiet { - println!("{} {}Publishing...", style("[2/2]").bold().dim(), PACKAGE); - } - - let q = - PublishPackageMutationChunked::build_query(publish_package_mutation_chunked::Variables { - name: package.name.to_string(), - version: package.version.to_string(), - description: package.description.clone(), - manifest: manifest_string.to_string(), - license: package.license.clone(), - license_file: license_file.to_owned(), - readme: readme.to_owned(), - repository: package.repository.clone(), - homepage: package.homepage.clone(), - file_name: Some(archive_name.to_string()), - signature: maybe_signature_data, - signed_url: Some(signed_url), - private: Some(package.private), - }); - - let _response: publish_package_mutation_chunked::ResponseData = - crate::graphql::execute_query(®istry, &token, &q)?; - - println!( - "Successfully published package `{}@{}`", - package.name, package.version - ); - Ok(()) } diff --git a/tests/integration/cli/tests/publish.rs b/tests/integration/cli/tests/publish.rs index 1b583467c51..b34ceae1974 100644 --- a/tests/integration/cli/tests/publish.rs +++ b/tests/integration/cli/tests/publish.rs @@ -1,4 +1,5 @@ use assert_cmd::prelude::OutputAssertExt; +use predicates::str::contains; use wasmer_integration_tests_cli::{fixtures, get_wasmer_path}; #[test] @@ -118,3 +119,64 @@ fn wasmer_init_publish() { "Successfully published package `{username}/randomversion@{random1}.{random2}.{random3}`\n" )); } + +#[test] +fn wasmer_publish_and_run() { + // Only run this test in the CI + if std::env::var("GITHUB_TOKEN").is_err() { + return; + } + + let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").ok(); + let tempdir = tempfile::tempdir().unwrap(); + let path = tempdir.path(); + let username = "ciuser"; + + let random_major = format!("{}", rand::random::()); + let random_minor = format!("{}", rand::random::()); + let random_patch = format!("{}", rand::random::()); + + std::fs::copy(fixtures::qjs(), path.join("largewasmfile.wasm")).unwrap(); + std::fs::write( + path.join("wasmer.toml"), + include_str!("./fixtures/init6.toml") + .replace("WAPMUSERNAME", username) // <-- TODO! + .replace("RANDOMVERSION1", &random_major) + .replace("RANDOMVERSION2", &random_minor) + .replace("RANDOMVERSION3", &random_patch), + ) + .unwrap(); + + let package_name = + format!("{username}/largewasmfile@{random_major}.{random_minor}.{random_patch}",); + + let mut cmd = std::process::Command::new(get_wasmer_path()); + cmd.arg("publish") + .arg("--quiet") + .arg("--wait") + .arg("--timeout=60s") + .arg("--registry=wasmer.wtf") + .arg(path); + + if let Some(token) = wapm_dev_token { + // Special case: GitHub secrets aren't visible to outside collaborators + if token.is_empty() { + return; + } + cmd.arg("--token").arg(token); + } + + cmd.assert() + .success() + .stdout(format!("Successfully published package `{package_name}`\n")); + + let assert = std::process::Command::new(get_wasmer_path()) + .arg("run") + .arg(format!("https://wasmer.wtf/{package_name}")) + .arg("--") + .arg("--eval") + .arg("console.log('Hello, World!')") + .assert(); + + assert.success().stdout(contains("Hello, World!")); +}