Skip to content

Commit 8a44caf

Browse files
committed
feat(tasks): monorepo vars and per-task vars
1 parent 0b6903b commit 8a44caf

File tree

8 files changed

+264
-7
lines changed

8 files changed

+264
-7
lines changed

docs/tasks/monorepo.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,17 @@ mise '//...:test*'
175175
mise //.../frontend:build
176176
```
177177

178-
## Tool and Environment Layering
178+
## Tool, Environment, and Vars Layering
179179

180180
Subdirectory tasks automatically use tools and environment variables from parent config files in the hierarchy. However, each subdirectory can also define its own tools and environment variables in its config_root. This allows you to:
181181

182182
1. Define common tools and environment at the monorepo root
183183
2. Override tools or environment in specific subdirectories
184184
3. Add additional tools or environment in subdirectories
185185

186+
`vars` follow the same hierarchy for task templating, so child config vars are available when
187+
running subdirectory tasks from the monorepo root.
188+
186189
### Layering Example
187190

188191
```toml

docs/tasks/task-configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,14 @@ e2e_args = '--headless'
485485
run = './scripts/test-e2e.sh {{vars.e2e_args}}'
486486
```
487487

488+
Tasks can also define task-local vars that override config vars for that task:
489+
490+
```mise-toml
491+
[tasks.test]
492+
vars = { e2e_args = "--headed" }
493+
run = './scripts/test-e2e.sh {{vars.e2e_args}}'
494+
```
495+
488496
Like most configuration in mise, vars can be defined across several files. So for example, you could
489497
put some vars in your global mise config `~/.config/mise/config.toml`, use them in a task at
490498
`~/src/work/myproject/mise.toml`. You can also override those vars in "later" config files such
@@ -532,6 +540,7 @@ task3 = "echo task3"
532540
533541
[task4]
534542
run = "echo task4"
543+
vars = { target = "linux" }
535544
```
536545

