Skip to main content

guild_cli/commands/
run.rs

1use std::collections::HashSet;
2use std::path::Path;
3
4use colored::Colorize;
5
6use crate::config::{ProjectName, TargetName, WorkspaceConfig};
7use crate::discovery::{discover_projects, find_workspace_root};
8use crate::error::RunnerError;
9use crate::graph::{ProjectGraph, TaskGraph};
10use crate::runner::{RunResult, TaskRunner};
11
12/// Run a target across the workspace or for a specific project.
13///
14/// If `project` is `Some`, scopes execution to that project and its upstream dependencies.
15/// Returns the run result with success/failure counts.
16pub async fn run_target(
17    cwd: &Path,
18    target: &str,
19    project: Option<&str>,
20) -> Result<RunResult, RunnerError> {
21    // Parse target name
22    let target_name: TargetName = target.parse().map_err(|e| RunnerError::InvalidTarget {
23        target: target.to_string(),
24        reason: format!("{e}"),
25    })?;
26
27    // Discover workspace
28    let root = find_workspace_root(cwd).map_err(|e| RunnerError::WorkspaceNotFound {
29        path: cwd.to_path_buf(),
30        reason: format!("{e}"),
31    })?;
32
33    let workspace = WorkspaceConfig::from_file(&root.join("guild.toml")).map_err(|e| {
34        RunnerError::ConfigError {
35            path: root.join("guild.toml"),
36            reason: format!("{e}"),
37        }
38    })?;
39
40    // Discover projects
41    let all_projects = discover_projects(&workspace).map_err(|e| RunnerError::ConfigError {
42        path: root.clone(),
43        reason: format!("{e}"),
44    })?;
45
46    if all_projects.is_empty() {
47        return Ok(RunResult {
48            success_count: 0,
49            failure_count: 0,
50            skipped_count: 0,
51            cached_count: 0,
52            task_results: vec![],
53            total_duration: std::time::Duration::ZERO,
54        });
55    }
56
57    // Build project graph from all projects
58    let full_project_graph =
59        ProjectGraph::build(all_projects.clone()).map_err(|e| RunnerError::GraphError {
60            reason: format!("{e}"),
61        })?;
62
63    // Optionally filter to specific project + its dependencies
64    let (project_graph, scoped_project) = if let Some(project_name) = project {
65        let project_name: ProjectName =
66            project_name
67                .parse()
68                .map_err(|e| RunnerError::InvalidTarget {
69                    target: project_name.to_string(),
70                    reason: format!("{e}"),
71                })?;
72
73        // Check that the project exists
74        if full_project_graph.get(&project_name).is_none() {
75            return Err(RunnerError::ProjectNotFound {
76                name: project_name.to_string(),
77            });
78        }
79
80        // Collect the project and all its transitive upstream dependencies
81        let required_projects = collect_upstream_projects(&full_project_graph, &project_name);
82
83        // Filter the projects to only those required
84        let filtered_projects: Vec<_> = all_projects
85            .into_iter()
86            .filter(|p| required_projects.contains(p.name()))
87            .collect();
88
89        let filtered_graph =
90            ProjectGraph::build(filtered_projects).map_err(|e| RunnerError::GraphError {
91                reason: format!("{e}"),
92            })?;
93
94        (filtered_graph, Some(project_name))
95    } else {
96        (full_project_graph, None)
97    };
98
99    // Build task graph
100    let task_graph =
101        TaskGraph::build(&project_graph, &target_name).map_err(|e| RunnerError::GraphError {
102            reason: format!("{e}"),
103        })?;
104
105    let task_count = task_graph.len();
106
107    if task_count == 0 {
108        let scope_msg = scoped_project
109            .map(|p| format!(" for project '{p}'"))
110            .unwrap_or_default();
111        println!(
112            "{} No projects have target '{target}'{scope_msg}",
113            "warning:".yellow().bold()
114        );
115        return Ok(RunResult {
116            success_count: 0,
117            failure_count: 0,
118            skipped_count: 0,
119            cached_count: 0,
120            task_results: vec![],
121            total_duration: std::time::Duration::ZERO,
122        });
123    }
124
125    // Print header
126    let scope_msg = scoped_project
127        .as_ref()
128        .map(|p| format!(" (scoped to {p})"))
129        .unwrap_or_default();
130    println!(
131        "\n{} Running {} task{} for target '{}'{}\n",
132        "guild".cyan().bold(),
133        task_count,
134        if task_count == 1 { "" } else { "s" },
135        target,
136        scope_msg
137    );
138
139    // Create runner with reasonable defaults
140    let concurrency = std::thread::available_parallelism()
141        .map(|n| n.get())
142        .unwrap_or(4);
143    let runner = TaskRunner::new(concurrency, root);
144
145    // Execute
146    let result = runner.run(task_graph, &project_graph).await?;
147
148    // Print summary
149    print_summary(&result, target);
150
151    Ok(result)
152}
153
154/// Collect a project and all its transitive upstream dependencies.
155fn collect_upstream_projects(graph: &ProjectGraph, start: &ProjectName) -> HashSet<ProjectName> {
156    let mut result = HashSet::new();
157    let mut to_visit = vec![start.clone()];
158
159    while let Some(name) = to_visit.pop() {
160        if result.contains(&name) {
161            continue;
162        }
163        result.insert(name.clone());
164
165        if let Some(deps) = graph.dependencies(&name) {
166            for dep in deps {
167                if !result.contains(dep) {
168                    to_visit.push(dep.clone());
169                }
170            }
171        }
172    }
173
174    result
175}
176
177/// Print a summary of the run result.
178fn print_summary(result: &RunResult, target: &str) {
179    println!();
180
181    let duration_str = format!("{:.2}s", result.total_duration.as_secs_f64());
182
183    if result.is_success() {
184        if result.success_count == 0 {
185            println!(
186                "{} No tasks executed for target '{target}'",
187                "warning:".yellow().bold()
188            );
189        } else {
190            println!(
191                "{} {} {} completed successfully in {}",
192                "Success".green().bold(),
193                result.success_count,
194                if result.success_count == 1 {
195                    "task"
196                } else {
197                    "tasks"
198                },
199                duration_str
200            );
201        }
202    } else {
203        let mut parts = Vec::new();
204
205        if result.success_count > 0 {
206            parts.push(format!("{} {}", result.success_count, "succeeded".green()));
207        }
208
209        parts.push(format!("{} {}", result.failure_count, "failed".red()));
210
211        if result.skipped_count > 0 {
212            parts.push(format!("{} {}", result.skipped_count, "skipped".yellow()));
213        }
214
215        println!(
216            "{} {} in {}",
217            "Failed".red().bold(),
218            parts.join(", "),
219            duration_str
220        );
221    }
222}