Skip to content

Commit c606025

Browse files
pepicrftclaude
andcommitted
feat: implement pipeline execution with confirmation prompts
- GitLab CI: Execute scripts directly with user confirmation - Show all commands before execution - Execute sequentially using tokio::process::Command - Display progress and status for each command - Stop on first failure with exit code - GitHub Actions: Display job details and recommend using act - Forgejo Actions: Display job details and recommend using act - Update README with execution documentation - Add test fixture for quick verification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent db6698b commit c606025

6 files changed

Lines changed: 100 additions & 28 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ clap = { version = "4.5", features = ["derive"] }
1414
serde = { version = "1.0", features = ["derive"] }
1515
serde_yaml = "0.9"
1616
serde_json = "1.0"
17-
tokio = { version = "1.41", features = ["full"] }
17+
tokio = { version = "1.41", features = ["full", "process"] }
1818
colored = "2.1"
1919
anyhow = "1.0"
2020
inquire = "0.7"

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ magnolia .github/workflows/test.yml
2222
magnolia .forgejo/workflows/deploy.yml
2323
```
2424

25+
### Execution
26+
27+
- **GitLab CI**: Scripts are executed directly on your machine after confirmation. You'll see each command before it runs.
28+
- **GitHub Actions / Forgejo Actions**: Displays job details and recommends using [act](https://github.com/nektos/act) for local execution with Docker containers.
29+
2530
## Supported Systems
2631

2732
- GitLab CI (`.gitlab-ci.yml`)

fixtures/test-gitlab.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
test-job:
2+
script:
3+
- echo "Starting test"
4+
- echo "Running step 2"
5+
- echo "Test completed successfully"

src/forgejo.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,14 @@ pub async fn run_job_from_file(pipeline_path: &PathBuf, job_name: &str) -> Resul
108108
.context(format!("Failed to parse {}", pipeline_path.display()))?;
109109

110110
if let Some(job) = workflow.jobs.get(job_name) {
111-
println!("{}", format!("Executing job: {}", job_name).green().bold());
111+
println!("\n{}", format!("▶ Analyzing job: {}", job_name).cyan().bold());
112112
println!(
113-
"Runs on: {}",
113+
" Runs on: {}",
114114
job.runs_on.as_deref().unwrap_or("N/A").blue()
115115
);
116116

117117
if let Some(container) = &job.container {
118-
println!("Container: {}", container.dimmed());
118+
println!(" Container: {}", container.dimmed());
119119
}
120120

121121
if let Some(steps) = &job.steps {
@@ -135,11 +135,15 @@ pub async fn run_job_from_file(pipeline_path: &PathBuf, job_name: &str) -> Resul
135135
}
136136
}
137137

138-
println!(
139-
"\n{}",
140-
"Note: Actual execution is not yet implemented.".yellow()
141-
);
142-
println!("This would execute the above steps in the specified environment.");
138+
println!("\n{}", "─".repeat(60).dimmed());
139+
println!("\n{}", "ℹ Forgejo Actions Execution".yellow().bold());
140+
println!("Forgejo Actions are compatible with GitHub Actions and use marketplace actions.");
141+
println!("\nFor local execution, consider using:");
142+
println!(" • {} - Run GitHub Actions locally using Docker", "act".cyan());
143+
println!(" Install: brew install act");
144+
println!(" Usage: act -j {}", job_name);
145+
println!("\n{}", "─".repeat(60).dimmed());
146+
143147
return Ok(());
144148
}
145149

src/github.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ pub async fn run_job_from_file(pipeline_path: &PathBuf, job_name: &str) -> Resul
9696
.context(format!("Failed to parse {}", pipeline_path.display()))?;
9797

9898
if let Some(job) = workflow.jobs.get(job_name) {
99-
println!("{}", format!("Executing job: {}", job_name).green().bold());
99+
println!("\n{}", format!("▶ Analyzing job: {}", job_name).cyan().bold());
100100
println!(
101-
"Runs on: {}",
101+
" Runs on: {}",
102102
job.runs_on.as_deref().unwrap_or("N/A").blue()
103103
);
104104

@@ -119,11 +119,16 @@ pub async fn run_job_from_file(pipeline_path: &PathBuf, job_name: &str) -> Resul
119119
}
120120
}
121121

122-
println!(
123-
"\n{}",
124-
"Note: Actual execution is not yet implemented.".yellow()
125-
);
126-
println!("This would execute the above steps in the specified environment.");
122+
println!("\n{}", "─".repeat(60).dimmed());
123+
println!("\n{}", "ℹ GitHub Actions Execution".yellow().bold());
124+
println!("GitHub Actions workflows use marketplace actions (e.g., actions/checkout@v4)");
125+
println!("and require a GitHub Actions-compatible runtime.");
126+
println!("\nFor local execution, consider using:");
127+
println!(" • {} - Run GitHub Actions locally using Docker", "act".cyan());
128+
println!(" Install: brew install act");
129+
println!(" Usage: act -j {}", job_name);
130+
println!("\n{}", "─".repeat(60).dimmed());
131+
127132
return Ok(());
128133
}
129134

src/gitlab.rs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,80 @@ pub async fn run_job_from_file(pipeline_path: &PathBuf, job_name: &str) -> Resul
127127
if let Some(obj) = ci.as_mapping() {
128128
if let Some(job) = obj.get(&serde_yaml::Value::String(job_name.to_string())) {
129129
if let Some(job_map) = job.as_mapping() {
130-
println!("{}", format!("Executing job: {}", job_name).green().bold());
130+
println!("\n{}", format!("▶ Executing job: {}", job_name).green().bold());
131+
132+
// Get job metadata
133+
let stage = job_map
134+
.get(&serde_yaml::Value::String("stage".to_string()))
135+
.and_then(|v| v.as_str())
136+
.unwrap_or("default");
137+
138+
let image = job_map
139+
.get(&serde_yaml::Value::String("image".to_string()))
140+
.and_then(|v| v.as_str());
141+
142+
println!(" Stage: {}", stage.blue());
143+
if let Some(img) = image {
144+
println!(" Image: {}", img.dimmed());
145+
}
131146

132147
// Get the script
133148
if let Some(script) = job_map.get(&serde_yaml::Value::String("script".to_string()))
134149
{
135150
if let Some(script_array) = script.as_sequence() {
136-
println!("\n{}", "Script commands:".cyan());
137-
for cmd in script_array {
138-
if let Some(cmd_str) = cmd.as_str() {
139-
println!(" {}", cmd_str.dimmed());
151+
let commands: Vec<String> = script_array
152+
.iter()
153+
.filter_map(|cmd| cmd.as_str().map(String::from))
154+
.collect();
155+
156+
if commands.is_empty() {
157+
println!("\n{}", "No script commands found".yellow());
158+
return Ok(());
159+
}
160+
161+
println!("\n{}", "Commands to execute:".cyan());
162+
for cmd in &commands {
163+
println!(" $ {}", cmd.dimmed());
164+
}
165+
166+
// Ask for confirmation
167+
let confirm = inquire::Confirm::new("Execute these commands?")
168+
.with_default(false)
169+
.prompt()
170+
.unwrap_or(false);
171+
172+
if !confirm {
173+
println!("{}", "Execution cancelled".yellow());
174+
return Ok(());
175+
}
176+
177+
println!("\n{}", "─".repeat(60).dimmed());
178+
179+
// Execute commands
180+
for (i, cmd) in commands.iter().enumerate() {
181+
println!("\n{} {}", format!("[{}/{}]", i + 1, commands.len()).cyan(), cmd.yellow());
182+
183+
let status = tokio::process::Command::new("sh")
184+
.arg("-c")
185+
.arg(cmd)
186+
.status()
187+
.await
188+
.context(format!("Failed to execute command: {}", cmd))?;
189+
190+
if !status.success() {
191+
let code = status.code().unwrap_or(-1);
192+
println!("\n{}", format!("✗ Command failed with exit code {}", code).red().bold());
193+
anyhow::bail!("Command failed: {}", cmd);
194+
} else {
195+
println!("{}", format!("✓ Command succeeded").green());
140196
}
141197
}
142198

143-
println!(
144-
"\n{}",
145-
"Note: Actual execution is not yet implemented.".yellow()
146-
);
147-
println!(
148-
"This would execute the above commands in the specified environment."
149-
);
199+
println!("\n{}", "─".repeat(60).dimmed());
200+
println!("\n{}", format!("✓ Job '{}' completed successfully", job_name).green().bold());
150201
}
202+
} else {
203+
println!("\n{}", "No script defined for this job".yellow());
151204
}
152205

153206
return Ok(());

0 commit comments

Comments
 (0)