diff --git a/e2e/tasks/test_task_includes_glob b/e2e/tasks/test_task_includes_glob new file mode 100644 index 0000000000..5eb19f3efb --- /dev/null +++ b/e2e/tasks/test_task_includes_glob @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# Test glob pattern support in task_config.includes +# See: https://github.com/jdx/mise/discussions/7860 + +# Create a tasks directory with multiple task files +# Note: Standalone task TOML files use a different format than mise.toml +mkdir -p tasks + +cat <tasks/build.toml +[build] +run = 'echo "building"' +EOF + +cat <tasks/test.toml +[test] +run = 'echo "testing"' +EOF + +cat <tasks/deploy.toml +[deploy] +run = 'echo "deploying"' +EOF + +# Create mise.toml with glob pattern in includes +cat <mise.toml +[task_config] +includes = ["tasks/*.toml"] +EOF + +# Test that all tasks are discovered via glob pattern +assert_contains "mise tasks" "build" +assert_contains "mise tasks" "test" +assert_contains "mise tasks" "deploy" + +# Test that the tasks actually run +assert_contains "mise run build" "building" +assert_contains "mise run test" "testing" +assert_contains "mise run deploy" "deploying" + +# Test mixing glob with literal paths +cat <extra-tasks.toml +[extra] +run = 'echo "extra task"' +EOF + +cat <mise.toml +[task_config] +includes = ["tasks/*.toml", "extra-tasks.toml"] +EOF + +assert_contains "mise tasks" "build" +assert_contains "mise tasks" "extra" +assert_contains "mise run extra" "extra task" diff --git a/src/config/mod.rs b/src/config/mod.rs index ffa014ca09..6827d69fc4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2033,6 +2033,50 @@ async fn resolve_git_url_to_path(git_url: &str) -> Result { } } +/// Check if a pattern contains glob metacharacters +fn is_glob_pattern(pattern: &str) -> bool { + // Check for unescaped glob metacharacters: *, ?, [, ], {, } + // Note: This is a simple check that may have false positives with escaped chars, + // but glob() will handle those correctly + pattern.contains('*') + || pattern.contains('?') + || pattern.contains('[') + || pattern.contains(']') + || pattern.contains('{') + || pattern.contains('}') +} + +/// Expand a task include pattern (which may be a glob) to a list of paths +fn expand_task_include(dir: &Path, pattern: &str) -> Vec { + if is_glob_pattern(pattern) { + match glob(dir, pattern) { + Ok(paths) => paths, + Err(err) => { + warn!( + "failed to expand glob pattern '{}' in '{}': {}", + pattern, + display_path(dir), + err + ); + vec![] + } + } + } else { + // Literal path + let path = PathBuf::from(pattern); + let resolved = if path.is_absolute() { + path + } else { + dir.join(path) + }; + if resolved.exists() { + vec![resolved] + } else { + vec![] + } + } +} + async fn load_file_tasks( config: &Arc, cf: Arc, @@ -2046,14 +2090,17 @@ async fn load_file_tasks( let mut tasks = vec![]; let config_root = Arc::new(config_root.to_path_buf()); + let cf_dir = cf.get_path().parent().unwrap(); for include in includes { - let path = if include.starts_with("git::") { - resolve_git_url_to_path(&include).await? + let paths = if include.starts_with("git::") { + vec![resolve_git_url_to_path(&include).await?] } else { - cf.get_path().parent().unwrap().join(&include) + expand_task_include(cf_dir, &include) }; - tasks.extend(load_tasks_includes(config, &path, &config_root).await?); + for path in paths { + tasks.extend(load_tasks_includes(config, &path, &config_root).await?); + } } Ok(tasks) } @@ -2065,23 +2112,12 @@ pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Vec>()