Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions docs/tasks/task-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ run = "cargo build"

### `depends`

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

Tasks that must be run before this task. This is a list of task names or aliases. Arguments can be
passed to the task, e.g.: `depends = ["build --release"]`. If multiple tasks have the same dependency,
Expand All @@ -90,9 +90,46 @@ depends = ["build"]
run = "cargo test"
```

#### Passing environment variables to dependencies

You can pass environment variables to specific dependencies using two syntaxes:

**Shell-style inline:**

```mise-toml
[tasks.test]
depends = ["NODE_ENV=test setup"]
run = "npm test"

[tasks.setup]
run = 'echo "Setting up for $NODE_ENV"'
```

**Structured object format:**

```mise-toml
[tasks.test]
depends = [
{ task = "setup", env = { NODE_ENV = "test", DEBUG = "true" } }
]
run = "npm test"
```

The structured format also supports combining env vars with arguments:

```mise-toml
[tasks.deploy]
depends = [
{ task = "build", args = ["--release"], env = { RUSTFLAGS = "-C opt-level=3" } }
]
run = "./deploy.sh"
```

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

### `depends_post`

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

Like `depends` but these tasks run _after_ this task and its dependencies complete. For example, you
may want a `postlint` task that you can run individually without also running `lint`:
Expand All @@ -105,9 +142,11 @@ depends_post = ["postlint"]
run = "echo 'linting complete'"
```

Supports the same argument and environment variable syntax as `depends`.

### `wait_for`

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

