diff --git a/.moon/tasks/node.yml b/.moon/tasks/node.yml index 5f89768625d..40f12867a9f 100644 --- a/.moon/tasks/node.yml +++ b/.moon/tasks/node.yml @@ -52,7 +52,6 @@ tasks: - '--color' - '--ext' - '.js,.ts,.tsx' - - '--fix' - '--ignore-path' - '@in(5)' - '--exit-on-fatal-error' @@ -71,6 +70,11 @@ tasks: options: affectedFiles: true + lint-fix: + extends: 'lint' + args: '--fix' + local: true + test: command: 'jest' args: diff --git a/nextgen/config/src/inherited_tasks_config.rs b/nextgen/config/src/inherited_tasks_config.rs index 07cfd975122..376b32425fa 100644 --- a/nextgen/config/src/inherited_tasks_config.rs +++ b/nextgen/config/src/inherited_tasks_config.rs @@ -1,6 +1,6 @@ use crate::language_platform::{LanguageType, PlatformType}; use crate::project::{validate_deps, TaskConfig}; -use crate::project_config::ProjectType; +use crate::project_config::{validate_tasks, ProjectType}; use crate::shapes::InputPath; use moon_common::cacheable; use moon_common::{consts, Id}; @@ -49,7 +49,7 @@ cacheable!( #[setting(merge = merge::append_vec)] pub implicit_inputs: Vec, - #[setting(nested, merge = merge::merge_btreemap)] + #[setting(nested, merge = merge::merge_btreemap, validate = validate_tasks)] pub tasks: BTreeMap, } ); diff --git a/nextgen/config/src/project_config.rs b/nextgen/config/src/project_config.rs index 9565a884ee3..bb0d66d99da 100644 --- a/nextgen/config/src/project_config.rs +++ b/nextgen/config/src/project_config.rs @@ -27,7 +27,7 @@ pub fn validate_tasks( if let Some(extends_from) = &config.extends { if !tasks.contains_key(extends_from) { return Err(ValidateError::new(format!( - "task {} is extending a non-existent task {}", + "task {} is extending an unknown task {}", id, extends_from ))); } diff --git a/nextgen/config/tests/project_config_test.rs b/nextgen/config/tests/project_config_test.rs index aca9ab24310..53b28195286 100644 --- a/nextgen/config/tests/project_config_test.rs +++ b/nextgen/config/tests/project_config_test.rs @@ -579,7 +579,7 @@ tasks: } #[test] - #[should_panic(expected = "task extender is extending a non-existent task unknown")] + #[should_panic(expected = "task extender is extending an unknown task unknown")] fn errors_if_extending_unknown_task() { test_load_config( CONFIG_PROJECT_FILENAME, diff --git a/nextgen/config/tests/task_config_test.rs b/nextgen/config/tests/task_config_test.rs index 3bbc46985b4..514c3329d08 100644 --- a/nextgen/config/tests/task_config_test.rs +++ b/nextgen/config/tests/task_config_test.rs @@ -13,7 +13,7 @@ mod task_config { #[test] #[should_panic( - expected = "unknown field `unknown`, expected one of `command`, `args`, `deps`, `env`, `inputs`, `local`, `outputs`, `options`, `platform`, `type`" + expected = "unknown field `unknown`, expected one of `extends`, `command`, `args`, `deps`, `env`, `inputs`, `local`, `outputs`, `options`, `platform`, `type`" )] fn error_unknown_field() { test_parse_config("unknown: 123", |code| TaskConfig::parse(code)); diff --git a/nextgen/task-builder/src/tasks_builder.rs b/nextgen/task-builder/src/tasks_builder.rs index 5bbeb08ddd6..e101d2497c4 100644 --- a/nextgen/task-builder/src/tasks_builder.rs +++ b/nextgen/task-builder/src/tasks_builder.rs @@ -189,15 +189,7 @@ impl<'proj> TasksBuilder<'proj> { trace!(target = target.as_str(), "Building task"); let mut task = Task::default(); - let mut configs = vec![]; - - if let Some(config) = self.global_tasks.get(id) { - configs.push(*config); - } - - if let Some(config) = self.local_tasks.get(id) { - configs.push(*config); - } + let configs = self.get_config_inherit_chain(id); // Determine command and args before building options and the task, // as we need to figure out if we're running in local mode or not. @@ -219,7 +211,9 @@ impl<'proj> TasksBuilder<'proj> { } } - trace!(target = target.as_str(), "Marking task as local"); + if is_local { + trace!(target = target.as_str(), "Marking task as local"); + } task.options = self.build_task_options(id, is_local)?; task.flags.local = is_local; @@ -370,15 +364,11 @@ impl<'proj> TasksBuilder<'proj> { ..TaskOptions::default() }; - let mut configs = vec![]; - - if let Some(config) = self.global_tasks.get(id) { - configs.push(&config.options); - } - - if let Some(config) = self.local_tasks.get(id) { - configs.push(&config.options); - } + let configs = self + .get_config_inherit_chain(id) + .iter() + .map(|cfg| &cfg.options) + .collect::>(); for config in configs { if let Some(affected_files) = &config.affected_files { @@ -536,6 +526,32 @@ impl<'proj> TasksBuilder<'proj> { Ok((command, args)) } + fn get_config_inherit_chain(&self, id: &Id) -> Vec<&TaskConfig> { + let mut configs = vec![]; + + if let Some(config) = self.global_tasks.get(id) { + if let Some(extends_from) = &config.extends { + if let Some(extends_config) = self.global_tasks.get(extends_from) { + configs.push(*extends_config); + } + } + + configs.push(*config); + } + + if let Some(config) = self.local_tasks.get(id) { + if let Some(extends_from) = &config.extends { + if let Some(extends_config) = self.local_tasks.get(extends_from) { + configs.push(*extends_config); + } + } + + configs.push(*config); + } + + configs + } + fn merge_map( &self, base: FxHashMap, diff --git a/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml b/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml new file mode 100644 index 00000000000..c7501e6eaa0 --- /dev/null +++ b/nextgen/task-builder/tests/__fixtures__/builder/extends/moon.yml @@ -0,0 +1,48 @@ +tags: [extends] + +tasks: + base: + command: lint ./src + local: true + inputs: + - '**/*' + + extend-args: + extends: base + args: --fix + options: + mergeArgs: prepend + + extend-inputs: + extends: base + inputs: + - 'src/**/*' + options: + mergeInputs: replace + + extend-options: + extends: base + options: + retryCount: 3 + runInCI: true + + extend-local: + extends: base + local: false + + # Tests global inheritance + + local-base: + inputs: + - 'local-base' + options: + cache: true + retryCount: 3 + + extender: + extends: 'local-base' + args: '-qux' + inputs: + - 'local-extender' + options: + mergeArgs: 'prepend' diff --git a/nextgen/task-builder/tests/__fixtures__/builder/global/tasks/tag-extends.yml b/nextgen/task-builder/tests/__fixtures__/builder/global/tasks/tag-extends.yml new file mode 100644 index 00000000000..14802525d4e --- /dev/null +++ b/nextgen/task-builder/tests/__fixtures__/builder/global/tasks/tag-extends.yml @@ -0,0 +1,12 @@ +tasks: + global-base: + command: 'global-base' + local: true + inputs: + - 'global-base' + + extender: + extends: 'global-base' + args: '--foo --bar -z' + inputs: + - 'global-extender' diff --git a/nextgen/task-builder/tests/tasks_builder_test.rs b/nextgen/task-builder/tests/tasks_builder_test.rs index 62080dd92e6..5e579930244 100644 --- a/nextgen/task-builder/tests/tasks_builder_test.rs +++ b/nextgen/task-builder/tests/tasks_builder_test.rs @@ -1120,4 +1120,82 @@ mod tasks_builder { ); } } + + mod extending { + use super::*; + + #[tokio::test] + async fn handles_args() { + let sandbox = create_sandbox("builder"); + let tasks = build_tasks(sandbox.path(), "extends/moon.yml").await; + let task = tasks.get("extend-args").unwrap(); + + assert_eq!(task.command, "lint"); + assert_eq!(task.args, vec!["--fix", "./src"]); + } + + #[tokio::test] + async fn handles_inputs() { + let sandbox = create_sandbox("builder"); + let tasks = build_tasks(sandbox.path(), "extends/moon.yml").await; + let task = tasks.get("extend-inputs").unwrap(); + + assert_eq!( + task.inputs, + vec![ + InputPath::ProjectGlob("src/**/*".into()), + InputPath::WorkspaceGlob(".moon/*.yml".into()), + ] + ); + } + + #[tokio::test] + async fn handles_options() { + let sandbox = create_sandbox("builder"); + let tasks = build_tasks(sandbox.path(), "extends/moon.yml").await; + let task = tasks.get("extend-options").unwrap(); + + assert!(!task.options.cache); + assert!(task.options.run_in_ci); + assert!(task.options.persistent); + assert_eq!(task.options.retry_count, 3); + } + + #[tokio::test] + async fn handles_local() { + let sandbox = create_sandbox("builder"); + let tasks = build_tasks(sandbox.path(), "extends/moon.yml").await; + let task = tasks.get("extend-local").unwrap(); + + assert!(task.options.cache); + assert!(task.options.run_in_ci); + assert!(!task.options.persistent); + } + + #[tokio::test] + async fn inherits_and_merges_globals_extend_chain() { + let sandbox = create_sandbox("builder"); + let tasks = build_tasks(sandbox.path(), "extends/moon.yml").await; + let task = tasks.get("extender").unwrap(); + + assert_eq!(task.command, "global-base"); + assert_eq!(task.args, vec!["-qux", "--foo", "--bar", "-z"]); + assert_eq!( + task.inputs, + vec![ + InputPath::ProjectFile("global-base".into()), + InputPath::ProjectFile("global-extender".into()), + InputPath::ProjectFile("local-base".into()), + InputPath::ProjectFile("local-extender".into()), + InputPath::WorkspaceGlob(".moon/*.yml".into()), + InputPath::WorkspaceFile(".moon/tasks/tag-extends.yml".into()), + ] + ); + + assert!(task.options.cache); + assert!(!task.options.run_in_ci); + assert!(task.options.persistent); + assert_eq!(task.options.retry_count, 3); + } + } }