warg_cli/commands/
publish.rs

1use super::CommonOptions;
2use anyhow::{anyhow, bail, Context, Result};
3use clap::{Args, Subcommand};
4use dialoguer::{theme::ColorfulTheme, Confirm};
5use futures::TryStreamExt;
6use itertools::Itertools;
7use std::{future::Future, path::PathBuf, time::Duration};
8use tokio::io::BufReader;
9use tokio_util::io::ReaderStream;
10use warg_client::{
11    storage::{ContentStorage as _, PublishEntry, PublishInfo, RegistryStorage as _},
12    FileSystemClient,
13};
14use warg_crypto::{
15    hash::AnyHash,
16    signing::{KeyID, PublicKey},
17};
18use warg_protocol::{
19    package::Permission,
20    registry::{PackageName, RecordId},
21    Version,
22};
23
24const DEFAULT_WAIT_INTERVAL: Duration = Duration::from_secs(1);
25
26/// Used to enqueue a publish entry if there is a pending publish.
27/// Returns `Ok(None)` if the entry was enqueued or `Ok(Some(entry))` if there
28/// was no pending publish.
29async fn enqueue<'a, T>(
30    client: &'a FileSystemClient,
31    name: &PackageName,
32    entry: impl FnOnce(&'a FileSystemClient) -> T,
33) -> Result<Option<PublishEntry>>
34where
35    T: Future<Output = Result<PublishEntry>> + 'a,
36{
37    match client.registry().load_publish().await? {
38        Some(mut info) => {
39            if &info.name != name {
40                bail!(
41                    "there is already publish in progress for package `{name}`",
42                    name = info.name
43                );
44            }
45
46            let entry = entry(client).await?;
47
48            if matches!(entry, PublishEntry::Init) && info.initializing() {
49                bail!("there is already a pending initializing for package `{name}`");
50            }
51
52            info.entries.push(entry);
53            client.registry().store_publish(Some(&info)).await?;
54            Ok(None)
55        }
56        None => Ok(Some(entry(client).await?)),
57    }
58}
59
60/// Publish a package to a warg registry.
61#[derive(Subcommand)]
62pub enum PublishCommand {
63    /// Initialize a new package.
64    Init(PublishInitCommand),
65    /// Release a package version.
66    Release(PublishReleaseCommand),
67    /// Yank a package version.
68    Yank(PublishYankCommand),
69    /// Grant permissions for the package.
70    Grant(PublishGrantCommand),
71    /// Revoke permissions for the package.
72    Revoke(PublishRevokeCommand),
73    /// Start a new pending publish.
74    Start(PublishStartCommand),
75    /// List the records in a pending publish.
76    List(PublishListCommand),
77    /// Abort a pending publish.
78    Abort(PublishAbortCommand),
79    /// Submit a pending publish.
80    Submit(PublishSubmitCommand),
81    /// Wait for a pending publish to complete.
82    Wait(PublishWaitCommand),
83}
84
85impl PublishCommand {
86    /// Executes the command.
87    pub async fn exec(self) -> Result<()> {
88        match self {
89            Self::Init(cmd) => cmd.exec().await,
90            Self::Release(cmd) => cmd.exec().await,
91            Self::Yank(cmd) => cmd.exec().await,
92            Self::Grant(cmd) => cmd.exec().await,
93            Self::Revoke(cmd) => cmd.exec().await,
94            Self::Start(cmd) => cmd.exec().await,
95            Self::List(cmd) => cmd.exec().await,
96            Self::Abort(cmd) => cmd.exec().await,
97            Self::Submit(cmd) => cmd.exec().await,
98            Self::Wait(cmd) => cmd.exec().await,
99        }
100    }
101}
102
103/// Initialize a new package.
104#[derive(Args)]
105#[clap(disable_version_flag = true)]
106pub struct PublishInitCommand {
107    /// The common command options.
108    #[clap(flatten)]
109    pub common: CommonOptions,
110    /// The package name being initialized.
111    #[clap(value_name = "PACKAGE")]
112    pub name: PackageName,
113    /// Whether to wait for the publish to complete.
114    #[clap(long)]
115    pub no_wait: bool,
116}
117
118impl PublishInitCommand {
119    /// Executes the command.
120    pub async fn exec(self) -> Result<()> {
121        let config = self.common.read_config()?;
122        let client = self.common.create_client(&config).await?;
123        let registry_domain = client.get_warg_registry(self.name.namespace()).await?;
124
125        let signing_key = self.common.signing_key(registry_domain.as_ref()).await?;
126        match enqueue(&client, &self.name, |_| {
127            std::future::ready(Ok(PublishEntry::Init))
128        })
129        .await?
130        {
131            Some(entry) => {
132                let record_id = client
133                    .publish_with_info(
134                        &signing_key,
135                        PublishInfo {
136                            name: self.name.clone(),
137                            head: None,
138                            entries: vec![entry],
139                        },
140                    )
141                    .await?;
142
143                if self.no_wait {
144                    println!("submitted record `{record_id}` for publishing");
145                } else {
146                    client
147                        .wait_for_publish(&self.name, &record_id, DEFAULT_WAIT_INTERVAL)
148                        .await?;
149
150                    println!(
151                        "published initialization of package `{name}`",
152                        name = self.name,
153                    );
154                }
155            }
156            None => {
157                println!(
158                    "added initialization of package `{name}` to pending publish",
159                    name = self.name
160                );
161            }
162        }
163
164        Ok(())
165    }
166}
167
168/// Publish a package to a warg registry.
169#[derive(Args)]
170#[clap(disable_version_flag = true)]
171pub struct PublishReleaseCommand {
172    /// The common command options.
173    #[clap(flatten)]
174    pub common: CommonOptions,
175    /// The package name being published.
176    #[clap(long, short, value_name = "PACKAGE")]
177    pub name: PackageName,
178    /// The version of the package being published.
179    #[clap(long, short, value_name = "VERSION")]
180    pub version: Version,
181    /// The path to the package being published.
182    #[clap(value_name = "PATH")]
183    pub path: PathBuf,
184    /// Whether to wait for the publish to complete.
185    #[clap(long)]
186    pub no_wait: bool,
187}
188
189impl PublishReleaseCommand {
190    /// Executes the command.
191    pub async fn exec(self) -> Result<()> {
192        let config = self.common.read_config()?;
193        let client = self.common.create_client(&config).await?;
194        let registry_domain = client.get_warg_registry(self.name.namespace()).await?;
195        let signing_key = self.common.signing_key(registry_domain.as_ref()).await?;
196
197        let path = self.path.clone();
198        let version = self.version.clone();
199        match enqueue(&client, &self.name, move |c| async move {
200            let content = c
201                .content()
202                .store_content(
203                    Box::pin(
204                        ReaderStream::new(BufReader::new(
205                            tokio::fs::File::open(&path).await.with_context(|| {
206                                format!("failed to open `{path}`", path = path.display())
207                            })?,
208                        ))
209                        .map_err(|e| anyhow!(e)),
210                    ),
211                    None,
212                )
213                .await?;
214
215            Ok(PublishEntry::Release { version, content })
216        })
217        .await?
218        {
219            Some(entry) => {
220                let record_id = client
221                    .publish_with_info(
222                        &signing_key,
223                        PublishInfo {
224                            name: self.name.clone(),
225                            head: None,
226                            entries: vec![entry],
227                        },
228                    )
229                    .await?;
230
231                if self.no_wait {
232                    println!("submitted record `{record_id}` for publishing");
233                } else {
234                    client
235                        .wait_for_publish(&self.name, &record_id, DEFAULT_WAIT_INTERVAL)
236                        .await?;
237
238                    println!(
239                        "published version {version} of package `{name}`",
240                        version = self.version,
241                        name = self.name
242                    );
243                }
244            }
245            None => {
246                println!(
247                    "added release of version {version} for package `{name}` to pending publish",
248                    version = self.version,
249                    name = self.name
250                );
251            }
252        }
253
254        Ok(())
255    }
256}
257
258/// Yank a package release from a warg registry.
259#[derive(Args)]
260#[clap(disable_version_flag = true)]
261pub struct PublishYankCommand {
262    /// The common command options.
263    #[clap(flatten)]
264    pub common: CommonOptions,
265    /// The package name being yanked.
266    #[clap(long, short, value_name = "PACKAGE")]
267    pub name: PackageName,
268    /// The version of the package being yanked.
269    #[clap(long, short, value_name = "VERSION")]
270    pub version: Version,
271    /// Whether to wait for the publish to complete.
272    #[clap(long)]
273    pub no_wait: bool,
274}
275
276impl PublishYankCommand {
277    /// Executes the command.
278    pub async fn exec(self) -> Result<()> {
279        if !Confirm::with_theme(&ColorfulTheme::default())
280            .with_prompt(format!(
281                "`Yank` revokes a version, making it unavailable. It is permanent and cannot be reversed.
282Yank `{version}` of `{package}`?",
283                version = &self.version,
284                package = &self.name,
285            ))
286            .default(false)
287            .interact()?
288        {
289            println!("Aborted and did not yank.");
290            return Ok(());
291        }
292
293        let config = self.common.read_config()?;
294        let client = self.common.create_client(&config).await?;
295        let registry_domain = client.get_warg_registry(self.name.namespace()).await?;
296        let signing_key = self.common.signing_key(registry_domain.as_ref()).await?;
297
298        let version = self.version.clone();
299        match enqueue(&client, &self.name, move |_| async move {
300            Ok(PublishEntry::Yank { version })
301        })
302        .await?
303        {
304            Some(entry) => {
305                let record_id = client
306                    .publish_with_info(
307                        &signing_key,
308                        PublishInfo {
309                            name: self.name.clone(),
310                            head: None,
311                            entries: vec![entry],
312                        },
313                    )
314                    .await?;
315
316                if self.no_wait {
317                    println!("submitted record `{record_id}` for publishing");
318                } else {
319                    client
320                        .wait_for_publish(&self.name, &record_id, DEFAULT_WAIT_INTERVAL)
321                        .await?;
322
323                    println!(
324                        "yanked version {version} of package `{name}`",
325                        version = self.version,
326                        name = self.name
327                    );
328                }
329            }
330            None => {
331                println!(
332                    "added yank of version {version} for package `{name}` to pending publish",
333                    version = self.version,
334                    name = self.name
335                );
336            }
337        }
338
339        Ok(())
340    }
341}
342
343/// Publish a package to a warg registry.
344#[derive(Args)]
345#[clap(disable_version_flag = true)]
346pub struct PublishGrantCommand {
347    /// The common command options.
348    #[clap(flatten)]
349    pub common: CommonOptions,
350    /// The package name.
351    #[clap(long, short, value_name = "PACKAGE")]
352    pub name: PackageName,
353    /// The public key to grant permissions to.
354    #[clap(value_name = "PUBLIC_KEY")]
355    pub public_key: PublicKey,
356    /// The permission(s) to grant.
357    #[clap(
358        long = "permission",
359        value_delimiter = ',',
360        default_value = "release,yank"
361    )]
362    pub permissions: Vec<Permission>,
363    /// Whether to wait for the publish to complete.
364    #[clap(long)]
365    pub no_wait: bool,
366}
367
368impl PublishGrantCommand {
369    /// Executes the command.
370    pub async fn exec(self) -> Result<()> {
371        let config = self.common.read_config()?;
372        let client = self.common.create_client(&config).await?;
373        let registry_domain = client.get_warg_registry(self.name.namespace()).await?;
374        let signing_key = self.common.signing_key(registry_domain.as_ref()).await?;
375
376        match enqueue(&client, &self.name, |_| async {
377            Ok(PublishEntry::Grant {
378                key: self.public_key.clone(),
379                permissions: self.permissions.clone(),
380            })
381        })
382        .await?
383        {
384            Some(entry) => {
385                let record_id = client
386                    .publish_with_info(
387                        &signing_key,
388                        PublishInfo {
389                            name: self.name.clone(),
390                            head: None,
391                            entries: vec![entry],
392                        },
393                    )
394                    .await?;
395
396                if self.no_wait {
397                    println!("submitted record `{record_id}` for publishing");
398                } else {
399                    client
400                        .wait_for_publish(&self.name, &record_id, DEFAULT_WAIT_INTERVAL)
401                        .await?;
402
403                    println!(
404                        "granted ({permissions_str}) to key ID `{key_id}` for package `{name}`",
405                        permissions_str = self.permissions.iter().join(","),
406                        key_id = self.public_key.fingerprint(),
407                        name = self.name
408                    );
409                }
410            }
411            None => {
412                println!(
413                    "added grant of ({permissions_str}) to key ID `{key_id}` for package `{name}` to pending publish",
414                    permissions_str = self.permissions.iter().join(","),
415                    key_id = self.public_key.fingerprint(),
416                    name = self.name
417                );
418            }
419        }
420
421        Ok(())
422    }
423}
424
425/// Publish a package to a warg registry.
426#[derive(Args)]
427#[clap(disable_version_flag = true)]
428pub struct PublishRevokeCommand {
429    /// The common command options.
430    #[clap(flatten)]
431    pub common: CommonOptions,
432    /// The package name.
433    #[clap(long, short, value_name = "PACKAGE")]
434    pub name: PackageName,
435    /// The key ID to revoke permissions from.
436    #[clap(value_name = "KEY_ID")]
437    pub key: KeyID,
438    /// The permission(s) to revoke.
439    #[clap(
440        long = "permission",
441        value_delimiter = ',',
442        default_value = "release,yank"
443    )]
444    pub permissions: Vec<Permission>,
445    /// Whether to wait for the publish to complete.
446    #[clap(long)]
447    pub no_wait: bool,
448}
449
450impl PublishRevokeCommand {
451    /// Executes the command.
452    pub async fn exec(self) -> Result<()> {
453        let config = self.common.read_config()?;
454        let client = self.common.create_client(&config).await?;
455        let registry_domain = client.get_warg_registry(self.name.namespace()).await?;
456        let signing_key = self.common.signing_key(registry_domain.as_ref()).await?;
457
458        match enqueue(&client, &self.name, |_| async {
459            Ok(PublishEntry::Revoke {
460                key_id: self.key.clone(),
461                permissions: self.permissions.clone(),
462            })
463        })
464        .await?
465        {
466            Some(entry) => {
467                let record_id = client
468                    .publish_with_info(
469                        &signing_key,
470                        PublishInfo {
471                            name: self.name.clone(),
472                            head: None,
473                            entries: vec![entry],
474                        },
475                    )
476                    .await?;
477
478                if self.no_wait {
479                    println!("submitted record `{record_id}` for publishing");
480                } else {
481                    client
482                        .wait_for_publish(&self.name, &record_id, DEFAULT_WAIT_INTERVAL)
483                        .await?;
484
485                    println!(
486                        "revoked ({permissions_str}) from key ID `{key_id}` for package `{name}`",
487                        permissions_str = self.permissions.iter().join(","),
488                        key_id = self.key,
489                        name = self.name
490                    );
491                }
492            }
493            None => {
494                println!(
495                    "added revoke of ({permissions_str}) from key ID `{key_id}` for package `{name}` to pending publish",
496                    permissions_str = self.permissions.iter().join(","),
497                    key_id = self.key,
498                    name = self.name
499                );
500            }
501        }
502
503        Ok(())
504    }
505}
506
507/// Start a new pending publish.
508#[derive(Args)]
509#[clap(disable_version_flag = true)]
510pub struct PublishStartCommand {
511    /// The common command options.
512    #[clap(flatten)]
513    pub common: CommonOptions,
514    /// The package name being published.
515    #[clap(value_name = "PACKAGE")]
516    pub name: PackageName,
517}
518
519impl PublishStartCommand {
520    /// Executes the command.
521    pub async fn exec(self) -> Result<()> {
522        let config = self.common.read_config()?;
523        let client = self.common.create_client(&config).await?;
524
525        match client.registry().load_publish().await? {
526            Some(info) => bail!("a publish is already in progress for package `{name}`; use `publish abort` to abort the current publish", name = info.name),
527            None => {
528                client.registry().store_publish(Some(&PublishInfo {
529                    name: self.name.clone(),
530                    head: None,
531                    entries: Default::default(),
532                }))
533                .await?;
534
535                println!(
536                    "started new pending publish for package `{name}`",
537                    name = self.name
538                );
539                Ok(())
540            },
541        }
542    }
543}
544
545/// List the records in a pending publish.
546#[derive(Args)]
547pub struct PublishListCommand {
548    /// The common command options.
549    #[clap(flatten)]
550    pub common: CommonOptions,
551}
552
553impl PublishListCommand {
554    /// Executes the command.
555    pub async fn exec(self) -> Result<()> {
556        let config = self.common.read_config()?;
557        let client = self.common.create_client(&config).await?;
558
559        match client.registry().load_publish().await? {
560            Some(info) => {
561                println!(
562                    "publishing package `{name}` with {count} record(s) to publish\n",
563                    name = info.name,
564                    count = info.entries.len()
565                );
566
567                for (i, entry) in info.entries.iter().enumerate() {
568                    print!("record {i}: ");
569                    match entry {
570                        PublishEntry::Init => {
571                            println!("initialize package");
572                        }
573                        PublishEntry::Release { version, content } => {
574                            println!("release {version} with content digest `{content}`")
575                        }
576                        PublishEntry::Yank { version } => {
577                            println!("yank {version}")
578                        }
579                        PublishEntry::Grant { key, permissions } => println!(
580                            "grant ({permissions_str}) to `{key_id}`",
581                            permissions_str = permissions.iter().join(","),
582                            key_id = key.fingerprint(),
583                        ),
584                        PublishEntry::Revoke {
585                            key_id,
586                            permissions,
587                        } => println!(
588                            "revoke ({permissions_str}) from `{key_id}`",
589                            permissions_str = permissions.iter().join(","),
590                        ),
591                    }
592                }
593            }
594            None => bail!("no pending publish to list"),
595        }
596
597        Ok(())
598    }
599}
600
601/// Abort a pending publish.
602#[derive(Args)]
603pub struct PublishAbortCommand {
604    /// The common command options.
605    #[clap(flatten)]
606    pub common: CommonOptions,
607}
608
609impl PublishAbortCommand {
610    /// Executes the command.
611    pub async fn exec(self) -> Result<()> {
612        let config = self.common.read_config()?;
613        let client = self.common.create_client(&config).await?;
614
615        match client.registry().load_publish().await? {
616            Some(info) => {
617                client.registry().store_publish(None).await?;
618                println!(
619                    "aborted the pending publish for package `{name}`",
620                    name = info.name
621                );
622            }
623            None => bail!("no pending publish to abort"),
624        }
625
626        Ok(())
627    }
628}
629
630/// Submit a pending publish.
631#[derive(Args)]
632pub struct PublishSubmitCommand {
633    /// The common command options.
634    #[clap(flatten)]
635    pub common: CommonOptions,
636    /// Whether to wait for the publish to complete.
637    #[clap(long)]
638    pub no_wait: bool,
639}
640
641impl PublishSubmitCommand {
642    /// Executes the command.
643    pub async fn exec(self) -> Result<()> {
644        let config = self.common.read_config()?;
645        let client = self.common.create_client(&config).await?;
646
647        match client.registry().load_publish().await? {
648            Some(info) => {
649                println!(
650                    "submitting publish for package `{name}`...",
651                    name = info.name
652                );
653
654                let signing_key = self.common.signing_key(None).await?;
655                let record_id = client.publish_with_info(&signing_key, info.clone()).await?;
656
657                client.registry().store_publish(None).await?;
658
659                if self.no_wait {
660                    println!("submitted record `{record_id}` for publishing");
661                } else {
662                    client
663                        .wait_for_publish(&info.name, &record_id, DEFAULT_WAIT_INTERVAL)
664                        .await?;
665
666                    for entry in &info.entries {
667                        let name = &info.name;
668                        match entry {
669                            PublishEntry::Init => {
670                                println!("published initialization of package `{name}`");
671                            }
672                            PublishEntry::Release { version, .. } => {
673                                println!("published version {version} of package `{name}`");
674                            }
675                            PublishEntry::Yank { version } => {
676                                println!("yanked version {version} of package `{name}`")
677                            }
678                            PublishEntry::Grant { key, permissions } => {
679                                println!(
680                                    "granted ({permissions_str}) to `{key_id}`",
681                                    permissions_str = permissions.iter().join(","),
682                                    key_id = key.fingerprint(),
683                                )
684                            }
685                            PublishEntry::Revoke {
686                                key_id,
687                                permissions,
688                            } => println!(
689                                "revoked ({permissions_str}) from `{key_id}`",
690                                permissions_str = permissions.iter().join(","),
691                            ),
692                        }
693                    }
694                }
695            }
696            None => bail!("no pending publish to submit"),
697        }
698
699        Ok(())
700    }
701}
702
703/// Wait for a pending publish to complete.
704#[derive(Args)]
705pub struct PublishWaitCommand {
706    /// The common command options.
707    #[clap(flatten)]
708    pub common: CommonOptions,
709
710    /// The name of the published package.
711    #[clap(value_name = "PACKAGE")]
712    pub name: PackageName,
713
714    /// The identifier of the package record to wait for completion.
715    #[clap(value_name = "RECORD")]
716    pub record_id: AnyHash,
717}
718
719impl PublishWaitCommand {
720    /// Executes the command.
721    pub async fn exec(self) -> Result<()> {
722        let config = self.common.read_config()?;
723        let client = self.common.create_client(&config).await?;
724        let record_id = RecordId::from(self.record_id);
725
726        println!(
727            "waiting for record `{record_id} of package `{name}` to be published...",
728            name = self.name
729        );
730
731        client
732            .wait_for_publish(&self.name, &record_id, Duration::from_secs(1))
733            .await?;
734
735        println!(
736            "record `{record_id} of package `{name}` has been published",
737            name = self.name
738        );
739
740        Ok(())
741    }
742}