Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --wait and --timeout flags to wamer publish #4328

Merged
merged 10 commits into from
Nov 28, 2023
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions lib/cli/src/commands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ pub struct Publish {
/// Defaults to current working directory.
#[clap(name = "PACKAGE_PATH")]
pub package_path: Option<String>,
/// 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 {
Expand All @@ -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)?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mutation PublishPackageMutationChunked(
$signature: InputSignature
$signedUrl: String
$private: Boolean
$wait: Boolean
) {
publishPackage(
input: {
Expand All @@ -29,6 +30,7 @@ mutation PublishPackageMutationChunked(
signature: $signature
clientMutationId: ""
private: $private
wait: $wait
}
) {
success
Expand Down
7 changes: 7 additions & 0 deletions lib/registry/src/package/builder.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<String>,
/// 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)]
Expand Down Expand Up @@ -186,6 +191,8 @@ impl Publish {
&maybe_signature_data,
archived_data_size,
self.quiet,
self.wait,
self.timeout,
)
}

Expand Down
135 changes: 90 additions & 45 deletions lib/registry/src/publish.rs
Original file line number Diff line number Diff line change
@@ -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},
};
Expand Down Expand Up @@ -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(&registry, &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(&registry, &token, timeout, &q)?;

println!(
"Successfully published package `{}@{}`",
package.name, package.version
);

Ok(())
}

fn initialize_registry_and_token(
registry: Option<String>,
token: Option<String>,
) -> Result<(String, String), anyhow::Error> {
let registry = match registry.as_ref() {
Some(s) => format_graphql(s),
None => {
Expand Down Expand Up @@ -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<publish_package_mutation_chunked::InputSignature> {
match maybe_signature_data {
SignArchiveResult::Ok {
public_key_id,
signature,
Expand All @@ -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<GetSignedUrlUrl, anyhow::Error> {
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(&registry, &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!(
Expand All @@ -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()
Michael-F-Bryan marked this conversation as resolved.
Show resolved Hide resolved
.unwrap();

Expand Down Expand Up @@ -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(&registry, &token, &q)?;

println!(
"Successfully published package `{}@{}`",
package.name, package.version
);

Ok(())
}
62 changes: 62 additions & 0 deletions tests/integration/cli/tests/publish.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use assert_cmd::prelude::OutputAssertExt;
use predicates::str::contains;
use wasmer_integration_tests_cli::{fixtures, get_wasmer_path};

#[test]
Expand Down Expand Up @@ -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::<u32>());
let random_minor = format!("{}", rand::random::<u32>());
let random_patch = format!("{}", rand::random::<u32>());

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!"));
}
Loading