Skip to content

Commit 45d8a36

Browse files
jdxclaudeautofix-ci[bot]
authored
feat(task): allow passing arguments to task dependencies via {{usage.*}} templates (#8893)
## Summary - Task dependencies can now reference parent task arguments using `{{usage.*}}` templates in `depends`, `depends_post`, and `wait_for` - Arguments flow through dependency chains (A -> B -> C) — each task parses its own usage spec and re-renders child dependency templates with resolved values - Works with both positional args (`arg`) and flags (`flag`), and with both string and structured dependency syntax ### Example ```toml [tasks.build] usage = 'arg "<app>"' run = 'echo "building {{usage.app}}"' [tasks.deploy] usage = 'arg "<app>"' depends = [{ task = "build", args = ["{{usage.app}}"] }] run = 'echo "deploying {{usage.app}}"' ``` ``` $ mise run deploy myapp [build] building myapp [deploy] deploying myapp ``` Closes discussion #4331 ## Implementation Dependency templates containing `{{usage.*}}` are deferred during initial config loading (since CLI args aren't available yet). They are re-rendered later with actual usage values at two points: 1. **Top-level tasks** — after CLI args are resolved in `run.rs`, before dependency graph construction 2. **Dependency chain tasks** — during `Deps::new()` graph building, when a task receives args from its parent Key changes: - `Task` stores raw (unrendered) dependency templates in `depends_raw`/`depends_post_raw`/`wait_for_raw` - New `render_depends_with_usage()` method re-renders deps with a `usage` context - New `parse_usage_values_from_task()` parses a task's usage spec against its args to extract named values - Dependency resolution (`resolve_depends`, `all_depends`) skips deps with unresolved `{{usage.*}}` refs ## Test plan - [x] E2E test `test_task_dep_args` covering: - Basic arg passing to dependency - Flag passing to dependency - Multiple parallel dependencies receiving the same arg - String syntax (`depends = ["child {{usage.name}}"]`) - Dependency chaining (A -> B -> C, args flow through all levels) - [x] All existing dep-related e2e tests pass (dep_env, deps, run_depends, depends_post, depends_post_multiple, deps_circular, skip_deps, failure_hang_depends, delegation_dedup, double_dash_behavior, args_position) - [x] All 563 unit tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches task dependency resolution and graph building to defer and later re-render dependency specs, which can affect what tasks run and with what args across chains. Added e2e coverage reduces risk but dependency parsing/rendering changes can have edge cases (e.g., `skip_deps`, mixed template/env syntax). > > **Overview** > Enables `{{usage.*}}` templates inside `depends`, `depends_post`, and `wait_for` so a task can forward its resolved usage args/flags to dependency tasks (including through multi-level dependency chains). > > Implements deferred rendering for dependency entries that reference `{{usage.*}}` by preserving raw dependency templates on `Task`, skipping unresolved deps during initial resolution, then re-rendering them later once CLI/parent-provided args are known (during `mise run` task list creation and again while building the dependency graph). > > Adds an end-to-end test covering positional args, flags, string vs structured dependency syntax, fan-out dependencies, and chained forwarding, and updates docs to describe the new argument-forwarding behavior. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 076b9c5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 34ae864 commit 45d8a36

File tree

6 files changed

+302
-5
lines changed

6 files changed

+302
-5
lines changed

docs/tasks/task-arguments.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ used with Tera's `for` loops and filters like `length`. The `usage` map is
7070
`flag()`) described later on this page—you should not mix the two approaches in
7171
the same task.
7272

