1use clap::{Args, Subcommand};
22use ggen_utils::error::Result;
23#[cfg_attr(test, mockall::automock)]
26pub trait WorkflowLister {
27 fn list(&self, active: bool, json: bool) -> Result<WorkflowListResult>;
28}
29
30#[cfg_attr(test, mockall::automock)]
31pub trait WorkflowStatusChecker {
32 fn check_status(
33 &self, workflow: Option<String>, verbose: bool, json: bool,
34 ) -> Result<WorkflowStatusResult>;
35}
36
37#[cfg_attr(test, mockall::automock)]
38pub trait WorkflowLogViewer {
39 fn view_logs(&self, workflow: Option<String>, follow: bool) -> Result<WorkflowLogsResult>;
40}
41
42#[cfg_attr(test, mockall::automock)]
43pub trait WorkflowCanceler {
44 fn cancel(&self, workflow: &str) -> Result<WorkflowCancelResult>;
45}
46
47pub struct CargoMakeWorkflowLister;
49pub struct CargoMakeWorkflowStatusChecker;
50pub struct CargoMakeWorkflowLogViewer;
51pub struct CargoMakeWorkflowCanceler;
52
53#[derive(Debug, Clone)]
54pub struct WorkflowListResult {
55 pub stdout: String,
56 pub stderr: String,
57 pub success: bool,
58}
59
60#[derive(Debug, Clone)]
61pub struct WorkflowStatusResult {
62 pub stdout: String,
63 pub stderr: String,
64 pub success: bool,
65}
66
67#[derive(Debug, Clone)]
68pub struct WorkflowLogsResult {
69 pub stdout: String,
70 pub stderr: String,
71 pub success: bool,
72}
73
74#[derive(Debug, Clone)]
75pub struct WorkflowCancelResult {
76 pub stdout: String,
77 pub stderr: String,
78 pub success: bool,
79}
80
81#[derive(Args, Debug)]
82pub struct WorkflowArgs {
83 #[command(subcommand)]
84 pub action: WorkflowAction,
85}
86
87#[derive(Subcommand, Debug)]
88pub enum WorkflowAction {
89 List(ListArgs),
91
92 Status(StatusArgs),
94
95 Logs(LogsArgs),
97
98 Cancel(CancelArgs),
100}
101
102#[derive(Args, Debug)]
103pub struct ListArgs {
104 #[arg(long)]
106 pub active: bool,
107
108 #[arg(long)]
110 pub json: bool,
111}
112
113#[derive(Args, Debug)]
114pub struct StatusArgs {
115 #[arg(long)]
117 pub workflow: Option<String>,
118
119 #[arg(long)]
121 pub verbose: bool,
122
123 #[arg(long)]
125 pub json: bool,
126}
127
128#[derive(Args, Debug)]
129pub struct LogsArgs {
130 #[arg(long)]
132 pub workflow: Option<String>,
133
134 #[arg(long)]
136 pub follow: bool,
137
138 #[arg(long, default_value = "100")]
140 pub lines: usize,
141}
142
143#[derive(Args, Debug)]
144pub struct CancelArgs {
145 #[arg(long)]
147 pub workflow: Option<String>,
148
149 #[arg(long)]
151 pub all: bool,
152}
153
154pub async fn run(args: &WorkflowArgs) -> Result<()> {
155 let lister = CargoMakeWorkflowLister;
156 let status_checker = CargoMakeWorkflowStatusChecker;
157 let log_viewer = CargoMakeWorkflowLogViewer;
158 let canceler = CargoMakeWorkflowCanceler;
159
160 run_with_deps(args, &lister, &status_checker, &log_viewer, &canceler).await
161}
162
163pub async fn run_with_deps(
164 args: &WorkflowArgs, lister: &dyn WorkflowLister, status_checker: &dyn WorkflowStatusChecker,
165 log_viewer: &dyn WorkflowLogViewer, canceler: &dyn WorkflowCanceler,
166) -> Result<()> {
167 match &args.action {
168 WorkflowAction::List(list_args) => list_workflows_with_deps(list_args, lister).await,
169 WorkflowAction::Status(status_args) => {
170 check_workflow_status_with_deps(status_args, status_checker).await
171 }
172 WorkflowAction::Logs(logs_args) => {
173 view_workflow_logs_with_deps(logs_args, log_viewer).await
174 }
175 WorkflowAction::Cancel(cancel_args) => {
176 cancel_workflows_with_deps(cancel_args, canceler).await
177 }
178 }
179}
180
181async fn list_workflows_with_deps(args: &ListArgs, lister: &dyn WorkflowLister) -> Result<()> {
182 println!("Listing GitHub Actions workflows");
183
184 let result = lister.list(args.active, args.json)?;
185
186 if !result.success {
187 return Err(ggen_utils::error::Error::new_fmt(format_args!(
188 "Workflow listing failed: {}",
189 result.stderr
190 )));
191 }
192
193 println!("{}", result.stdout);
194 Ok(())
195}
196
197#[allow(dead_code)]
198async fn list_workflows(args: &ListArgs) -> Result<()> {
199 let lister = CargoMakeWorkflowLister;
200 list_workflows_with_deps(args, &lister).await
201}
202
203async fn check_workflow_status_with_deps(
204 args: &StatusArgs, status_checker: &dyn WorkflowStatusChecker,
205) -> Result<()> {
206 println!("Checking GitHub Actions workflow status");
207
208 let result = status_checker.check_status(args.workflow.clone(), args.verbose, args.json)?;
209
210 if !result.success {
211 return Err(ggen_utils::error::Error::new_fmt(format_args!(
212 "Workflow status check failed: {}",
213 result.stderr
214 )));
215 }
216
217 println!("{}", result.stdout);
218 Ok(())
219}
220
221#[allow(dead_code)]
222async fn check_workflow_status(args: &StatusArgs) -> Result<()> {
223 let status_checker = CargoMakeWorkflowStatusChecker;
224 check_workflow_status_with_deps(args, &status_checker).await
225}
226
227async fn view_workflow_logs_with_deps(
228 args: &LogsArgs, log_viewer: &dyn WorkflowLogViewer,
229) -> Result<()> {
230 println!("Viewing GitHub Actions workflow logs");
231
232 let result = log_viewer.view_logs(args.workflow.clone(), args.follow)?;
233
234 if !result.success {
235 return Err(ggen_utils::error::Error::new_fmt(format_args!(
236 "Workflow logs retrieval failed: {}",
237 result.stderr
238 )));
239 }
240
241 println!("{}", result.stdout);
242 Ok(())
243}
244
245#[allow(dead_code)]
246async fn view_workflow_logs(args: &LogsArgs) -> Result<()> {
247 let log_viewer = CargoMakeWorkflowLogViewer;
248 view_workflow_logs_with_deps(args, &log_viewer).await
249}
250
251async fn cancel_workflows_with_deps(
252 args: &CancelArgs, canceler: &dyn WorkflowCanceler,
253) -> Result<()> {
254 println!("Cancelling GitHub Actions workflows");
255
256 if args.all {
257 println!("Cancelling all running workflows");
258 let result = canceler.cancel("all")?;
259
260 if !result.success {
261 return Err(ggen_utils::error::Error::new_fmt(format_args!(
262 "Failed to cancel all workflows: {}",
263 result.stderr
264 )));
265 }
266
267 println!("✅ All running workflows cancelled");
268 } else if let Some(workflow) = &args.workflow {
269 println!("Cancelling workflow: {}", workflow);
270
271 let result = canceler.cancel(workflow)?;
272
273 if !result.success {
274 return Err(ggen_utils::error::Error::new_fmt(format_args!(
275 "Failed to cancel workflow {}: {}",
276 workflow, result.stderr
277 )));
278 }
279
280 println!("✅ Workflow {} cancelled", workflow);
281 } else {
282 return Err(ggen_utils::error::Error::new(
283 "Must specify either --workflow or --all",
284 ));
285 }
286
287 Ok(())
288}
289
290#[allow(dead_code)]
291async fn cancel_workflows(args: &CancelArgs) -> Result<()> {
292 let canceler = CargoMakeWorkflowCanceler;
293 cancel_workflows_with_deps(args, &canceler).await
294}
295
296impl WorkflowLister for CargoMakeWorkflowLister {
299 fn list(&self, active: bool, json: bool) -> Result<WorkflowListResult> {
300 let mut cmd = std::process::Command::new("cargo");
301 cmd.args(["make", "gh-workflow-status"]);
302
303 if active {
304 cmd.arg("--active");
305 }
306
307 if json {
308 cmd.arg("--json");
309 }
310
311 let output = cmd.output()?;
312 Ok(WorkflowListResult {
313 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
314 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
315 success: output.status.success(),
316 })
317 }
318}
319
320impl WorkflowStatusChecker for CargoMakeWorkflowStatusChecker {
321 fn check_status(
322 &self, workflow: Option<String>, verbose: bool, json: bool,
323 ) -> Result<WorkflowStatusResult> {
324 let mut cmd = std::process::Command::new("cargo");
325 cmd.args(["make", "gh-workflow-status"]);
326
327 if let Some(workflow) = workflow {
328 cmd.arg("--workflow").arg(workflow);
329 }
330
331 if verbose {
332 cmd.arg("--verbose");
333 }
334
335 if json {
336 cmd.arg("--json");
337 }
338
339 let output = cmd.output()?;
340 Ok(WorkflowStatusResult {
341 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
342 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
343 success: output.status.success(),
344 })
345 }
346}
347
348impl WorkflowLogViewer for CargoMakeWorkflowLogViewer {
349 fn view_logs(&self, workflow: Option<String>, follow: bool) -> Result<WorkflowLogsResult> {
350 let mut cmd = std::process::Command::new("cargo");
351 cmd.args(["make", "gh-workflow-logs"]);
352
353 if let Some(workflow) = workflow {
354 cmd.arg("--workflow").arg(workflow);
355 }
356
357 if follow {
358 cmd.arg("--follow");
359 }
360
361 let output = cmd.output()?;
362 Ok(WorkflowLogsResult {
363 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
364 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
365 success: output.status.success(),
366 })
367 }
368}
369
370impl WorkflowCanceler for CargoMakeWorkflowCanceler {
371 fn cancel(&self, workflow: &str) -> Result<WorkflowCancelResult> {
372 let mut cmd = std::process::Command::new("gh");
373 if workflow == "all" {
374 cmd.args(["run", "cancel", "--all"]);
375 } else {
376 cmd.args(["run", "cancel", workflow]);
377 }
378
379 let output = cmd.output()?;
380 Ok(WorkflowCancelResult {
381 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
382 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
383 success: output.status.success(),
384 })
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use mockall::predicate::*;
392
393 #[tokio::test]
394 async fn test_list_calls_lister() {
395 let mut mock = MockWorkflowLister::new();
396 mock.expect_list()
397 .with(eq(false), eq(false))
398 .times(1)
399 .returning(|_, _| {
400 Ok(WorkflowListResult {
401 stdout: "Workflow list".to_string(),
402 stderr: "".to_string(),
403 success: true,
404 })
405 });
406
407 let args = ListArgs {
408 active: false,
409 json: false,
410 };
411 let result = list_workflows_with_deps(&args, &mock).await;
412 assert!(result.is_ok());
413 }
414
415 #[tokio::test]
416 async fn test_status_calls_checker() {
417 let mut mock = MockWorkflowStatusChecker::new();
418 mock.expect_check_status()
419 .with(eq(Some("build".to_string())), eq(false), eq(false))
420 .times(1)
421 .returning(|_, _, _| {
422 Ok(WorkflowStatusResult {
423 stdout: "Status OK".to_string(),
424 stderr: "".to_string(),
425 success: true,
426 })
427 });
428
429 let args = StatusArgs {
430 workflow: Some("build".to_string()),
431 verbose: false,
432 json: false,
433 };
434 let result = check_workflow_status_with_deps(&args, &mock).await;
435 assert!(result.is_ok());
436 }
437
438 #[tokio::test]
439 async fn test_logs_calls_viewer() {
440 let mut mock = MockWorkflowLogViewer::new();
441 mock.expect_view_logs()
442 .with(eq(Some("test".to_string())), eq(false))
443 .times(1)
444 .returning(|_, _| {
445 Ok(WorkflowLogsResult {
446 stdout: "Log output".to_string(),
447 stderr: "".to_string(),
448 success: true,
449 })
450 });
451
452 let args = LogsArgs {
453 workflow: Some("test".to_string()),
454 follow: false,
455 lines: 100,
456 };
457 let result = view_workflow_logs_with_deps(&args, &mock).await;
458 assert!(result.is_ok());
459 }
460
461 #[tokio::test]
462 async fn test_cancel_calls_canceler() {
463 let mut mock = MockWorkflowCanceler::new();
464 mock.expect_cancel()
465 .with(eq("deploy"))
466 .times(1)
467 .returning(|_| {
468 Ok(WorkflowCancelResult {
469 stdout: "Cancel complete".to_string(),
470 stderr: "".to_string(),
471 success: true,
472 })
473 });
474
475 let args = CancelArgs {
476 workflow: Some("deploy".to_string()),
477 all: false,
478 };
479 let result = cancel_workflows_with_deps(&args, &mock).await;
480 assert!(result.is_ok());
481 }
482
483 #[tokio::test]
484 async fn test_cancel_all_calls_canceler() {
485 let mut mock = MockWorkflowCanceler::new();
486 mock.expect_cancel()
487 .with(eq("all"))
488 .times(1)
489 .returning(|_| {
490 Ok(WorkflowCancelResult {
491 stdout: "All cancelled".to_string(),
492 stderr: "".to_string(),
493 success: true,
494 })
495 });
496
497 let args = CancelArgs {
498 workflow: None,
499 all: true,
500 };
501 let result = cancel_workflows_with_deps(&args, &mock).await;
502 assert!(result.is_ok());
503 }
504
505 #[tokio::test]
506 async fn test_cancel_requires_workflow_or_all() {
507 let mock = MockWorkflowCanceler::new();
508
509 let args = CancelArgs {
510 workflow: None,
511 all: false,
512 };
513 let result = cancel_workflows_with_deps(&args, &mock).await;
514 assert!(result.is_err());
515 assert!(result
516 .unwrap_err()
517 .to_string()
518 .contains("Must specify either --workflow or --all"));
519 }
520
521 #[tokio::test]
522 async fn test_run_with_deps_dispatches_correctly() {
523 let mut mock_lister = MockWorkflowLister::new();
524 mock_lister
525 .expect_list()
526 .with(eq(false), eq(false))
527 .times(1)
528 .returning(|_, _| {
529 Ok(WorkflowListResult {
530 stdout: "Workflow list".to_string(),
531 stderr: "".to_string(),
532 success: true,
533 })
534 });
535
536 let mock_status_checker = MockWorkflowStatusChecker::new();
537 let mock_log_viewer = MockWorkflowLogViewer::new();
538 let mock_canceler = MockWorkflowCanceler::new();
539
540 let args = WorkflowArgs {
541 action: WorkflowAction::List(ListArgs {
542 active: false,
543 json: false,
544 }),
545 };
546
547 let result = run_with_deps(
548 &args,
549 &mock_lister,
550 &mock_status_checker,
551 &mock_log_viewer,
552 &mock_canceler,
553 )
554 .await;
555 assert!(result.is_ok());
556 }
557
558 #[test]
559 fn test_list_args_defaults() {
560 let args = ListArgs {
561 active: false,
562 json: false,
563 };
564 assert!(!args.active);
565 assert!(!args.json);
566 }
567
568 #[test]
569 fn test_status_args_defaults() {
570 let args = StatusArgs {
571 workflow: None,
572 verbose: false,
573 json: false,
574 };
575 assert!(args.workflow.is_none());
576 assert!(!args.verbose);
577 assert!(!args.json);
578 }
579
580 #[test]
581 fn test_logs_args_defaults() {
582 let args = LogsArgs {
583 workflow: None,
584 follow: false,
585 lines: 100,
586 };
587 assert!(args.workflow.is_none());
588 assert!(!args.follow);
589 assert_eq!(args.lines, 100);
590 }
591
592 #[test]
593 fn test_cancel_args_defaults() {
594 let args = CancelArgs {
595 workflow: None,
596 all: false,
597 };
598 assert!(args.workflow.is_none());
599 assert!(!args.all);
600 }
601}