diff --git a/crates/action-graph/src/action_graph_builder.rs b/crates/action-graph/src/action_graph_builder.rs index 7922314c58..2d0b2cea05 100644 --- a/crates/action-graph/src/action_graph_builder.rs +++ b/crates/action-graph/src/action_graph_builder.rs @@ -541,6 +541,10 @@ impl<'app> ActionGraphBuilder<'app> { let mut inserted_nodes = vec![]; let mut initial_targets = vec![]; + if let Some(affected) = &mut self.affected { + affected.set_ci_check(reqs.ci_check); + } + // Track the qualified as an initial target for locator in reqs.target_locators.clone() { initial_targets.push(match locator { diff --git a/crates/action-graph/tests/action_graph_test.rs b/crates/action-graph/tests/action_graph_test.rs index 46a38a87c6..144ead54f7 100644 --- a/crates/action-graph/tests/action_graph_test.rs +++ b/crates/action-graph/tests/action_graph_test.rs @@ -7,7 +7,7 @@ use moon_action_context::TargetState; use moon_action_graph::*; use moon_common::path::WorkspaceRelativePathBuf; use moon_common::Id; -use moon_config::{PlatformType, TaskArgs, TaskDependencyConfig}; +use moon_config::{PlatformType, TaskArgs, TaskDependencyConfig, TaskOptionRunInCI}; use moon_platform::*; use moon_task::{Target, TargetLocator, Task}; use moon_test_utils2::generate_workspace_graph; @@ -1027,7 +1027,7 @@ mod action_graph { let project = container.workspace_graph.get_project("bar").unwrap(); let mut task = create_task("build", "bar"); - task.options.run_in_ci = true; + task.options.run_in_ci = TaskOptionRunInCI::Enabled(true); builder .run_task( @@ -1121,7 +1121,7 @@ mod action_graph { let project = container.workspace_graph.get_project("bar").unwrap(); let mut task = create_task("build", "bar"); - task.options.run_in_ci = false; + task.options.run_in_ci = TaskOptionRunInCI::Enabled(false); builder .run_task( @@ -1158,7 +1158,7 @@ mod action_graph { let project = container.workspace_graph.get_project("bar").unwrap(); let mut task = create_task("build", "bar"); - task.options.run_in_ci = false; + task.options.run_in_ci = TaskOptionRunInCI::Enabled(false); builder .run_task( @@ -1188,7 +1188,7 @@ mod action_graph { let project = container.workspace_graph.get_project("bar").unwrap(); let mut task = create_task("build", "bar"); - task.options.run_in_ci = false; + task.options.run_in_ci = TaskOptionRunInCI::Enabled(false); builder .run_task( diff --git a/crates/affected/src/affected_tracker.rs b/crates/affected/src/affected_tracker.rs index e139a064b6..cdf2c696a6 100644 --- a/crates/affected/src/affected_tracker.rs +++ b/crates/affected/src/affected_tracker.rs @@ -10,6 +10,8 @@ use std::fmt; use tracing::{debug, trace}; pub struct AffectedTracker<'app> { + ci: bool, + workspace_graph: &'app WorkspaceGraph, touched_files: &'app FxHashSet, @@ -38,6 +40,7 @@ impl<'app> AffectedTracker<'app> { tasks: FxHashMap::default(), task_downstream: DownstreamScope::None, task_upstream: UpstreamScope::Deep, + ci: false, } } @@ -82,6 +85,11 @@ impl<'app> AffectedTracker<'app> { affected } + pub fn set_ci_check(&mut self, ci: bool) -> &mut Self { + self.ci = ci; + self + } + pub fn with_project_scopes( &mut self, upstream_scope: UpstreamScope, @@ -322,13 +330,15 @@ impl<'app> AffectedTracker<'app> { return Ok(Some(AffectedBy::AlreadyMarked)); } - match &task.options.run_in_ci { - TaskOptionRunInCI::Always => { - return Ok(Some(AffectedBy::AlwaysAffected)); - } - TaskOptionRunInCI::Enabled(false) => return Ok(None), - _ => {} - }; + if self.ci { + match &task.options.run_in_ci { + TaskOptionRunInCI::Always => { + return Ok(Some(AffectedBy::AlwaysAffected)); + } + TaskOptionRunInCI::Enabled(false) => return Ok(None), + _ => {} + }; + } // inputs: [] if task.state.empty_inputs { diff --git a/crates/affected/tests/__fixtures__/tasks/ci/moon.yml b/crates/affected/tests/__fixtures__/tasks/ci/moon.yml new file mode 100644 index 0000000000..8b72603701 --- /dev/null +++ b/crates/affected/tests/__fixtures__/tasks/ci/moon.yml @@ -0,0 +1,13 @@ +tasks: + enabled: + options: + runInCI: true + disabled: + options: + runInCI: false + affected: + options: + runInCI: affected + always: + options: + runInCI: always diff --git a/crates/affected/tests/affected_tracker_test.rs b/crates/affected/tests/affected_tracker_test.rs index 2b078e4d7f..e38585b8b1 100644 --- a/crates/affected/tests/affected_tracker_test.rs +++ b/crates/affected/tests/affected_tracker_test.rs @@ -956,4 +956,110 @@ mod affected_tasks { ); } } + + mod ci { + use super::*; + + #[tokio::test] + async fn when_ci_tracks_for_true() { + let workspace_graph = generate_workspace_graph("tasks").await; + let touched_files = FxHashSet::from_iter(["ci/file.txt".into()]); + + let mut tracker = AffectedTracker::new(&workspace_graph, &touched_files); + tracker.set_ci_check(true); + tracker + .track_tasks_by_target(&[Target::parse("ci:enabled").unwrap()]) + .unwrap(); + let affected = tracker.build(); + + assert_eq!( + affected.tasks, + FxHashMap::from_iter([( + Target::parse("ci:enabled").unwrap(), + create_state_from_file("ci/file.txt") + )]) + ); + } + + #[tokio::test] + async fn when_not_ci_tracks_for_true() { + let workspace_graph = generate_workspace_graph("tasks").await; + let touched_files = FxHashSet::from_iter(["ci/file.txt".into()]); + + let mut tracker = AffectedTracker::new(&workspace_graph, &touched_files); + tracker.set_ci_check(false); + tracker + .track_tasks_by_target(&[Target::parse("ci:enabled").unwrap()]) + .unwrap(); + let affected = tracker.build(); + + assert_eq!( + affected.tasks, + FxHashMap::from_iter([( + Target::parse("ci:enabled").unwrap(), + create_state_from_file("ci/file.txt") + )]) + ); + } + + #[tokio::test] + async fn when_ci_doesnt_track_for_false() { + let workspace_graph = generate_workspace_graph("tasks").await; + let touched_files = FxHashSet::from_iter(["ci/file.txt".into()]); + + let mut tracker = AffectedTracker::new(&workspace_graph, &touched_files); + tracker.set_ci_check(true); + tracker + .track_tasks_by_target(&[Target::parse("ci:disabled").unwrap()]) + .unwrap(); + let affected = tracker.build(); + + assert!(affected.tasks.is_empty()); + } + + #[tokio::test] + async fn when_not_ci_tracks_for_false() { + let workspace_graph = generate_workspace_graph("tasks").await; + let touched_files = FxHashSet::from_iter(["ci/file.txt".into()]); + + let mut tracker = AffectedTracker::new(&workspace_graph, &touched_files); + tracker.set_ci_check(false); + tracker + .track_tasks_by_target(&[Target::parse("ci:disabled").unwrap()]) + .unwrap(); + let affected = tracker.build(); + + assert_eq!( + affected.tasks, + FxHashMap::from_iter([( + Target::parse("ci:disabled").unwrap(), + create_state_from_file("ci/file.txt") + )]) + ); + } + + #[tokio::test] + async fn when_ci_always_tracks_if_not_touched() { + let workspace_graph = generate_workspace_graph("tasks").await; + let touched_files = FxHashSet::default(); + + let mut tracker = AffectedTracker::new(&workspace_graph, &touched_files); + tracker.set_ci_check(true); + tracker + .track_tasks_by_target(&[Target::parse("ci:always").unwrap()]) + .unwrap(); + let affected = tracker.build(); + + assert_eq!( + affected.tasks, + FxHashMap::from_iter([( + Target::parse("ci:always").unwrap(), + AffectedTaskState { + other: true, + ..Default::default() + } + )]) + ); + } + } } diff --git a/crates/config/src/project/task_options_config.rs b/crates/config/src/project/task_options_config.rs index 05b263cd7a..5fdd20c45c 100644 --- a/crates/config/src/project/task_options_config.rs +++ b/crates/config/src/project/task_options_config.rs @@ -24,13 +24,14 @@ fn validate_interactive( derive_enum!( /// The pattern in which affected files will be passed to the affected task. - #[serde(untagged, expecting = "expected `args`, `env`, or a boolean")] + #[serde(expecting = "expected `args`, `env`, or a boolean")] pub enum TaskOptionAffectedFiles { /// Passed as command line arguments. Args, /// Passed as environment variables. Env, /// Passed as command line arguments and environment variables. + #[serde(untagged)] Enabled(bool), } ); @@ -87,13 +88,14 @@ impl Schematic for TaskOptionEnvFile { derive_enum!( /// The pattern in which to run the task automatically in CI. - #[serde(untagged, expecting = "expected `always`, `affected`, or a boolean")] + #[serde(expecting = "expected `always`, `affected`, or a boolean")] pub enum TaskOptionRunInCI { /// Always run, regardless of affected. Always, /// Only run if affected by touched files. Affected, /// Either affected, or don't run at all. + #[serde(untagged)] Enabled(bool), } ); diff --git a/crates/config/tests/inherited_tasks_config_test.rs b/crates/config/tests/inherited_tasks_config_test.rs index bda4bad1fb..653d5d62db 100644 --- a/crates/config/tests/inherited_tasks_config_test.rs +++ b/crates/config/tests/inherited_tasks_config_test.rs @@ -2,11 +2,7 @@ mod utils; use httpmock::prelude::*; use moon_common::Id; -use moon_config::{ - ConfigLoader, InheritedTasksConfig, InheritedTasksManager, InputPath, LanguageType, - PlatformType, ProjectType, StackType, TaskArgs, TaskConfig, TaskDependency, - TaskDependencyConfig, TaskMergeStrategy, TaskOptionsConfig, -}; +use moon_config::*; use moon_target::Target; use rustc_hash::FxHashMap; use schematic::Config; @@ -97,7 +93,7 @@ tasks: TaskConfig { command: TaskArgs::String("e".to_owned()), options: TaskOptionsConfig { - run_in_ci: Some(false), + run_in_ci: Some(TaskOptionRunInCI::Enabled(false)), ..TaskOptionsConfig::default() }, ..TaskConfig::default() @@ -1111,7 +1107,6 @@ mod task_manager { mod pkl { use super::*; use moon_common::Id; - use moon_config::*; use starbase_sandbox::locate_fixture; #[test] @@ -1183,7 +1178,7 @@ mod task_manager { persistent: Some(true), retry_count: Some(3), run_deps_in_parallel: Some(false), - run_in_ci: Some(true), + run_in_ci: Some(TaskOptionRunInCI::Enabled(true)), run_from_workspace_root: Some(false), shell: Some(false), timeout: Some(60), diff --git a/crates/task-builder/tests/task_deps_builder_test.rs b/crates/task-builder/tests/task_deps_builder_test.rs index dd5fa7a974..f3ca9541df 100644 --- a/crates/task-builder/tests/task_deps_builder_test.rs +++ b/crates/task-builder/tests/task_deps_builder_test.rs @@ -92,7 +92,7 @@ mod task_deps_builder { #[should_panic(expected = "Task project:task cannot depend on task project:no-ci")] fn errors_if_dep_not_run_in_ci() { let mut task = create_task(); - task.options.run_in_ci = true; + task.options.run_in_ci = TaskOptionRunInCI::Enabled(true); task.deps .push(TaskDependencyConfig::new(Target::parse("no-ci").unwrap())); @@ -101,7 +101,7 @@ mod task_deps_builder { FxHashMap::from_iter([( Target::parse("project:no-ci").unwrap(), TaskOptions { - run_in_ci: false, + run_in_ci: TaskOptionRunInCI::Enabled(false), ..Default::default() }, )]), @@ -111,7 +111,7 @@ mod task_deps_builder { #[test] fn doesnt_errors_if_dep_run_in_ci() { let mut task = create_task(); - task.options.run_in_ci = false; + task.options.run_in_ci = TaskOptionRunInCI::Enabled(false); task.deps .push(TaskDependencyConfig::new(Target::parse("ci").unwrap())); @@ -120,7 +120,7 @@ mod task_deps_builder { FxHashMap::from_iter([( Target::parse("project:ci").unwrap(), TaskOptions { - run_in_ci: true, + run_in_ci: TaskOptionRunInCI::Enabled(true), ..Default::default() }, )]), diff --git a/crates/task-builder/tests/tasks_builder_test.rs b/crates/task-builder/tests/tasks_builder_test.rs index 952ab6b02f..574fa5a4d7 100644 --- a/crates/task-builder/tests/tasks_builder_test.rs +++ b/crates/task-builder/tests/tasks_builder_test.rs @@ -616,21 +616,21 @@ mod tasks_builder { // assert!(!task.options.cache); // assert!(!task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); let task = tasks.get("interactive-local").unwrap(); // assert!(!task.options.cache); // assert!(!task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); let task = tasks.get("interactive-override").unwrap(); // assert!(!task.options.cache); // assert!(!task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); } @@ -706,7 +706,7 @@ mod tasks_builder { assert!(!task.options.cache); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); assert!(task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); } #[tokio::test] @@ -742,7 +742,7 @@ mod tasks_builder { let ci = tasks.get("override-ci").unwrap(); assert!(ci.state.local_only); - assert!(ci.options.run_in_ci); + assert!(ci.options.run_in_ci.is_enabled()); } #[tokio::test] @@ -774,7 +774,7 @@ mod tasks_builder { assert!(!task.options.cache); assert!(!task.options.interactive); assert!(task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); // Custom overrides @@ -784,7 +784,7 @@ mod tasks_builder { assert!(task.options.cache); assert!(!task.options.interactive); assert!(task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); } @@ -799,7 +799,7 @@ mod tasks_builder { assert!(!task.options.cache); assert!(task.options.interactive); assert!(task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); // Custom overrides @@ -809,7 +809,7 @@ mod tasks_builder { assert!(!task.options.cache); assert!(!task.options.interactive); assert!(task.options.persistent); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert_eq!(task.options.output_style, Some(TaskOutputStyle::Stream)); } } @@ -1925,7 +1925,7 @@ mod tasks_builder { let task = tasks.get("extend-options").unwrap(); assert!(!task.options.cache); - assert!(task.options.run_in_ci); + assert!(task.options.run_in_ci.is_enabled()); assert!(task.options.persistent); assert_eq!(task.options.retry_count, 3); } @@ -1937,7 +1937,7 @@ mod tasks_builder { let task = tasks.get("extend-local").unwrap(); assert!(task.options.cache); - assert!(task.options.run_in_ci); + assert!(task.options.run_in_ci.is_enabled()); assert!(!task.options.persistent); } @@ -1962,7 +1962,7 @@ mod tasks_builder { ); assert!(task.options.cache); - assert!(!task.options.run_in_ci); + assert!(!task.options.run_in_ci.is_enabled()); assert!(task.options.persistent); assert_eq!(task.options.retry_count, 3); } diff --git a/website/docs/config/project.mdx b/website/docs/config/project.mdx index 35c50007d3..5e2bab1923 100644 --- a/website/docs/config/project.mdx +++ b/website/docs/config/project.mdx @@ -1300,9 +1300,14 @@ tasks: Whether to run the task automatically in a CI (continuous integration) environment when affected by -touched files, typically through the [`moon ci`](../commands/ci) command. Defaults to `true` unless -the [`local`](#local) setting is disabled, but is _always_ true when a task defines -[`outputs`](#outputs). +touched files, typically through the [`moon ci`](../commands/ci) command. Supports the following +values: + +- `always` - Always run in CI, regardless if affected or not. +- `true`, `affected` - Only run if affected by touched files. +- `false` - Never run in CI. + +Defaults to `true` unless the [`local`](#local) setting is enabled. ```yaml title="moon.yml" {5} tasks: