ggen_cli_lib/cmds/ci/
trigger.rs

1//! GitHub Actions workflow triggering and management.
2//!
3//! This module provides functionality to trigger GitHub Actions workflows,
4//! manage workflow runs, and monitor execution status. It integrates with
5//! GitHub API to provide comprehensive workflow management capabilities.
6//!
7//! # Examples
8//!
9//! ```bash
10//! ggen ci trigger workflow --name "build" --ref "main"
11//! ggen ci trigger dispatch --event "release" --payload '{"version": "1.0.0"}'
12//! ggen ci trigger rerun --run-id "12345"
13//! ```
14//!
15//! # Errors
16//!
17//! Returns errors if GitHub API calls fail, workflows don't exist, or if
18//! authentication is not properly configured.
19
20use clap::{Args, Subcommand};
21use ggen_utils::error::Result;
22// CLI output only - no library logging
23
24#[derive(Args, Debug)]
25pub struct TriggerArgs {
26    #[command(subcommand)]
27    pub action: TriggerAction,
28}
29
30#[derive(Subcommand, Debug)]
31pub enum TriggerAction {
32    /// Trigger a specific workflow
33    Workflow(WorkflowTriggerArgs),
34
35    /// Trigger all workflows
36    All(AllTriggerArgs),
37
38    /// Trigger local testing with act
39    Local(LocalTriggerArgs),
40}
41
42#[derive(Args, Debug)]
43pub struct WorkflowTriggerArgs {
44    /// Workflow name to trigger
45    #[arg(long)]
46    pub workflow: String,
47
48    /// Branch to trigger workflow on [default: main]
49    #[arg(long, default_value = "main")]
50    pub branch: String,
51
52    /// Input parameters for the workflow
53    #[arg(long)]
54    pub inputs: Option<Vec<String>>,
55}
56
57#[derive(Args, Debug)]
58pub struct AllTriggerArgs {
59    /// Branch to trigger workflows on [default: main]
60    #[arg(long, default_value = "main")]
61    pub branch: String,
62
63    /// Wait for workflows to complete
64    #[arg(long)]
65    pub wait: bool,
66}
67
68#[derive(Args, Debug)]
69pub struct LocalTriggerArgs {
70    /// Workflow to run locally [default: all]
71    #[arg(long, default_value = "all")]
72    pub workflow: String,
73
74    /// Use lightweight mode (less memory)
75    #[arg(long)]
76    pub light: bool,
77
78    /// Dry run (no execution)
79    #[arg(long)]
80    pub dry_run: bool,
81}
82
83pub async fn run(args: &TriggerArgs) -> Result<()> {
84    match &args.action {
85        TriggerAction::Workflow(workflow_args) => trigger_workflow(workflow_args).await,
86        TriggerAction::All(all_args) => trigger_all_workflows(all_args).await,
87        TriggerAction::Local(local_args) => trigger_local_testing(local_args).await,
88    }
89}
90
91/// Validate and sanitize workflow name input
92fn validate_workflow_name(workflow: &str) -> Result<()> {
93    // Validate workflow name is not empty
94    if workflow.trim().is_empty() {
95        return Err(ggen_utils::error::Error::new(
96            "Workflow name cannot be empty",
97        ));
98    }
99
100    // Validate workflow name length
101    if workflow.len() > 200 {
102        return Err(ggen_utils::error::Error::new(
103            "Workflow name too long (max 200 characters)",
104        ));
105    }
106
107    // Validate workflow name format (basic pattern check)
108    if !workflow
109        .chars()
110        .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
111    {
112        return Err(ggen_utils::error::Error::new(
113            "Invalid workflow name format: only alphanumeric characters, dashes, underscores, and dots allowed",
114        ));
115    }
116
117    Ok(())
118}
119
120/// Validate and sanitize branch name input
121fn validate_branch_name(branch: &str) -> Result<()> {
122    // Validate branch name is not empty
123    if branch.trim().is_empty() {
124        return Err(ggen_utils::error::Error::new("Branch name cannot be empty"));
125    }
126
127    // Validate branch name length
128    if branch.len() > 200 {
129        return Err(ggen_utils::error::Error::new(
130            "Branch name too long (max 200 characters)",
131        ));
132    }
133
134    // Validate branch name format (basic pattern check)
135    if !branch
136        .chars()
137        .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '/' || c == '.')
138    {
139        return Err(ggen_utils::error::Error::new(
140            "Invalid branch name format: only alphanumeric characters, dashes, underscores, slashes, and dots allowed",
141        ));
142    }
143
144    Ok(())
145}
146
147/// Validate and sanitize input parameters
148fn validate_inputs(inputs: &Option<Vec<String>>) -> Result<()> {
149    if let Some(inputs) = inputs {
150        for input in inputs {
151            // Validate input is not empty
152            if input.trim().is_empty() {
153                return Err(ggen_utils::error::Error::new(
154                    "Input parameter cannot be empty",
155                ));
156            }
157
158            // Validate input length
159            if input.len() > 1000 {
160                return Err(ggen_utils::error::Error::new(
161                    "Input parameter too long (max 1000 characters)",
162                ));
163            }
164
165            // Validate input format (key=value)
166            if !input.contains('=') {
167                return Err(ggen_utils::error::Error::new_fmt(format_args!(
168                    "Invalid input format: '{}'. Expected 'key=value'",
169                    input
170                )));
171            }
172        }
173    }
174
175    Ok(())
176}
177
178async fn trigger_workflow(args: &WorkflowTriggerArgs) -> Result<()> {
179    // Validate inputs
180    validate_workflow_name(&args.workflow)?;
181    validate_branch_name(&args.branch)?;
182    validate_inputs(&args.inputs)?;
183
184    println!("๐Ÿš€ Triggering workflow: {}", args.workflow);
185
186    let mut cmd = std::process::Command::new("gh");
187    cmd.args(["workflow", "run", &args.workflow]);
188    cmd.arg("--ref").arg(&args.branch);
189
190    if let Some(inputs) = &args.inputs {
191        for input in inputs {
192            cmd.arg("--input").arg(input);
193        }
194    }
195
196    let output = cmd.output()?;
197
198    if !output.status.success() {
199        let stderr = String::from_utf8_lossy(&output.stderr);
200        return Err(ggen_utils::error::Error::new_fmt(format_args!(
201            "Failed to trigger workflow {}: {}",
202            args.workflow, stderr
203        )));
204    }
205
206    let stdout = String::from_utf8_lossy(&output.stdout);
207    println!("โœ… Workflow {} triggered successfully", args.workflow);
208    println!("{}", stdout);
209    Ok(())
210}
211
212async fn trigger_all_workflows(args: &AllTriggerArgs) -> Result<()> {
213    // Validate inputs
214    validate_branch_name(&args.branch)?;
215
216    println!("๐Ÿš€ Triggering all workflows on branch: {}", args.branch);
217
218    // Get list of workflows
219    let mut list_cmd = std::process::Command::new("gh");
220    list_cmd.args(["workflow", "list"]);
221
222    let list_output = list_cmd.output().map_err(ggen_utils::error::Error::from)?;
223
224    if !list_output.status.success() {
225        let stderr = String::from_utf8_lossy(&list_output.stderr);
226        return Err(ggen_utils::error::Error::new_fmt(format_args!(
227            "Failed to list workflows: {}",
228            stderr
229        )));
230    }
231
232    let stdout = String::from_utf8_lossy(&list_output.stdout);
233    let workflows: Vec<&str> = stdout
234        .lines()
235        .filter_map(|line| line.split_whitespace().next())
236        .collect();
237
238    println!("๐Ÿ“‹ Found {} workflows to trigger", workflows.len());
239
240    for workflow in workflows {
241        println!("๐Ÿš€ Triggering workflow: {}", workflow);
242
243        let mut cmd = std::process::Command::new("gh");
244        cmd.args(["workflow", "run", workflow]);
245        cmd.arg("--ref").arg(&args.branch);
246
247        let output = cmd.output()?;
248
249        if !output.status.success() {
250            let stderr = String::from_utf8_lossy(&output.stderr);
251            println!("โŒ Failed to trigger workflow {}: {}", workflow, stderr);
252            continue;
253        }
254
255        println!("โœ… Workflow {} triggered", workflow);
256    }
257
258    if args.wait {
259        println!("โณ Waiting for workflows to complete...");
260        // This would implement waiting logic
261        // For now, just show a message
262        println!("Use 'ggen ci workflow status' to check progress");
263    }
264
265    Ok(())
266}
267
268async fn trigger_local_testing(args: &LocalTriggerArgs) -> Result<()> {
269    // Validate inputs
270    validate_workflow_name(&args.workflow)?;
271
272    println!("๐Ÿงช Running local testing with act");
273
274    let mut cmd = std::process::Command::new("cargo");
275    cmd.args(["make"]);
276
277    match args.workflow.as_str() {
278        "all" => {
279            if args.light {
280                cmd.arg("act-all-light");
281            } else {
282                cmd.arg("act-all");
283            }
284        }
285        "lint" => {
286            if args.light {
287                cmd.arg("act-lint-light");
288            } else {
289                cmd.arg("act-lint");
290            }
291        }
292        "test" => {
293            if args.light {
294                cmd.arg("act-test-light");
295            } else {
296                cmd.arg("act-test");
297            }
298        }
299        "build" => {
300            if args.light {
301                cmd.arg("act-build-light");
302            } else {
303                cmd.arg("act-build");
304            }
305        }
306        _ => {
307            return Err(ggen_utils::error::Error::new_fmt(format_args!(
308                "Unknown workflow: {}. Valid options: all, lint, test, build",
309                args.workflow
310            )));
311        }
312    }
313
314    if args.dry_run {
315        cmd.arg("act-dry-run");
316    }
317
318    let output = cmd.output()?;
319
320    if !output.status.success() {
321        let stderr = String::from_utf8_lossy(&output.stderr);
322        return Err(ggen_utils::error::Error::new_fmt(format_args!(
323            "Local testing failed: {}",
324            stderr
325        )));
326    }
327
328    let stdout = String::from_utf8_lossy(&output.stdout);
329    println!("โœ… Local testing completed successfully");
330    println!("{}", stdout);
331    Ok(())
332}