1use clap::{Args, Subcommand};
23use ggen_utils::error::Result;
24#[cfg_attr(test, mockall::automock)]
27pub trait ReleaseWorkflowRunner {
28 fn run(&self, args: &ReleaseRunArgs) -> Result<ReleaseResult>;
29 fn run_with_retry(&self, args: &ReleaseRetryArgs) -> Result<ReleaseResult>;
30 fn run_with_metrics(&self, args: &ReleaseMetricsArgs) -> Result<ReleaseResult>;
31 fn run_with_timeout(&self, args: &ReleaseTimeoutArgs) -> Result<ReleaseResult>;
32 fn dry_run(&self, args: &ReleaseDryRunArgs) -> Result<ReleaseResult>;
33}
34
35#[derive(Debug, Clone)]
36pub struct ReleaseResult {
37 pub stdout: String,
38 pub stderr: String,
39 pub success: bool,
40}
41
42#[derive(Args, Debug)]
43pub struct ReleaseArgs {
44 #[command(subcommand)]
45 pub action: ReleaseAction,
46}
47
48#[derive(Subcommand, Debug)]
49pub enum ReleaseAction {
50 Run(RunArgs),
52
53 Retry(RetryArgs),
55
56 Metrics(MetricsArgs),
58
59 Timeout(TimeoutArgs),
61
62 DryRun(DryRunArgs),
64}
65
66pub type ReleaseRunArgs = RunArgs;
68pub type ReleaseRetryArgs = RetryArgs;
69pub type ReleaseMetricsArgs = MetricsArgs;
70pub type ReleaseTimeoutArgs = TimeoutArgs;
71pub type ReleaseDryRunArgs = DryRunArgs;
72
73#[derive(Args, Debug, PartialEq)]
74pub struct RunArgs {
75 #[arg(long)]
77 pub workflow_only: bool,
78
79 #[arg(long)]
81 pub homebrew_only: bool,
82
83 #[arg(long)]
85 pub json: bool,
86
87 #[arg(long)]
89 pub verbose: bool,
90
91 #[arg(long)]
93 pub debug: bool,
94}
95
96#[derive(Args, Debug, PartialEq)]
97pub struct RetryArgs {
98 #[arg(long)]
100 pub workflow_only: bool,
101
102 #[arg(long)]
104 pub homebrew_only: bool,
105
106 #[arg(long, default_value = "3")]
108 pub max_retries: u8,
109
110 #[arg(long)]
112 pub json: bool,
113
114 #[arg(long)]
116 pub verbose: bool,
117}
118
119#[derive(Args, Debug, PartialEq)]
120pub struct MetricsArgs {
121 #[arg(long)]
123 pub workflow_only: bool,
124
125 #[arg(long)]
127 pub homebrew_only: bool,
128
129 #[arg(long)]
131 pub json: bool,
132
133 #[arg(long)]
135 pub verbose: bool,
136}
137
138#[derive(Args, Debug, PartialEq)]
139pub struct TimeoutArgs {
140 #[arg(long, default_value = "1800")]
142 pub timeout: u32,
143
144 #[arg(long)]
146 pub workflow_only: bool,
147
148 #[arg(long)]
150 pub homebrew_only: bool,
151
152 #[arg(long)]
154 pub json: bool,
155
156 #[arg(long)]
158 pub verbose: bool,
159}
160
161#[derive(Args, Debug, PartialEq)]
162pub struct DryRunArgs {
163 #[arg(long)]
165 pub workflow_only: bool,
166
167 #[arg(long)]
169 pub homebrew_only: bool,
170
171 #[arg(long)]
173 pub json: bool,
174
175 #[arg(long)]
177 pub verbose: bool,
178}
179
180pub async fn run(args: &ReleaseArgs) -> Result<()> {
181 let runner = CargoMakeReleaseRunner;
182 run_with_deps(args, &runner).await
183}
184
185pub async fn run_with_deps(args: &ReleaseArgs, runner: &dyn ReleaseWorkflowRunner) -> Result<()> {
186 match &args.action {
187 ReleaseAction::Run(run_args) => run_release_workflows_with_deps(run_args, runner).await,
188 ReleaseAction::Retry(retry_args) => {
189 run_release_with_retry_with_deps(retry_args, runner).await
190 }
191 ReleaseAction::Metrics(metrics_args) => {
192 run_release_with_metrics_with_deps(metrics_args, runner).await
193 }
194 ReleaseAction::Timeout(timeout_args) => {
195 run_release_with_timeout_with_deps(timeout_args, runner).await
196 }
197 ReleaseAction::DryRun(dry_run_args) => {
198 run_release_dry_run_with_deps(dry_run_args, runner).await
199 }
200 }
201}
202
203async fn run_release_workflows_with_deps(
204 args: &RunArgs, runner: &dyn ReleaseWorkflowRunner,
205) -> Result<()> {
206 println!("🚀 Running release workflows locally with act");
207
208 let result = runner.run(args)?;
209
210 if !result.success {
211 return Err(ggen_utils::error::Error::new_fmt(format_args!(
212 "Release workflows failed: {}",
213 result.stderr
214 )));
215 }
216
217 if !args.json {
218 println!("✅ Release workflows completed successfully");
219 }
220 println!("{}", result.stdout);
221 Ok(())
222}
223
224#[allow(dead_code)]
225async fn run_release_workflows(args: &RunArgs) -> Result<()> {
226 let runner = CargoMakeReleaseRunner;
227 run_release_workflows_with_deps(args, &runner).await
228}
229
230async fn run_release_with_retry_with_deps(
231 args: &RetryArgs, runner: &dyn ReleaseWorkflowRunner,
232) -> Result<()> {
233 println!("🔄 Running release workflows with retry logic");
234
235 let result = runner.run_with_retry(args)?;
236
237 if !result.success {
238 return Err(ggen_utils::error::Error::new_fmt(format_args!(
239 "Release workflows with retry failed: {}",
240 result.stderr
241 )));
242 }
243
244 if !args.json {
245 println!("✅ Release workflows with retry completed successfully");
246 }
247 println!("{}", result.stdout);
248 Ok(())
249}
250
251#[allow(dead_code)]
252async fn run_release_with_retry(args: &RetryArgs) -> Result<()> {
253 let runner = CargoMakeReleaseRunner;
254 run_release_with_retry_with_deps(args, &runner).await
255}
256
257async fn run_release_with_metrics_with_deps(
258 args: &MetricsArgs, runner: &dyn ReleaseWorkflowRunner,
259) -> Result<()> {
260 println!("📊 Running release workflows with metrics collection");
261
262 let result = runner.run_with_metrics(args)?;
263
264 if !result.success {
265 return Err(ggen_utils::error::Error::new_fmt(format_args!(
266 "Release workflows with metrics failed: {}",
267 result.stderr
268 )));
269 }
270
271 if !args.json {
272 println!("✅ Release workflows with metrics completed successfully");
273 }
274 println!("{}", result.stdout);
275 Ok(())
276}
277
278#[allow(dead_code)]
279async fn run_release_with_metrics(args: &MetricsArgs) -> Result<()> {
280 let runner = CargoMakeReleaseRunner;
281 run_release_with_metrics_with_deps(args, &runner).await
282}
283
284async fn run_release_with_timeout_with_deps(
285 args: &TimeoutArgs, runner: &dyn ReleaseWorkflowRunner,
286) -> Result<()> {
287 println!("⏱️ Running release workflows with custom timeout");
288
289 let result = runner.run_with_timeout(args)?;
290
291 if !result.success {
292 return Err(ggen_utils::error::Error::new_fmt(format_args!(
293 "Release workflows with timeout failed: {}",
294 result.stderr
295 )));
296 }
297
298 if !args.json {
299 println!("✅ Release workflows with timeout completed successfully");
300 }
301 println!("{}", result.stdout);
302 Ok(())
303}
304
305#[allow(dead_code)]
306async fn run_release_with_timeout(args: &TimeoutArgs) -> Result<()> {
307 let runner = CargoMakeReleaseRunner;
308 run_release_with_timeout_with_deps(args, &runner).await
309}
310
311async fn run_release_dry_run_with_deps(
312 args: &DryRunArgs, runner: &dyn ReleaseWorkflowRunner,
313) -> Result<()> {
314 println!("🔍 Showing what would be executed (dry run)");
315
316 let result = runner.dry_run(args)?;
317
318 if !result.success {
319 return Err(ggen_utils::error::Error::new_fmt(format_args!(
320 "Release workflows dry run failed: {}",
321 result.stderr
322 )));
323 }
324
325 println!("{}", result.stdout);
326 Ok(())
327}
328
329#[allow(dead_code)]
330async fn run_release_dry_run(args: &DryRunArgs) -> Result<()> {
331 let runner = CargoMakeReleaseRunner;
332 run_release_dry_run_with_deps(args, &runner).await
333}
334
335pub struct CargoMakeReleaseRunner;
337
338impl ReleaseWorkflowRunner for CargoMakeReleaseRunner {
339 fn run(&self, args: &ReleaseRunArgs) -> Result<ReleaseResult> {
340 let mut cmd = std::process::Command::new("cargo");
341 cmd.args(["make", "act-release"]);
342
343 if args.workflow_only {
344 cmd.arg("--workflow-only");
345 }
346
347 if args.homebrew_only {
348 cmd.arg("--homebrew-only");
349 }
350
351 if args.json {
352 cmd.arg("--json");
353 }
354
355 if args.verbose {
356 cmd.arg("--verbose");
357 }
358
359 if args.debug {
360 cmd.env("DEBUG", "true");
361 }
362
363 let output = cmd.output()?;
364 Ok(ReleaseResult {
365 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
366 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
367 success: output.status.success(),
368 })
369 }
370
371 fn run_with_retry(&self, args: &ReleaseRetryArgs) -> Result<ReleaseResult> {
372 let mut cmd = std::process::Command::new("cargo");
373 cmd.args(["make", "act-release-retry"]);
374 cmd.arg("--max-retries").arg(args.max_retries.to_string());
375
376 if args.workflow_only {
377 cmd.arg("--workflow-only");
378 }
379
380 if args.homebrew_only {
381 cmd.arg("--homebrew-only");
382 }
383
384 if args.json {
385 cmd.arg("--json");
386 }
387
388 if args.verbose {
389 cmd.arg("--verbose");
390 }
391
392 let output = cmd.output()?;
393 Ok(ReleaseResult {
394 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
395 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
396 success: output.status.success(),
397 })
398 }
399
400 fn run_with_metrics(&self, args: &ReleaseMetricsArgs) -> Result<ReleaseResult> {
401 let mut cmd = std::process::Command::new("cargo");
402 cmd.args(["make", "act-release-metrics"]);
403
404 if args.workflow_only {
405 cmd.arg("--workflow-only");
406 }
407
408 if args.homebrew_only {
409 cmd.arg("--homebrew-only");
410 }
411
412 if args.json {
413 cmd.arg("--json");
414 }
415
416 if args.verbose {
417 cmd.arg("--verbose");
418 }
419
420 let output = cmd.output()?;
421 Ok(ReleaseResult {
422 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
423 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
424 success: output.status.success(),
425 })
426 }
427
428 fn run_with_timeout(&self, args: &ReleaseTimeoutArgs) -> Result<ReleaseResult> {
429 let mut cmd = std::process::Command::new("cargo");
430 cmd.args(["make", "act-release-timeout"]);
431 cmd.arg("--timeout").arg(args.timeout.to_string());
432
433 if args.workflow_only {
434 cmd.arg("--workflow-only");
435 }
436
437 if args.homebrew_only {
438 cmd.arg("--homebrew-only");
439 }
440
441 if args.json {
442 cmd.arg("--json");
443 }
444
445 if args.verbose {
446 cmd.arg("--verbose");
447 }
448
449 let output = cmd.output()?;
450 Ok(ReleaseResult {
451 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
452 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
453 success: output.status.success(),
454 })
455 }
456
457 fn dry_run(&self, args: &ReleaseDryRunArgs) -> Result<ReleaseResult> {
458 let mut cmd = std::process::Command::new("cargo");
459 cmd.args(["make", "act-release-dry-run"]);
460
461 if args.workflow_only {
462 cmd.arg("--workflow-only");
463 }
464
465 if args.homebrew_only {
466 cmd.arg("--homebrew-only");
467 }
468
469 if args.json {
470 cmd.arg("--json");
471 }
472
473 if args.verbose {
474 cmd.arg("--verbose");
475 }
476
477 let output = cmd.output()?;
478 Ok(ReleaseResult {
479 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
480 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
481 success: output.status.success(),
482 })
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use mockall::predicate::*;
490
491 #[tokio::test]
492 async fn test_run_calls_runner() {
493 let mut mock = MockReleaseWorkflowRunner::new();
494 mock.expect_run()
495 .with(eq(RunArgs {
496 workflow_only: false,
497 homebrew_only: false,
498 json: false,
499 verbose: false,
500 debug: false,
501 }))
502 .times(1)
503 .returning(|_| {
504 Ok(ReleaseResult {
505 stdout: "Release complete".to_string(),
506 stderr: "".to_string(),
507 success: true,
508 })
509 });
510
511 let args = RunArgs {
512 workflow_only: false,
513 homebrew_only: false,
514 json: false,
515 verbose: false,
516 debug: false,
517 };
518 let result = run_release_workflows_with_deps(&args, &mock).await;
519 assert!(result.is_ok());
520 }
521
522 #[tokio::test]
523 async fn test_run_handles_failure() {
524 let mut mock = MockReleaseWorkflowRunner::new();
525 mock.expect_run()
526 .with(eq(RunArgs {
527 workflow_only: false,
528 homebrew_only: false,
529 json: false,
530 verbose: false,
531 debug: false,
532 }))
533 .times(1)
534 .returning(|_| {
535 Ok(ReleaseResult {
536 stdout: "".to_string(),
537 stderr: "Release failed".to_string(),
538 success: false,
539 })
540 });
541
542 let args = RunArgs {
543 workflow_only: false,
544 homebrew_only: false,
545 json: false,
546 verbose: false,
547 debug: false,
548 };
549 let result = run_release_workflows_with_deps(&args, &mock).await;
550 assert!(result.is_err());
551 assert!(result
552 .unwrap_err()
553 .to_string()
554 .contains("Release workflows failed"));
555 }
556
557 #[tokio::test]
558 async fn test_retry_calls_runner() {
559 let mut mock = MockReleaseWorkflowRunner::new();
560 mock.expect_run_with_retry()
561 .with(eq(RetryArgs {
562 workflow_only: false,
563 homebrew_only: false,
564 max_retries: 3,
565 json: false,
566 verbose: false,
567 }))
568 .times(1)
569 .returning(|_| {
570 Ok(ReleaseResult {
571 stdout: "Retry complete".to_string(),
572 stderr: "".to_string(),
573 success: true,
574 })
575 });
576
577 let args = RetryArgs {
578 workflow_only: false,
579 homebrew_only: false,
580 max_retries: 3,
581 json: false,
582 verbose: false,
583 };
584 let result = run_release_with_retry_with_deps(&args, &mock).await;
585 assert!(result.is_ok());
586 }
587
588 #[tokio::test]
589 async fn test_metrics_calls_runner() {
590 let mut mock = MockReleaseWorkflowRunner::new();
591 mock.expect_run_with_metrics()
592 .with(eq(MetricsArgs {
593 workflow_only: false,
594 homebrew_only: false,
595 json: false,
596 verbose: false,
597 }))
598 .times(1)
599 .returning(|_| {
600 Ok(ReleaseResult {
601 stdout: "Metrics complete".to_string(),
602 stderr: "".to_string(),
603 success: true,
604 })
605 });
606
607 let args = MetricsArgs {
608 workflow_only: false,
609 homebrew_only: false,
610 json: false,
611 verbose: false,
612 };
613 let result = run_release_with_metrics_with_deps(&args, &mock).await;
614 assert!(result.is_ok());
615 }
616
617 #[tokio::test]
618 async fn test_timeout_calls_runner() {
619 let mut mock = MockReleaseWorkflowRunner::new();
620 mock.expect_run_with_timeout()
621 .with(eq(TimeoutArgs {
622 timeout: 1800,
623 workflow_only: false,
624 homebrew_only: false,
625 json: false,
626 verbose: false,
627 }))
628 .times(1)
629 .returning(|_| {
630 Ok(ReleaseResult {
631 stdout: "Timeout complete".to_string(),
632 stderr: "".to_string(),
633 success: true,
634 })
635 });
636
637 let args = TimeoutArgs {
638 timeout: 1800,
639 workflow_only: false,
640 homebrew_only: false,
641 json: false,
642 verbose: false,
643 };
644 let result = run_release_with_timeout_with_deps(&args, &mock).await;
645 assert!(result.is_ok());
646 }
647
648 #[tokio::test]
649 async fn test_dry_run_calls_runner() {
650 let mut mock = MockReleaseWorkflowRunner::new();
651 mock.expect_dry_run()
652 .with(eq(DryRunArgs {
653 workflow_only: false,
654 homebrew_only: false,
655 json: false,
656 verbose: false,
657 }))
658 .times(1)
659 .returning(|_| {
660 Ok(ReleaseResult {
661 stdout: "Dry run complete".to_string(),
662 stderr: "".to_string(),
663 success: true,
664 })
665 });
666
667 let args = DryRunArgs {
668 workflow_only: false,
669 homebrew_only: false,
670 json: false,
671 verbose: false,
672 };
673 let result = run_release_dry_run_with_deps(&args, &mock).await;
674 assert!(result.is_ok());
675 }
676
677 #[tokio::test]
678 async fn test_run_with_deps_dispatches_correctly() {
679 let mut mock = MockReleaseWorkflowRunner::new();
680 mock.expect_run()
681 .with(eq(RunArgs {
682 workflow_only: false,
683 homebrew_only: false,
684 json: false,
685 verbose: false,
686 debug: false,
687 }))
688 .times(1)
689 .returning(|_| {
690 Ok(ReleaseResult {
691 stdout: "Release complete".to_string(),
692 stderr: "".to_string(),
693 success: true,
694 })
695 });
696
697 let args = ReleaseArgs {
698 action: ReleaseAction::Run(RunArgs {
699 workflow_only: false,
700 homebrew_only: false,
701 json: false,
702 verbose: false,
703 debug: false,
704 }),
705 };
706
707 let result = run_with_deps(&args, &mock).await;
708 assert!(result.is_ok());
709 }
710
711 #[test]
712 fn test_run_args_defaults() {
713 let args = RunArgs {
714 workflow_only: false,
715 homebrew_only: false,
716 json: false,
717 verbose: false,
718 debug: false,
719 };
720 assert!(!args.workflow_only);
721 assert!(!args.homebrew_only);
722 assert!(!args.json);
723 assert!(!args.verbose);
724 assert!(!args.debug);
725 }
726
727 #[test]
728 fn test_retry_args_defaults() {
729 let args = RetryArgs {
730 workflow_only: false,
731 homebrew_only: false,
732 max_retries: 3,
733 json: false,
734 verbose: false,
735 };
736 assert!(!args.workflow_only);
737 assert!(!args.homebrew_only);
738 assert_eq!(args.max_retries, 3);
739 assert!(!args.json);
740 assert!(!args.verbose);
741 }
742
743 #[test]
744 fn test_metrics_args_defaults() {
745 let args = MetricsArgs {
746 workflow_only: false,
747 homebrew_only: false,
748 json: false,
749 verbose: false,
750 };
751 assert!(!args.workflow_only);
752 assert!(!args.homebrew_only);
753 assert!(!args.json);
754 assert!(!args.verbose);
755 }
756
757 #[test]
758 fn test_timeout_args_defaults() {
759 let args = TimeoutArgs {
760 timeout: 1800,
761 workflow_only: false,
762 homebrew_only: false,
763 json: false,
764 verbose: false,
765 };
766 assert_eq!(args.timeout, 1800);
767 assert!(!args.workflow_only);
768 assert!(!args.homebrew_only);
769 assert!(!args.json);
770 assert!(!args.verbose);
771 }
772
773 #[test]
774 fn test_dry_run_args_defaults() {
775 let args = DryRunArgs {
776 workflow_only: false,
777 homebrew_only: false,
778 json: false,
779 verbose: false,
780 };
781 assert!(!args.workflow_only);
782 assert!(!args.homebrew_only);
783 assert!(!args.json);
784 assert!(!args.verbose);
785 }
786}