Similar to `depends`, it will wait for these tasks to complete before running however they won't be
added to the list of tasks to run. This is essentially optional dependencies.
Expand All @@ -118,6 +157,8 @@ wait_for = ["render"] # creates some js files, so if it's running, wait for it t
run = "eslint ."
```

Supports the same argument and environment variable syntax as `depends`.

### `env`

- **Type**: `{ [key]: string | int | bool }`
Expand Down
42 changes: 42 additions & 0 deletions e2e/tasks/test_task_dep_env
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash

# Test passing env vars to task dependencies

# Test shell-style env var syntax: "FOO=bar mytask"
cat <<EOF >mise.toml
[tasks.show-env]
run = 'echo "FOO=\$FOO BAZ=\$BAZ"'

[tasks.with-env-string]
depends = ["FOO=hello show-env"]
run = 'echo done'

[tasks.with-multiple-env]
depends = ["FOO=hello BAZ=world show-env"]
run = 'echo done'
EOF

assert_contains "mise run with-env-string" "FOO=hello"
assert_contains "mise run with-multiple-env" "FOO=hello BAZ=world"

# Test structured object syntax: { task = "mytask", env = { FOO = "bar" } }
cat <<EOF >mise.toml
[tasks.show-env]
run = 'echo "FOO=\$FOO BAZ=\$BAZ"'

[tasks.with-env-object]
depends = [{ task = "show-env", env = { FOO = "hello", BAZ = "world" } }]
run = 'echo done'

[tasks.with-env-and-args]
depends = [{ task = "echo-args", env = { FOO = "test" }, args = ["arg1", "arg2"] }]
run = 'echo done'

[tasks.echo-args]
run = 'echo "FOO=\$FOO" && echo'

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test verifies that FOO=test appears in output but doesn't validate that args arg1 and arg2 are actually passed to the echo-args task. The task's run command only echoes the FOO variable and doesn't incorporate the args, making line 42's assertion about args appearing in output unlikely to succeed as intended.

Copilot uses AI. Check for mistakes.
EOF

assert_contains "mise run with-env-object" "FOO=hello BAZ=world"
assert_contains "mise run with-env-and-args" "FOO=test"
# Args are appended to the command line, verify they appear in output
assert_contains "mise run with-env-and-args" "arg1 arg2"
29 changes: 23 additions & 6 deletions src/task/deps.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::config::env_directive::EnvDirective;
use crate::task::Task;
use crate::{config::Config, task::task_list::resolve_depends};
use itertools::Itertools;
Expand All @@ -9,17 +10,33 @@ use std::{
};
use tokio::sync::mpsc;

/// Unique key for a task instance, including name, args, and env vars
pub type TaskKey = (String, Vec<String>, Vec<(String, String)>);

#[derive(Debug, Clone)]
pub struct Deps {
pub graph: DiGraph<Task, ()>,
sent: HashSet<(String, Vec<String>)>, // tasks+args that have already started so should not run again
removed: HashSet<(String, Vec<String>)>, // tasks+args that have already finished to track if we are in an infinitve loop
sent: HashSet<TaskKey>, // tasks that have already started so should not run again
removed: HashSet<TaskKey>, // tasks that have already finished to track if we are in an infinitve loop
tx: mpsc::UnboundedSender<Option<Task>>,
// not clone, notify waiters via tx None
}

pub fn task_key(task: &Task) -> (String, Vec<String>) {
(task.name.clone(), task.args.clone())
/// Extract a hashable key from a task, including env vars set via dependencies
pub fn task_key(task: &Task) -> TaskKey {
// Extract simple key-value env vars for deduplication
// This ensures tasks with same name/args but different env are treated as distinct
let env_key: Vec<(String, String)> = task
.env
.0
.iter()
.filter_map(|d| match d {
EnvDirective::Val(k, v, _) => Some((k.clone(), v.clone())),
_ => None,
})
.sorted()
.collect();
(task.name.clone(), task.args.clone(), env_key)
Comment thread
cursor[bot] marked this conversation as resolved.
}

/// manages a dependency graph of tasks so `mise run` knows what to run next
Expand Down Expand Up @@ -79,7 +96,7 @@ impl Deps {
let leaves_is_empty = leaves.is_empty();

for task in leaves {
let key = (task.name.clone(), task.args.clone());
let key = task_key(&task);

if self.sent.insert(key) {
trace!("Scheduling task {0}", task.name);
Expand Down Expand Up @@ -121,7 +138,7 @@ impl Deps {
pub fn remove(&mut self, task: &Task) {
if let Some(idx) = self.node_idx(task) {
self.graph.remove_node(idx);
let key = (task.name.clone(), task.args.clone());
let key = task_key(task);
self.removed.insert(key);
self.emit_leaves();
}
Expand Down
60 changes: 55 additions & 5 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ pub struct Task {
pub wait_for: Vec<TaskDep>,
#[serde(default)]
pub env: EnvList,
/// Env vars inherited from parent tasks at runtime (not used for task identity/deduplication)
#[serde(skip)]
pub inherited_env: EnvList,
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
Expand Down Expand Up @@ -373,7 +376,8 @@ impl Task {

pub fn derive_env(&self, env_directives: &[EnvDirective]) -> Self {
let mut new_task = self.clone();
new_task.env.0.extend_from_slice(env_directives);
// Put inherited env in separate field so it doesn't affect task identity/deduplication
new_task.inherited_env.0.extend_from_slice(env_directives);
new_task
}

Expand Down Expand Up @@ -861,10 +865,12 @@ impl Task {

// Convert task env directives to (EnvDirective, PathBuf) pairs
// Use the config file path as source for proper path resolution
let env_directives = self
.env
// Include inherited_env first (so task's own env can override it)
let env_directives: Vec<_> = self
.inherited_env
.0
.iter()
.chain(self.env.0.iter())
.map(|directive| (directive.clone(), self.config_source.clone()))
.collect();

Expand Down Expand Up @@ -1014,6 +1020,15 @@ fn match_tasks_with_context(
.map(|t| {
let mut t = (*t).clone();
t.args = td.args.clone();
// Apply env vars from dependency
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
if !td.env.is_empty() {
let env_directives: Vec<EnvDirective> = td
.env
.iter()
.map(|(k, v)| EnvDirective::Val(k.clone(), v.clone(), Default::default()))
.collect();
t = t.derive_env(&env_directives);
}
t
})
.collect_vec();
Expand Down Expand Up @@ -1064,6 +1079,7 @@ impl Default for Task {
depends_post: vec![],
wait_for: vec![],
env: Default::default(),
inherited_env: Default::default(),
dir: None,
hide: false,
global: false,
Expand Down Expand Up @@ -1115,10 +1131,27 @@ impl PartialOrd for Task {
}
}

/// Extract sorted env key-value pairs from task's own env (not inherited_env)
/// Used for consistent comparison/hashing of task identity
fn env_key(task: &Task) -> Vec<(&String, &String)> {
task.env
.0
.iter()
.filter_map(|d| match d {
EnvDirective::Val(k, v, _) => Some((k, v)),
_ => None,
})
.sorted()
.collect()
}

impl Ord for Task {
fn cmp(&self, other: &Self) -> Ordering {
match self.name.cmp(&other.name) {
Ordering::Equal => self.args.cmp(&other.args),
Ordering::Equal => match self.args.cmp(&other.args) {
Ordering::Equal => env_key(self).cmp(&env_key(other)),
o => o,
},
o => o,
}
}
Expand All @@ -1128,13 +1161,18 @@ impl Hash for Task {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.args.iter().for_each(|arg| arg.hash(state));
// Include task's own env (not inherited_env) for deduplication
for (k, v) in env_key(self) {
k.hash(state);
v.hash(state);
}
}
}

impl Eq for Task {}
impl PartialEq for Task {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.args == other.args
Comment thread
cursor[bot] marked this conversation as resolved.
self.name == other.name && self.args == other.args && env_key(self) == env_key(other)
}
}

Expand Down Expand Up @@ -1407,10 +1445,12 @@ mod tests {
TaskDep {
task: "post1".to_string(),
args: vec![],
env: Default::default(),
},
TaskDep {
task: "post2".to_string(),
args: vec![],
env: Default::default(),
},
],
..Default::default()
Expand All @@ -1422,6 +1462,7 @@ mod tests {
depends_post: vec![TaskDep {
task: "other_post".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand Down Expand Up @@ -1759,6 +1800,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "task_b".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand All @@ -1768,6 +1810,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "task_a".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand Down Expand Up @@ -1795,6 +1838,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "task_b".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand All @@ -1804,6 +1848,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "task_c".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand All @@ -1813,6 +1858,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "task_a".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand Down Expand Up @@ -1842,10 +1888,12 @@ echo "hello world"
crate::task::task_dep::TaskDep {
task: "task_a".to_string(),
args: vec![],
env: Default::default(),
},
crate::task::task_dep::TaskDep {
task: "task_b".to_string(),
args: vec![],
env: Default::default(),
},
],
..Default::default()
Expand All @@ -1856,6 +1904,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "common".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand All @@ -1865,6 +1914,7 @@ echo "hello world"
depends: vec![crate::task::task_dep::TaskDep {
task: "common".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
Expand Down
Loading
Loading