73+
<span v-pre>`{{usage.*}}`</span> templates can also be used in `depends`, `depends_post`, and
74+
`wait_for` to forward arguments to dependency tasks. See
75+
[Passing parent task arguments to dependencies](/tasks/task-configuration#passing-parent-task-arguments-to-dependencies)
76+
for details.
77+
7378
**Help output example:**
7479

7580
```shellsession

docs/tasks/task-configuration.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,49 @@ run = "./deploy.sh"
129129

130130
Note: These environment variables are passed only to the specified dependency, not to the current task or other dependencies.
131131

132+
#### Passing parent task arguments to dependencies
133+
134+
You can forward a parent task's arguments to its dependencies using <span v-pre>`{{usage.*}}`</span> templates.
135+
Both the parent and child tasks must define a `usage` spec for the arguments they accept:
136+
137+
```mise-toml
138+
[tasks.build]
139+
usage = 'arg "<app>"'
140+
run = 'echo "building {{usage.app}}"'
141+
142+
[tasks.deploy]
143+
usage = 'arg "<app>"'
144+
depends = [{ task = "build", args = ["{{usage.app}}"] }]
145+
run = 'echo "deploying {{usage.app}}"'
146+
```
147+
148+
Running `mise run deploy myapp` passes `"myapp"` to both `deploy` and its `build` dependency.
149+
150+
This also works with the string syntax:
151+
152+
```mise-toml
153+
[tasks.deploy]
154+
usage = 'arg "<app>"'
155+
depends = ["build {{usage.app}}"]
156+
run = 'echo "deploying {{usage.app}}"'
157+
```
158+
159+
And with flags:
160+
161+
```mise-toml
162+
[tasks.compile]
163+
usage = 'flag "--target <target>"'
164+
run = 'echo "compiling for $usage_target"'
165+
166+
[tasks.package]
167+
usage = 'flag "--target <target>"'
168+
depends = [{ task = "compile", args = ["--target", "{{usage.target}}"] }]
169+
run = 'echo "packaging for $usage_target"'
170+
```
171+
172+
Arguments flow through dependency chains — if A depends on B which depends on C, each task can
173+
forward its resolved arguments to its own dependencies.
174+
132175
### `depends_post`
133176

134177
- **Type**: `string | string[] | { task: string, args?: string[], env?: { [key]: string } }[]`

e2e/tasks/test_task_dep_args

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env bash
2+
3+
# Test passing arguments to task dependencies via {{usage.*}} templates
4+
5+
# Basic test: parent task passes its arg to a dependency
6+
cat <<'EOF' >mise.toml
7+
[tasks.build]
8+
usage = 'arg "<app>"'
9+
run = 'echo "building {{usage.app}}"'
10+
11+
[tasks.deploy]
12+
usage = 'arg "<app>"'
13+
depends = [{ task = "build", args = ["{{usage.app}}"] }]
14+
run = 'echo "deploying {{usage.app}}"'
15+
EOF
16+
17+
assert_contains "mise run deploy myapp" "building myapp"
18+
assert_contains "mise run deploy myapp" "deploying myapp"
19+
20+
# Test with flag syntax
21+
cat <<'EOF' >mise.toml
22+
[tasks.compile]
23+
usage = 'flag "--target <target>"'
24+
run = 'echo "compiling for $usage_target"'
25+
26+
[tasks.package]
27+
usage = 'flag "--target <target>"'
28+
depends = [{ task = "compile", args = ["--target", "{{usage.target}}"] }]
29+
run = 'echo "packaging for $usage_target"'
30+
EOF
31+
32+
assert_contains "mise run package --target linux" "compiling for linux"
33+
assert_contains "mise run package --target linux" "packaging for linux"
34+
35+
# Test with multiple args
36+
cat <<'EOF' >mise.toml
37+
[tasks.start-backend]
38+
usage = 'arg "<app>"'
39+
run = 'echo "backend {{usage.app}}"'
40+
41+
[tasks.start-frontend]
42+
usage = 'arg "<app>"'
43+
run = 'echo "frontend {{usage.app}}"'
44+
45+
[tasks.start]
46+
usage = 'arg "<app>"'
47+
depends = [
48+
{ task = "start-backend", args = ["{{usage.app}}"] },
49+
{ task = "start-frontend", args = ["{{usage.app}}"] },
50+
]
51+
run = 'echo "started {{usage.app}}"'
52+
EOF
53+
54+
output=$(mise run start myapp 2>&1)
55+
assert_contains "echo \"$output\"" "backend myapp"
56+
assert_contains "echo \"$output\"" "frontend myapp"
57+
assert_contains "echo \"$output\"" "started myapp"
58+
59+
# Test with string syntax for depends args
60+
cat <<'EOF' >mise.toml
61+
[tasks.greet]
62+
usage = 'arg "<name>"'
63+
run = 'echo "hello {{usage.name}}"'
64+
65+
[tasks.welcome]
66+
usage = 'arg "<name>"'
67+
depends = ["greet {{usage.name}}"]
68+
run = 'echo "welcome {{usage.name}}"'
69+
EOF
70+
71+
assert_contains "mise run welcome world" "hello world"
72+
assert_contains "mise run welcome world" "welcome world"
73+
74+
# Test dependency chaining: A -> B -> C, args flow through
75+
cat <<'EOF' >mise.toml
76+
[tasks.step1]
77+
usage = 'arg "<val>"'
78+
run = 'echo "step1 {{usage.val}}"'
79+
80+
[tasks.step2]
81+
usage = 'arg "<val>"'
82+
depends = [{ task = "step1", args = ["{{usage.val}}"] }]
83+
run = 'echo "step2 {{usage.val}}"'
84+
85+
[tasks.step3]
86+
usage = 'arg "<val>"'
87+
depends = [{ task = "step2", args = ["{{usage.val}}"] }]
88+
run = 'echo "step3 {{usage.val}}"'
89+
EOF
90+
91+
output=$(mise run step3 hello 2>&1)
92+
assert_contains "echo \"$output\"" "step1 hello"
93+
assert_contains "echo \"$output\"" "step2 hello"
94+
assert_contains "echo \"$output\"" "step3 hello"

src/cli/run.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,18 @@ impl Run {
324324
task.args.extend(self.args_last.clone());
325325
}
326326
}
327+
328+
// Re-render dependency templates with parent task's usage arg/flag values.
329+
// This enables patterns like: depends = ["child {{usage.app}}"]
330+
for task in &mut task_list {
331+
if !task.args.is_empty() {
332+
let usage_values = crate::task::parse_usage_values_from_task(&config, task).await?;
333+
if !usage_values.is_empty() {
334+
task.render_depends_with_usage(&config, &usage_values)
335+
.await?;
336+
}
337+
}
338+
}
327339
time!("run get_task_lists");
328340

329341
// Resolve transitive dependencies once upfront so we can:

src/task/deps.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::config::env_directive::EnvDirective;
2-
use crate::task::Task;
2+
use crate::task::{Task, dep_has_usage_ref, parse_usage_values_from_task};
33
use crate::{config::Config, task::task_list::resolve_depends};
44
use itertools::Itertools;
55
use petgraph::Direction;
@@ -63,11 +63,27 @@ impl Deps {
6363
add_idx(t, &mut graph);
6464
}
6565
let all_tasks_to_run = resolve_depends(config, tasks).await?;
66-
while let Some(a) = stack.pop() {
66+
while let Some(mut a) = stack.pop() {
6767
if seen.contains(&a) {
6868
// prevent infinite loop
6969
continue;
7070
}
71+
// If this task received args (from a parent dependency), re-render
72+
// its dependency templates with usage values so {{usage.*}} resolves.
73+
let has_usage_deps = |raw: &Option<Vec<_>>| {
74+
raw.as_ref()
75+
.is_some_and(|r| r.iter().any(dep_has_usage_ref))
76+
};
77+
if !a.args.is_empty()
78+
&& (has_usage_deps(&a.depends_raw)
79+
|| has_usage_deps(&a.depends_post_raw)
80+
|| has_usage_deps(&a.wait_for_raw))
81+
{
82+
let usage_values = parse_usage_values_from_task(config, &a).await?;
83+
if !usage_values.is_empty() {
84+
a.render_depends_with_usage(config, &usage_values).await?;
85+
}
86+
}
7187
let a_idx = add_idx(&a, &mut graph);
7288
let (pre, post) = a.resolve_depends(config, &all_tasks_to_run).await?;
7389
for b in pre {

0 commit comments

Comments
 (0)