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
26async 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#[derive(Subcommand)]
62pub enum PublishCommand {
63 Init(PublishInitCommand),
65 Release(PublishReleaseCommand),
67 Yank(PublishYankCommand),
69 Grant(PublishGrantCommand),
71 Revoke(PublishRevokeCommand),
73 Start(PublishStartCommand),
75 List(PublishListCommand),
77 Abort(PublishAbortCommand),
79 Submit(PublishSubmitCommand),
81 Wait(PublishWaitCommand),
83}
84
85impl PublishCommand {
86 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#[derive(Args)]
105#[clap(disable_version_flag = true)]
106pub struct PublishInitCommand {
107 #[clap(flatten)]
109 pub common: CommonOptions,
110 #[clap(value_name = "PACKAGE")]
112 pub name: PackageName,
113 #[clap(long)]
115 pub no_wait: bool,
116}
117
118impl PublishInitCommand {
119 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#[derive(Args)]
170#[clap(disable_version_flag = true)]
171pub struct PublishReleaseCommand {
172 #[clap(flatten)]
174 pub common: CommonOptions,
175 #[clap(long, short, value_name = "PACKAGE")]
177 pub name: PackageName,
178 #[clap(long, short, value_name = "VERSION")]
180 pub version: Version,
181 #[clap(value_name = "PATH")]
183 pub path: PathBuf,
184 #[clap(long)]
186 pub no_wait: bool,
187}
188
189impl PublishReleaseCommand {
190 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#[derive(Args)]
260#[clap(disable_version_flag = true)]
261pub struct PublishYankCommand {
262 #[clap(flatten)]
264 pub common: CommonOptions,
265 #[clap(long, short, value_name = "PACKAGE")]
267 pub name: PackageName,
268 #[clap(long, short, value_name = "VERSION")]
270 pub version: Version,
271 #[clap(long)]
273 pub no_wait: bool,
274}
275
276impl PublishYankCommand {
277 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#[derive(Args)]
345#[clap(disable_version_flag = true)]
346pub struct PublishGrantCommand {
347 #[clap(flatten)]
349 pub common: CommonOptions,
350 #[clap(long, short, value_name = "PACKAGE")]
352 pub name: PackageName,
353 #[clap(value_name = "PUBLIC_KEY")]
355 pub public_key: PublicKey,
356 #[clap(
358 long = "permission",
359 value_delimiter = ',',
360 default_value = "release,yank"
361 )]
362 pub permissions: Vec<Permission>,
363 #[clap(long)]
365 pub no_wait: bool,
366}
367
368impl PublishGrantCommand {
369 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#[derive(Args)]
427#[clap(disable_version_flag = true)]
428pub struct PublishRevokeCommand {
429 #[clap(flatten)]
431 pub common: CommonOptions,
432 #[clap(long, short, value_name = "PACKAGE")]
434 pub name: PackageName,
435 #[clap(value_name = "KEY_ID")]
437 pub key: KeyID,
438 #[clap(
440 long = "permission",
441 value_delimiter = ',',
442 default_value = "release,yank"
443 )]
444 pub permissions: Vec<Permission>,
445 #[clap(long)]
447 pub no_wait: bool,
448}
449
450impl PublishRevokeCommand {
451 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#[derive(Args)]
509#[clap(disable_version_flag = true)]
510pub struct PublishStartCommand {
511 #[clap(flatten)]
513 pub common: CommonOptions,
514 #[clap(value_name = "PACKAGE")]
516 pub name: PackageName,
517}
518
519impl PublishStartCommand {
520 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#[derive(Args)]
547pub struct PublishListCommand {
548 #[clap(flatten)]
550 pub common: CommonOptions,
551}
552
553impl PublishListCommand {
554 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#[derive(Args)]
603pub struct PublishAbortCommand {
604 #[clap(flatten)]
606 pub common: CommonOptions,
607}
608
609impl PublishAbortCommand {
610 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#[derive(Args)]
632pub struct PublishSubmitCommand {
633 #[clap(flatten)]
635 pub common: CommonOptions,
636 #[clap(long)]
638 pub no_wait: bool,
639}
640
641impl PublishSubmitCommand {
642 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#[derive(Args)]
705pub struct PublishWaitCommand {
706 #[clap(flatten)]
708 pub common: CommonOptions,
709
710 #[clap(value_name = "PACKAGE")]
712 pub name: PackageName,
713
714 #[clap(value_name = "RECORD")]
716 pub record_id: AnyHash,
717}
718
719impl PublishWaitCommand {
720 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}