537546
:::
@@ -616,6 +625,7 @@ passed as environment variables to the scripts. They are defined in the `vars` s
616625
e2e_args = '--headless'
617626
[tasks.test]
618627
run = './scripts/test-e2e.sh {{vars.e2e_args}}'
628+
vars = { e2e_args = '--headed' }
619629
```
620630

621631
Like `[env]`, vars can also be read in as a file:

schema/mise-task.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,9 @@
550550
"env": {
551551
"$ref": "#/$defs/env"
552552
},
553+
"vars": {
554+
"$ref": "#/$defs/vars"
555+
},
553556
"tools": {
554557
"description": "tools to install/activate before running this task",
555558
"additionalProperties": {
@@ -711,6 +714,55 @@
711714
}
712715
},
713716
"type": "object"
717+
},
718+
"vars": {
719+
"description": "variables to set",
720+
"type": "object",
721+
"properties": {
722+
"_": {
723+
"description": "vars modules",
724+
"additionalProperties": true,
725+
"properties": {
726+
"file": {
727+
"oneOf": [
728+
{
729+
"description": "dotenv file to load",
730+
"type": "string"
731+
},
732+
{
733+
"description": "dotenv files to load",
734+
"items": {
735+
"description": "dotenv file to load",
736+
"type": "string"
737+
},
738+
"type": "array"
739+
}
740+
]
741+
},
742+
"source": {
743+
"oneOf": [
744+
{
745+
"description": "bash script to load",
746+
"type": "string"
747+
},
748+
{
749+
"description": "bash scripts to load",
750+
"items": {
751+
"description": "bash script to load",
752+
"type": "string"
753+
},
754+
"type": "array"
755+
}
756+
]
757+
}
758+
},
759+
"type": "object"
760+
}
761+
},
762+
"additionalProperties": {
763+
"description": "value of variable",
764+
"type": "string"
765+
}
714766
}
715767
},
716768
"description": "Config file for included mise tasks (https://mise.jdx.dev/tasks/#task-configuration)",

schema/mise.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,9 @@
20372037
"env": {
20382038
"$ref": "#/$defs/env"
20392039
},
2040+
"vars": {
2041+
"$ref": "#/$defs/vars"
2042+
},
20402043
"tools": {
20412044
"description": "tools to install/activate before running this task",
20422045
"additionalProperties": {

src/config/mod.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl Config {
112112
*GLOBAL_CONFIG_FILES.lock().unwrap() = None;
113113
*SYSTEM_CONFIG_FILES.lock().unwrap() = None;
114114
GLOB_RESULTS.lock().unwrap().clear();
115+
crate::task::reset();
115116
Ok(())
116117
},
117118
Duration::from_secs(5),
@@ -1519,10 +1520,11 @@ fn load_plugins(config_files: &ConfigMap) -> Result<HashMap<String, String>> {
15191520
Ok(plugins)
15201521
}
15211522

1522-
async fn load_vars(config: &Arc<Config>) -> Result<EnvResults> {
1523-
time!("load_vars start");
1524-
let entries = config
1525-
.config_files
1523+
pub(crate) async fn resolve_vars_from_config_files(
1524+
config: &Arc<Config>,
1525+
config_files: &ConfigMap,
1526+
) -> Result<EnvResults> {
1527+
let entries = config_files
15261528
.iter()
15271529
.rev()
15281530
.map(|(source, cf)| {
@@ -1533,7 +1535,8 @@ async fn load_vars(config: &Arc<Config>) -> Result<EnvResults> {
15331535
.into_iter()
15341536
.flatten()
15351537
.collect();
1536-
let vars_results = EnvResults::resolve(
1538+
1539+
EnvResults::resolve(
15371540
config,
15381541
config.tera_ctx.clone(),
15391542
&env::PRISTINE_ENV,
@@ -1544,7 +1547,12 @@ async fn load_vars(config: &Arc<Config>) -> Result<EnvResults> {
15441547
warn_on_missing_required: false,
15451548
},
15461549
)
1547-
.await?;
1550+
.await
1551+
}
1552+
1553+
async fn load_vars(config: &Arc<Config>) -> Result<EnvResults> {
1554+
time!("load_vars start");
1555+
let vars_results = resolve_vars_from_config_files(config, &config.config_files).await?;
15481556
time!("load_vars done");
15491557
if log::log_enabled!(log::Level::Trace) {
15501558
trace!("{vars_results:#?}");
@@ -2435,4 +2443,26 @@ mod tests {
24352443

24362444
Ok(())
24372445
}
2446+
2447+
#[tokio::test]
2448+
async fn test_load_task_file_supports_per_task_vars() -> Result<()> {
2449+
let config = Config::reset().await?;
2450+
let temp_dir = TempDir::new()?;
2451+
let tasks_toml = temp_dir.path().join("tasks.toml");
2452+
fs::write(
2453+
&tasks_toml,
2454+
r#"
2455+
[build]
2456+
description = "{{vars.target}}"
2457+
run = "echo build"
2458+
vars = { target = "linux" }
2459+
"#,
2460+
)?;
2461+
2462+
let tasks = load_task_file(&config, &tasks_toml, temp_dir.path()).await?;
2463+
assert_eq!(tasks.len(), 1);
2464+
assert_eq!(tasks[0].name, "build");
2465+
assert_eq!(tasks[0].description, "linux");
2466+
Ok(())
2467+
}
24382468
}

src/task/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ use xx::regex;
3232

3333
static FUZZY_MATCHER: Lazy<SkimMatcherV2> =
3434
Lazy::new(|| SkimMatcherV2::default().use_cache(true).smart_case());
35+
static TASK_VARS_CACHE: Lazy<std::sync::Mutex<IndexMap<PathBuf, IndexMap<String, String>>>> =
36+
Lazy::new(|| std::sync::Mutex::new(IndexMap::new()));
37+
38+
pub(crate) fn reset() {
39+
TASK_VARS_CACHE.lock().unwrap().clear();
40+
}
3541

3642
/// Type alias for tracking failed tasks with their exit codes
3743
pub type FailedTasks = Arc<std::sync::Mutex<Vec<(Task, Option<i32>)>>>;
@@ -227,6 +233,8 @@ pub struct Task {
227233
pub wait_for: Vec<TaskDep>,
228234
#[serde(default)]
229235
pub env: EnvList,
236+
#[serde(default)]
237+
pub vars: EnvList,
230238
/// Env vars inherited from parent tasks at runtime (not used for task identity/deduplication)
231239
#[serde(skip)]
232240
pub inherited_env: EnvList,
@@ -848,10 +856,81 @@ impl Task {
848856
pub async fn tera_ctx(&self, config: &Arc<Config>) -> Result<tera::Context> {
849857
let ts = config.get_toolset().await?;
850858
let mut tera_ctx = ts.tera_ctx(config).await?.clone();
859+
let mut vars = self.resolve_base_vars(config).await?;
860+
tera_ctx.insert("vars", &vars);
861+
vars.extend(self.resolve_task_vars(config, &tera_ctx).await?);
862+
tera_ctx.insert("vars", &vars);
851863
tera_ctx.insert("config_root", &self.config_root);
852864
Ok(tera_ctx)
853865
}
854866

867+
async fn resolve_base_vars(&self, config: &Arc<Config>) -> Result<IndexMap<String, String>> {
868+
let Some(task_cf) = self.cf(config) else {
869+
return Ok(config.vars.clone());
870+
};
871+
872+
if task_cf.project_root() == config.project_root {
873+
return Ok(config.vars.clone());
874+
}
875+
876+
let config_path = task_cf.get_path().to_path_buf();
877+
if let Some(vars) = TASK_VARS_CACHE.lock().unwrap().get(&config_path) {
878+
return Ok(vars.clone());
879+
}
880+
881+
let task_dir = task_cf.get_path().parent().unwrap_or(task_cf.get_path());
882+
let config_paths = crate::config::load_config_hierarchy_from_dir(task_dir)?;
883+
let task_config_files = crate::config::load_config_files_from_paths(&config_paths).await?;
884+
let vars_results =
885+
crate::config::resolve_vars_from_config_files(config, &task_config_files).await?;
886+
let vars: IndexMap<String, String> = vars_results
887+
.vars
888+
.iter()
889+
.map(|(k, (v, _))| (k.clone(), v.clone()))
890+
.collect();
891+
TASK_VARS_CACHE
892+
.lock()
893+
.unwrap()
894+
.insert(config_path, vars.clone());
895+
Ok(vars)
896+
}
897+
898+
async fn resolve_task_vars(
899+
&self,
900+
config: &Arc<Config>,
901+
tera_ctx: &tera::Context,
902+
) -> Result<IndexMap<String, String>> {
903+
if self.vars.0.is_empty() {
904+
return Ok(IndexMap::new());
905+
}
906+
907+
let ts = config.get_toolset().await?;
908+
let env_map = ts.full_env(config).await?;
909+
let results = EnvResults::resolve(
910+
config,
911+
tera_ctx.clone(),
912+
&env_map,
913+
self.vars
914+
.0
915+
.iter()
916+
.cloned()
917+
.map(|directive| (directive, self.config_source.clone()))
918+
.collect(),
919+
EnvResolveOptions {
920+
vars: true,
921+
tools: ToolsFilter::NonToolsOnly,
922+
warn_on_missing_required: false,
923+
},
924+
)
925+
.await?;
926+
927+
Ok(results
928+
.vars
929+
.iter()
930+
.map(|(k, (v, _))| (k.clone(), v.clone()))
931+
.collect())
932+
}
933+
855934
pub fn cf<'a>(&'a self, config: &'a Config) -> Option<&'a Arc<dyn ConfigFile>> {
856935
// For monorepo tasks, use the stored config file reference
857936
if let Some(ref cf) = self.cf {
@@ -1161,6 +1240,7 @@ impl Default for Task {
11611240
depends_post: vec![],
11621241
wait_for: vec![],
11631242
env: Default::default(),
1243+
vars: Default::default(),
11641244
inherited_env: Default::default(),
11651245
dir: None,
11661246
hide: false,

0 commit comments

Comments
 (0)