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 std::io::IsTerminal as _;
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            && let Some(ns) = &pkg.name
70            && let Some(first) = ns.split('/').next()
71        {
72            return Ok(first.to_string());
73        }
74
75        if self.non_interactive {
76            // if not interactive we can't prompt the user to choose the owner of the app.
77            anyhow::bail!("No package namespace specified: use --namespace XXX");
78        }
79
80        let user = wasmer_backend_api::query::current_user_with_namespaces(client, None).await?;
81        let owner = crate::utils::prompts::prompt_for_namespace(
82            "Choose a namespace to push the package to",
83            None,
84            Some(&user),
85        )?;
86
87        Ok(owner.clone())
88    }
89
90    async fn get_name(&self, manifest: &Manifest) -> anyhow::Result<Option<String>> {
91        if let Some(name) = &self.package_name {
92            return Ok(Some(name.clone()));
93        }
94
95        if let Some(pkg) = &manifest.package
96            && let Some(ns) = &pkg.name
97            && let Some(name) = ns.split('/').nth(1)
98        {
99            return Ok(Some(name.to_string()));
100        }
101
102        Ok(None)
103    }
104
105    fn get_privacy(&self, manifest: &Manifest) -> bool {
106        match &manifest.package {
107            Some(pkg) => pkg.private,
108            None => true,
109        }
110    }
111
112    async fn should_push(&self, client: &WasmerClient, hash: &PackageHash) -> anyhow::Result<bool> {
113        let res = wasmer_backend_api::query::get_package_release(client, &hash.to_string()).await;
114        tracing::info!("{:?}", res);
115        res.map(|p| p.is_none())
116    }
117
118    async fn do_push(
119        &self,
120        client: &WasmerClient,
121        namespace: &str,
122        name: Option<String>,
123        package: &Package,
124        package_hash: &PackageHash,
125        private: bool,
126    ) -> anyhow::Result<()> {
127        let pb = make_spinner!(self.quiet, "Uploading the package..");
128
129        let signed_url = upload(
130            client,
131            package_hash,
132            self.timeout,
133            package,
134            pb.clone(),
135            self.env.proxy()?,
136        )
137        .await?;
138        spinner_ok!(pb, "Package correctly uploaded");
139
140        let pb = make_spinner!(self.quiet, "Waiting for package to become available...");
141        match wasmer_backend_api::query::push_package_release(
142            client,
143            name.as_deref(),
144            namespace,
145            &signed_url,
146            Some(private),
147        )
148        .await?
149        {
150            Some(r) => {
151                if r.success {
152                    r.package_webc.unwrap().id
153                } else {
154                    anyhow::bail!(
155                        "An unidentified error occurred while publishing the package. (response had success: false)"
156                    )
157                }
158            }
159            None => anyhow::bail!("An unidentified error occurred while publishing the package."), // <- This is extremely bad..
160        };
161
162        let msg = format!("Succesfully pushed release to namespace {namespace} on the registry");
163        spinner_ok!(pb, msg);
164
165        Ok(())
166    }
167
168    pub async fn push(
169        &self,
170        client: &WasmerClient,
171        manifest: &Manifest,
172        manifest_path: &Path,
173    ) -> anyhow::Result<(String, PackageHash)> {
174        tracing::info!("Building package");
175        let pb = make_spinner!(self.quiet, "Creating the package locally...");
176        let (package, hash) = PackageBuild::check(manifest_path.to_path_buf())
177            .execute()
178            .context("While trying to build the package locally")?;
179
180        spinner_ok!(pb, "Correctly built package locally");
181        tracing::info!("Package has hash: {hash}");
182
183        let namespace = self.get_namespace(client, manifest).await?;
184        let name = self.get_name(manifest).await?;
185
186        let private = self.get_privacy(manifest);
187        tracing::info!(
188            "If published, package privacy is {private}, namespace is {namespace} and name is {name:?}"
189        );
190
191        let pb = make_spinner!(
192            self.quiet,
193            "Checking if package is already in the registry.."
194        );
195        if self.should_push(client, &hash).await.map_err(on_error)? {
196            if !self.dry_run {
197                tracing::info!("Package should be published");
198                pb.finish_and_clear();
199                // spinner_ok!(pb, "Package not in the registry yet!");
200
201                self.do_push(client, &namespace, name, &package, &hash, private)
202                    .await
203                    .map_err(on_error)?;
204            } else {
205                tracing::info!("Package should be published, but dry-run is set");
206                spinner_ok!(pb, "Skipping push as dry-run is set");
207            }
208        } else {
209            tracing::info!("Package should not be published");
210            spinner_ok!(pb, "Package was already in the registry, no push needed");
211        }
212
213        tracing::info!("Proceeding to invalidate query cache..");
214
215        // Prevent `wasmer run` from using stale (cached) package versions after wasmer publish.
216        if let Err(e) = invalidate_graphql_query_cache(&self.env.cache_dir) {
217            tracing::warn!(
218                error = &*e,
219                "Unable to invalidate the cache used for package version queries",
220            );
221        }
222
223        Ok((namespace, hash))
224    }
225}
226
227#[async_trait::async_trait]
228impl AsyncCliCommand for PackagePush {
229    type Output = ();
230
231    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
232        tracing::info!("Checking if user is logged in");
233        let client = login_user(&self.env, !self.non_interactive, "push a package").await?;
234
235        tracing::info!("Loading manifest");
236        let (manifest_path, manifest) = get_manifest(&self.package_path)?;
237        tracing::info!("Got manifest at path {}", manifest_path.display());
238
239        let (_, hash) = self.push(&client, &manifest, &manifest_path).await?;
240
241        let bin_name = bin_name!();
242        if let Some(package) = &manifest.package {
243            if package.name.is_some() {
244                let mut manifest_path_dir = manifest_path.clone();
245                manifest_path_dir.pop();
246
247                eprintln!(
248                    "{} You can now tag your package with `{}`",
249                    "ð–¥”".yellow().bold(),
250                    format!(
251                        "{bin_name} package tag {}{}",
252                        hash,
253                        if manifest_path_dir.canonicalize()? == std::env::current_dir()? {
254                            String::new()
255                        } else {
256                            format!(" {}", manifest_path_dir.display())
257                        }
258                    )
259                    .bold()
260                )
261            } else {
262                eprintln!("{} Succesfully pushed package ({hash})", "✔".green().bold());
263            }
264        } else {
265            eprintln!("{} Succesfully pushed package ({hash})", "✔".green().bold());
266        }
267
268        Ok(())
269    }
270}