diff --git a/e2e/tasks/test_task_monorepo_includes b/e2e/tasks/test_task_monorepo_includes new file mode 100644 index 0000000000..2e681b9ba6 --- /dev/null +++ b/e2e/tasks/test_task_monorepo_includes @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +# Test monorepo task edge cases: multiple dot extensions +export MISE_EXPERIMENTAL=1 + +# Create monorepo root config +cat <mise.toml +experimental_monorepo_root = true + +[tasks.root-task] +run = 'echo "root task"' +EOF + +# Create project structure with task_config.includes +mkdir -p projects/frontend/tasks +cat <projects/frontend/mise.toml +[task_config] +includes = ["tasks.toml", "tasks"] +EOF + +# Create included TOML file with a task +cat <projects/frontend/tasks.toml +[toml-task] +run = 'echo "task from toml"' +EOF + +# Create included script task +cat <<'EOF' >projects/frontend/tasks/script-task.sh +#!/usr/bin/env bash +echo "task from script" +EOF +chmod +x projects/frontend/tasks/script-task.sh + +# Create included script task with multiple extensions +cat <<'EOF' >projects/frontend/tasks/another.long-name.sh +#!/usr/bin/env bash +echo "task with long name" +EOF +chmod +x projects/frontend/tasks/another.long-name.sh + +# --- Assertions --- + +# Verify all tasks are discovered +assert_contains "mise tasks ls --debug --all" "//:root-task" +assert_contains "mise tasks ls --all" "//projects/frontend:toml-task" +assert_contains "mise tasks ls --all" "//projects/frontend:script-task" +assert_contains "mise tasks ls --all" "//projects/frontend:another.long-name" + +# Verify running tasks from root +assert_contains "mise run '//projects/frontend:toml-task'" "task from toml" +assert_contains "mise run '//projects/frontend:script-task'" "task from script" +assert_contains "mise run '//projects/frontend:another.long-name'" "task with long name" +assert_contains "mise run '//projects/frontend:another.long-name.sh'" "task with long name" + +# Verify running from a subdirectory +cd projects/frontend +assert_contains "mise run :toml-task" "task from toml" +assert_contains "mise run :script-task" "task from script" +assert_contains "mise run :another.long-name" "task with long name" + +# Verify wildcard execution from subdirectory +output=$(mise run ':*') +assert_contains "echo '$output'" "task from toml" +assert_contains "echo '$output'" "task from script" +assert_contains "echo '$output'" "task with long name" + +cd ../.. + +# Verify wildcard execution from root +output=$(mise run '//projects/frontend:*') +assert_contains "echo '$output'" "task from toml" +assert_contains "echo '$output'" "task from script" +assert_contains "echo '$output'" "task with long name" diff --git a/src/config/mod.rs b/src/config/mod.rs index 7d04326d45..65665dcb26 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1611,36 +1611,39 @@ async fn load_tasks_includes( root: &Path, config_root: &Path, ) -> Result> { - if !root.is_dir() { - return Ok(vec![]); - } - let files = WalkDir::new(root) - .follow_links(true) - .into_iter() - // skip hidden directories (if the root is hidden that's ok) - .filter_entry(|e| e.path() == root || !e.file_name().to_string_lossy().starts_with('.')) - .filter_ok(|e| e.file_type().is_file()) - .map_ok(|e| e.path().to_path_buf()) - .try_collect::<_, Vec, _>()? - .into_iter() - .filter(|p| file::is_executable(p)) - .filter(|p| { - !Settings::get() - .task_disable_paths - .iter() - .any(|d| p.starts_with(d)) - }) - .collect::>(); - let mut tasks = vec![]; - let root = Arc::new(root.to_path_buf()); - let config_root = Arc::new(config_root.to_path_buf()); - for path in files { - let root = root.clone(); - let config_root = config_root.clone(); - let config = config.clone(); - tasks.push(Task::from_path(&config, &path, &root, &config_root).await?); + if root.is_file() && root.extension().map(|e| e == "toml").unwrap_or(false) { + load_task_file(config, root, config_root).await + } else if root.is_dir() { + let files = WalkDir::new(root) + .follow_links(true) + .into_iter() + // skip hidden directories (if the root is hidden that's ok) + .filter_entry(|e| e.path() == root || !e.file_name().to_string_lossy().starts_with('.')) + .filter_ok(|e| e.file_type().is_file()) + .map_ok(|e| e.path().to_path_buf()) + .try_collect::<_, Vec, _>()? + .into_iter() + .filter(|p| file::is_executable(p)) + .filter(|p| { + !Settings::get() + .task_disable_paths + .iter() + .any(|d| p.starts_with(d)) + }) + .collect::>(); + let mut tasks = vec![]; + let root = Arc::new(root.to_path_buf()); + let config_root = Arc::new(config_root.to_path_buf()); + for path in files { + let root = root.clone(); + let config_root = config_root.clone(); + let config = config.clone(); + tasks.push(Task::from_path(&config, &path, &root, &config_root).await?); + } + Ok(tasks) + } else { + Ok(vec![]) } - Ok(tasks) } async fn load_file_tasks( @@ -1692,22 +1695,9 @@ pub async fn load_tasks_in_dir( let dir = dir.to_path_buf(); config_tasks.extend(load_config_tasks(config, cf.clone(), &dir).await?); } - let includes = task_includes_for_dir(dir, config_files); - let extra_tasks = includes - .iter() - .filter(|p| p.is_file() && p.extension().unwrap_or_default().to_string_lossy() == "toml"); - for p in extra_tasks { - let p = p.clone(); - let dir = dir.to_path_buf(); - let config = config.clone(); - config_tasks.extend(load_task_file(&config, &p, &dir).await?); - } let mut file_tasks = vec![]; - for p in includes { - let dir = dir.to_path_buf(); - let p = p.clone(); - let config = config.clone(); - file_tasks.extend(load_tasks_includes(&config, &p, &dir).await?); + for p in task_includes_for_dir(dir, config_files) { + file_tasks.extend(load_tasks_includes(config, &p, dir).await?); } let mut tasks = file_tasks .into_iter()