Files
wrkflw/tests/reusable_workflow_execution_test.rs
Gokul 452044f9d2 feat(cli): add --job flag to run a specific job and --jobs to list them (#77)
* feat(cli): add --job flag to run a specific job and --jobs to list them

Until now, wrkflw only operated at the workflow level. You could
run an entire workflow or list workflow files, but if you wanted to
debug a single failing job you had to sit through every other job
first. This is not great.

Add `--job <name>` to `wrkflw run` so you can execute exactly one
job in isolation, skipping dependency resolution entirely. Add
`--jobs` to `wrkflw list` so you can actually *see* what jobs are
available before running them. Both work for GitHub workflows and
GitLab pipelines.

The filtering happens after dependency resolution — we just replace
the execution plan with a single-job batch. If the job name doesn't
exist, we tell you what's available instead of silently doing
nothing. The TUI still runs full workflows; job selection there is
a separate concern.

Closes #68

* fix(executor): include transitive deps when running a single job

The --job flag was replacing the entire execution plan with just the
target job, silently dropping all its dependencies. So if you ran
--job deploy and deploy needs build which needs setup, you'd get
deploy running alone with none of its prerequisites. Confusion ensues.

Extract the duplicated inline filtering (copy-pasted verbatim across
both the GitHub and GitLab execution paths) into a shared
filter_plan_to_job() helper in dependency.rs. The new logic walks
the needs graph via BFS to collect transitive deps, then prunes the
existing topologically-sorted plan to only include relevant jobs
while preserving batch ordering.

Add 9 unit tests covering the dependency collection and plan
filtering — linear chains, diamond graphs, partial subgraph
isolation, error paths, and empty batch removal.

* fix(executor): use stage-aware filtering for GitLab --job flag

It turns out that filter_plan_to_job walks `needs` edges to find
transitive dependencies, which works fine for GitHub workflows. But
GitLab pipelines use *stage ordering* for implicit dependencies, and
convert_to_workflow_format sets `needs: None` on every converted
job. So running `--job deploy` on a GitLab pipeline would silently
drop all build and test jobs. Not great.

Add filter_plan_to_job_by_stage that understands the GitLab model:
keep all jobs in earlier stage batches (they're implicit deps) and
filter only the target's own batch down to just the target job.
The GitHub workflow path continues using the needs-based filter.

While at it, extract the job-not-found error into a shared helper
and add proper test coverage: 6 unit tests for the stage-aware
filter plus 3 integration tests exercising the full execute_workflow
path with target_job set.
2026-04-02 13:22:58 +05:30

123 lines
3.1 KiB
Rust

use std::fs;
use tempfile::tempdir;
use wrkflw::executor::engine::{execute_workflow, ExecutionConfig, RuntimeType};
fn write_file(path: &std::path::Path, content: &str) {
fs::write(path, content).expect("failed to write file");
}
#[tokio::test]
async fn test_local_reusable_workflow_execution_success() {
// Create temp workspace
let dir = tempdir().unwrap();
let called_path = dir.path().join("called.yml");
let caller_path = dir.path().join("caller.yml");
// Minimal called workflow with one successful job
let called = r#"
name: Called
on: workflow_dispatch
jobs:
inner:
runs-on: ubuntu-latest
steps:
- run: echo "hello from called"
"#;
write_file(&called_path, called);
// Caller workflow that uses the called workflow via absolute local path
let caller = format!(
r#"
name: Caller
on: workflow_dispatch
jobs:
call:
uses: {}
with:
foo: bar
secrets:
token: testsecret
"#,
called_path.display()
);
write_file(&caller_path, &caller);
// Execute caller workflow with emulation runtime
let cfg = ExecutionConfig {
runtime_type: RuntimeType::Emulation,
verbose: false,
preserve_containers_on_failure: false,
target_job: None,
};
let result = execute_workflow(&caller_path, cfg)
.await
.expect("workflow execution failed");
// Expect a single caller job summarized
assert_eq!(result.jobs.len(), 1, "expected one caller job result");
let job = &result.jobs[0];
assert_eq!(job.name, "call");
assert_eq!(format!("{:?}", job.status), "Success");
// Summary step should include reference to called workflow and inner job status
assert!(job
.logs
.contains("Called workflow:"),
"expected summary logs to include called workflow path");
assert!(job.logs.contains("- inner: Success"), "expected inner job success in summary");
}
#[tokio::test]
async fn test_local_reusable_workflow_execution_failure_propagates() {
// Create temp workspace
let dir = tempdir().unwrap();
let called_path = dir.path().join("called.yml");
let caller_path = dir.path().join("caller.yml");
// Called workflow with failing job
let called = r#"
name: Called
on: workflow_dispatch
jobs:
inner:
runs-on: ubuntu-latest
steps:
- run: false
"#;
write_file(&called_path, called);
// Caller workflow
let caller = format!(
r#"
name: Caller
on: workflow_dispatch
jobs:
call:
uses: {}
"#,
called_path.display()
);
write_file(&caller_path, &caller);
// Execute caller workflow
let cfg = ExecutionConfig {
runtime_type: RuntimeType::Emulation,
verbose: false,
preserve_containers_on_failure: false,
target_job: None,
};
let result = execute_workflow(&caller_path, cfg)
.await
.expect("workflow execution failed");
assert_eq!(result.jobs.len(), 1);
let job = &result.jobs[0];
assert_eq!(job.name, "call");
assert_eq!(format!("{:?}", job.status), "Failure");
assert!(job.logs.contains("- inner: Failure"));
}