wasmer_cli/commands/package/
push.rs

1use super::common::{macros::*, *};
2use crate::{
3    commands::{AsyncCliCommand, PackageBuild},
4    config::WasmerEnv,
5};
6use anyhow::Context;
7use colored::Colorize;
8use is_terminal::IsTerminal;
9use std::path::{Path, PathBuf};
10use wasmer_backend_api::WasmerClient;
11use wasmer_config::package::{Manifest, PackageHash};
12use wasmer_package::package::Package;
13
14/// Push a package to the registry.
15///
16/// The result of this operation is that the hash of the package can be used to reference the
17/// pushed package.
18#[derive(Debug, clap::Parser)]
19pub struct PackagePush {
20    #[clap(flatten)]
21    pub env: WasmerEnv,
22
23    /// Run the publish logic without sending anything to the registry server
24    #[clap(long, name = "dry-run")]
25    pub dry_run: bool,
26
27    /// Run the publish command without any output
28    #[clap(long)]
29    pub quiet: bool,
30
31    /// Override the namespace of the package to upload
32    #[clap(long = "namespace")]
33    pub package_namespace: Option<String>,
34
35    /// Override the name of the package to upload. If a name is specified,
36    /// no version will be attached to the package.
37    #[clap(long = "name")]
38    pub package_name: Option<String>,
39
40    /// Timeout (in seconds) for the publish query to the registry.
41    ///
42    /// Note that this is not the timeout for the entire publish process, but
43    /// for each individual query to the registry during the publish flow.
44    #[clap(long, default_value = "5m")]
45    pub timeout: humantime::Duration,
46
47    /// Do not prompt for user input.
48    #[clap(long, default_value_t = !std::io::stdin().is_terminal())]
49    pub non_interactive: bool,
50
51    /// Directory containing the `wasmer.toml`, or a custom *.toml manifest file.
52    ///
53    /// Defaults to current working directory.
54    #[clap(name = "path", default_value = ".")]
55    pub package_path: PathBuf,
56}
57
58impl PackagePush {
59    async fn get_namespace(
60        &self,
61        client: &WasmerClient,
62        manifest: &Manifest,
63    ) -> anyhow::Result<String> {
64        if let Some(owner) = &self.package_namespace {
65            return Ok(owner.clone());
66        }
67
68        if let Some(pkg) = &manifest.package {
69            if let Some(ns) = &pkg.name {
70                if let Some(first) = ns.split('/').next() {
71                    return Ok(first.to_string());
72                }
73            }
74        }
75
76        if self.non_interactive {
77            // if not interactive we can't prompt the user to choose the owner of the app.
78            anyhow::bail!("No package namespace specified: use --namespace XXX");
79        }
80
81        let user = wasmer_backend_api::query::current_user_with_namespaces(client, None).await?;
82        let owner = crate::utils::prompts::prompt_for_namespace(
83            "Choose a namespace to push the package to",
84            None,
85            Some(&user),
86        )?;
87
88        Ok(owner.clone())
89    }
90
91    async fn get_name(&self, manifest: &Manifest) -> anyhow::Result<Option<String>> {
92        if let Some(name) = &self.package_name {
93            return Ok(Some(name.clone()));
94        }
95
96        if let Some(pkg) = &manifest.package {
97            if let Some(ns) = &pkg.name {
98                if let Some(name) = ns.split('/').nth(1) {
99                    return Ok(Some(name.to_string()));
100                }
101            }
102        }
103
104        Ok(None)
105    }
106
107    fn get_privacy(&self, manifest: &Manifest) -> bool {
108        match &manifest.package {
109            Some(pkg) => pkg.private,
110            None => true,
111        }
112    }
113
114    async fn should_push(&self, client: &WasmerClient, hash: &PackageHash) -> anyhow::Result<bool> {
115        let res = wasmer_backend_api::query::get_package_release(client, &hash.to_string()).await;
116        tracing::info!("{:?}", res);
117        res.map(|p| p.is_none())
118    }
119
120    async fn do_push(
121        &self,
122        client: &WasmerClient,
123        namespace: &str,
124        name: Option<String>,
125        package: &Package,
126        package_hash: &PackageHash,
127        private: bool,
128    ) -> anyhow::Result<()> {
129        let pb = make_spinner!(self.quiet, "Uploading the package..");
130
131        let signed_url = upload(
132            client,
133            package_hash,
134            self.timeout,
135            package,
136            pb.clone(),
137            self.env.proxy()?,
138        )
139        .await?;
140        spinner_ok!(pb, "Package correctly uploaded");
141
142        let pb = make_spinner!(self.quiet, "Waiting for package to become available...");
143        match wasmer_backend_api::query::push_package_release(
144            client,
145            name.as_deref(),
146            namespace,
147            &signed_url,
148            Some(private),
149        )
150        .await?
151        {
152            Some(r) => {
153                if r.success {
154                    r.package_webc.unwrap().id
155                } else {
156                    anyhow::bail!(
157                        "An unidentified error occurred while publishing the package. (response had success: false)"
158                    )
159                }
160            }
161            None => anyhow::bail!("An unidentified error occurred while publishing the package."), // <- This is extremely bad..
162        };
163
164        let msg = format!("Succesfully pushed release to namespace {namespace} on the registry");
165        spinner_ok!(pb, msg);
166
167        Ok(())
168    }
169
170    pub async fn push(
171        &self,
172        client: &WasmerClient,
173        manifest: &Manifest,
174        manifest_path: &Path,
175    ) -> anyhow::Result<(String, PackageHash)> {
176        tracing::info!("Building package");
177        let pb = make_spinner!(self.quiet, "Creating the package locally...");
178        let (package, hash) = PackageBuild::check(manifest_path.to_path_buf())
179            .execute()
180            .context("While trying to build the package locally")?;
181
182        spinner_ok!(pb, "Correctly built package locally");
183        tracing::info!("Package has hash: {hash}");
184
185        let namespace = self.get_namespace(client, manifest).await?;
186        let name = self.get_name(manifest).await?;
187
188        let private = self.get_privacy(manifest);
189        tracing::info!(
190            "If published, package privacy is {private}, namespace is {namespace} and name is {name:?}"
191        );
192
193        let pb = make_spinner!(
194            self.quiet,
195            "Checking if package is already in the registry.."
196        );
197        if self.should_push(client, &hash).await.map_err(on_error)? {
198            if !self.dry_run {
199                tracing::info!("Package should be published");
200                pb.finish_and_clear();
201                // spinner_ok!(pb, "Package not in the registry yet!");
202
203                self.do_push(client, &namespace, name, &package, &hash, private)
204                    .await
205                    .map_err(on_error)?;
206            } else {
207                tracing::info!("Package should be published, but dry-run is set");
208                spinner_ok!(pb, "Skipping push as dry-run is set");
209            }
210        } else {
211            tracing::info!("Package should not be published");
212            spinner_ok!(pb, "Package was already in the registry, no push needed");
213        }
214
215        tracing::info!("Proceeding to invalidate query cache..");
216
217        // Prevent `wasmer run` from using stale (cached) package versions after wasmer publish.
218        if let Err(e) = invalidate_graphql_query_cache(&self.env.cache_dir) {
219            tracing::warn!(
220                error = &*e,
221                "Unable to invalidate the cache used for package version queries",
222            );
223        }
224
225        Ok((namespace, hash))
226    }
227}
228
229#[async_trait::async_trait]
230impl AsyncCliCommand for PackagePush {
231    type Output = ();
232
233    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
234        tracing::info!("Checking if user is logged in");
235        let client = login_user(&self.env, !self.non_interactive, "push a package").await?;
236
237        tracing::info!("Loading manifest");
238        let (manifest_path, manifest) = get_manifest(&self.package_path)?;
239        tracing::info!("Got manifest at path {}", manifest_path.display());
240
241        let (_, hash) = self.push(&client, &manifest, &manifest_path).await?;
242
243        let bin_name = bin_name!();
244        if let Some(package) = &manifest.package {
245            if package.name.is_some() {
246                let mut manifest_path_dir = manifest_path.clone();
247                manifest_path_dir.pop();
248
249                eprintln!(
250                    "{} You can now tag your package with `{}`",
251                    "ð–¥”".yellow().bold(),
252                    format!(
253                        "{bin_name} package tag {}{}",
254                        hash,
255                        if manifest_path_dir.canonicalize()? == std::env::current_dir()? {
256                            String::new()
257                        } else {
258                            format!(" {}", manifest_path_dir.display())
259                        }
260                    )
261                    .bold()
262                )
263            } else {
264                eprintln!("{} Succesfully pushed package ({hash})", "✔".green().bold());
265            }
266        } else {
267            eprintln!("{} Succesfully pushed package ({hash})", "✔".green().bold());
268        }
269
270        Ok(())
271    }
272}