Skip to main content

moon_config/
task_options_config.rs

1use crate::shapes::{FilePath, OneOrMany};
2use crate::{config_enum, config_struct, config_unit_enum, config_untagged_enum, generate_switch};
3use schematic::schema::{StringType, UnionType};
4use schematic::{Config, ConfigEnum, Schema, SchemaBuilder, Schematic, ValidateError};
5use std::env::consts;
6
7fn validate_interactive<C>(
8    enabled: &bool,
9    options: &PartialTaskOptionsConfig,
10    _ctx: &C,
11    _finalize: bool,
12) -> Result<(), ValidateError> {
13    if *enabled && options.persistent.is_some_and(|v| v) {
14        return Err(ValidateError::new(
15            "an interactive task cannot be persistent",
16        ));
17    }
18
19    Ok(())
20}
21
22config_enum!(
23    /// The pattern in which affected files will be passed to the affected task.
24    #[serde(expecting = "expected `args`, `env`, or a boolean")]
25    pub enum TaskOptionAffectedFilesPattern {
26        /// Passed as command line arguments.
27        Args,
28        /// Passed as environment variables.
29        Env,
30        /// Passed as command line arguments and environment variables.
31        #[serde(untagged)]
32        Enabled(bool),
33    }
34);
35
36generate_switch!(TaskOptionAffectedFilesPattern, ["args", "env"]);
37
38impl Default for TaskOptionAffectedFilesPattern {
39    fn default() -> Self {
40        Self::Enabled(true)
41    }
42}
43
44config_struct!(
45    /// Expanded information about affected files handling.
46    #[derive(Config)]
47    pub struct TaskOptionAffectedFilesConfig {
48        /// The pattern in which affected files will be passed to the affected task.
49        pub pass: TaskOptionAffectedFilesPattern,
50
51        /// When no affected files are matching, pass the task's inputs
52        /// as arguments to the command, instead of `.`.
53        #[serde(default, skip_serializing_if = "Option::is_none")]
54        pub pass_inputs_when_no_match: Option<bool>,
55    }
56);
57
58config_untagged_enum!(
59    #[derive(Config)]
60    pub enum TaskOptionAffectedFilesEntry {
61        Pattern(TaskOptionAffectedFilesPattern),
62
63        #[setting(nested)]
64        Object(TaskOptionAffectedFilesConfig),
65    }
66);
67
68config_enum!(
69    /// The environment in which to apply task caching.
70    #[serde(expecting = "expected `local`, `remote`, or a boolean")]
71    pub enum TaskOptionCache {
72        /// Only cache locally.
73        Local,
74        /// Only cache remotely.
75        Remote,
76        /// Cache both locally and remotely.
77        #[serde(untagged)]
78        Enabled(bool),
79    }
80);
81
82generate_switch!(TaskOptionCache, ["local", "remote"]);
83
84impl TaskOptionCache {
85    pub fn is_local_enabled(&self) -> bool {
86        matches!(self, Self::Enabled(true) | Self::Local)
87    }
88
89    pub fn is_remote_enabled(&self) -> bool {
90        matches!(self, Self::Enabled(true) | Self::Remote)
91    }
92}
93
94config_untagged_enum!(
95    /// The pattern in which a task is dependent on a `.env` file.
96    pub enum TaskOptionEnvFile {
97        /// Uses an `.env` file in the project root.
98        Enabled(bool),
99        /// An explicit path to an `.env` file.
100        File(FilePath),
101        /// A list of explicit `.env` file paths.
102        Files(Vec<FilePath>),
103    }
104);
105
106impl Schematic for TaskOptionEnvFile {
107    fn schema_name() -> Option<String> {
108        Some("TaskOptionEnvFile".into())
109    }
110
111    fn build_schema(mut schema: SchemaBuilder) -> Schema {
112        schema.union(UnionType::new_any([
113            schema.infer::<bool>(),
114            schema.infer::<String>(),
115            schema.infer::<Vec<String>>(),
116        ]))
117    }
118}
119
120config_enum!(
121    /// The pattern in which to run the task automatically in CI.
122    #[serde(expecting = "expected `always`, `affected`, `only`, `skip`, or a boolean")]
123    pub enum TaskOptionRunInCI {
124        /// Always run, regardless of affected status.
125        Always,
126        /// Only run if affected by changed files.
127        Affected,
128        /// Only run in CI and not locally if affected by changed files.
129        Only,
130        /// Skip running in CI but run locally and allow task relationships to be valid.
131        Skip,
132        /// Either affected, or don't run at all.
133        #[serde(untagged)]
134        Enabled(bool),
135    }
136);
137
138generate_switch!(TaskOptionRunInCI, ["always", "affected", "only", "skip"]);
139
140config_unit_enum!(
141    /// The strategy in which to merge a specific task option.
142    #[derive(ConfigEnum)]
143    pub enum TaskMergeStrategy {
144        #[default]
145        Append,
146        Prepend,
147        Preserve,
148        Replace,
149    }
150);
151
152config_unit_enum!(
153    /// The style in which task output will be printed to the console.
154    #[derive(ConfigEnum)]
155    pub enum TaskOutputStyle {
156        #[default]
157        Buffer,
158        BufferOnlyFailure,
159        Hash,
160        None,
161        Stream,
162    }
163);
164
165config_enum!(
166    /// The operating system in which to only run this task on.
167    #[derive(ConfigEnum, Copy)]
168    pub enum TaskOperatingSystem {
169        Linux,
170        #[serde(alias = "mac")]
171        Macos,
172        #[serde(alias = "win")]
173        Windows,
174    }
175);
176
177impl TaskOperatingSystem {
178    pub fn is_current_system(&self) -> bool {
179        let os = consts::OS;
180
181        match self {
182            Self::Linux => os == "linux" || os.ends_with("bsd"),
183            Self::Macos => os == "macos",
184            Self::Windows => os == "windows",
185        }
186    }
187}
188
189config_unit_enum!(
190    /// The priority levels a task can be bucketed into when running
191    /// in the action pipeline.
192    #[derive(ConfigEnum)]
193    pub enum TaskPriority {
194        Critical = 0,
195        High = 1,
196        #[default]
197        Normal = 2,
198        Low = 3,
199    }
200);
201
202impl TaskPriority {
203    pub fn get_level(&self) -> u8 {
204        *self as u8
205    }
206}
207
208config_unit_enum!(
209    /// A list of available shells on Unix.
210    #[derive(ConfigEnum)]
211    pub enum TaskUnixShell {
212        #[default]
213        Bash,
214        Elvish,
215        Fish,
216        Ion,
217        Murex,
218        #[serde(alias = "nushell")]
219        Nu,
220        #[serde(alias = "powershell")]
221        Pwsh,
222        Xonsh,
223        Zsh,
224    }
225);
226
227config_unit_enum!(
228    /// A list of available shells on Windows.
229    #[derive(ConfigEnum)]
230    pub enum TaskWindowsShell {
231        Bash,
232        Elvish,
233        Fish,
234        Murex,
235        #[serde(alias = "nushell")]
236        Nu,
237        #[default]
238        #[serde(alias = "powershell")]
239        Pwsh,
240        Xonsh,
241    }
242);
243
244config_struct!(
245    /// Options to control task inheritance, execution, and more.
246    #[derive(Config)]
247    #[serde(default)]
248    pub struct TaskOptionsConfig {
249        /// The pattern in which affected files will be passed to the task.
250        #[serde(skip_serializing_if = "Option::is_none")]
251        pub affected_files: Option<TaskOptionAffectedFilesEntry>,
252
253        /// Allow the task to fail without failing the entire action pipeline.
254        /// @since 1.13.0
255        #[serde(skip_serializing_if = "Option::is_none")]
256        pub allow_failure: Option<bool>,
257
258        /// Cache the `outputs` of the task for incremental builds.
259        /// Defaults to `true` if outputs are configured for the task.
260        #[serde(skip_serializing_if = "Option::is_none")]
261        pub cache: Option<TaskOptionCache>,
262
263        /// A custom key to include in the cache hashing process. Can be
264        /// used to invalidate local and remote caches.
265        #[serde(skip_serializing_if = "Option::is_none")]
266        pub cache_key: Option<String>,
267
268        /// Lifetime to cache the task itself, in the format of "1h", "30m", etc.
269        /// If not defined, caches live forever, or until inputs change.
270        /// @since 1.29.0
271        #[serde(skip_serializing_if = "Option::is_none")]
272        pub cache_lifetime: Option<String>,
273
274        /// Loads and sets environment variables from the `.env` file(s) when
275        /// running the task.
276        #[serde(skip_serializing_if = "Option::is_none")]
277        pub env_file: Option<TaskOptionEnvFile>,
278
279        /// Automatically infer inputs from file groups or environment variables
280        /// that were utilized within `command`, `script`, `args`, and `env`.
281        /// @since 1.31.0
282        #[serde(skip_serializing_if = "Option::is_none")]
283        pub infer_inputs: Option<bool>,
284
285        /// Marks the task as interactive, so that it will run in isolation,
286        /// and have direct access to stdin.
287        /// @since 1.12.0
288        #[setting(validate = validate_interactive)]
289        #[serde(skip_serializing_if = "Option::is_none")]
290        pub interactive: Option<bool>,
291
292        /// Marks the task as internal, which disables it from being ran
293        /// from the command line, but can still be depended on by other tasks.
294        /// @since 1.23.0
295        #[serde(skip_serializing_if = "Option::is_none")]
296        pub internal: Option<bool>,
297
298        /// The default strategy to use when merging `args`, `deps`, `env`,
299        /// `inputs`, or `outputs` with an inherited task. Can be overridden
300        /// with the other field-specific merge options.
301        #[serde(skip_serializing_if = "Option::is_none")]
302        pub merge: Option<TaskMergeStrategy>,
303
304        /// The strategy to use when merging `args` with an inherited task.
305        #[serde(skip_serializing_if = "Option::is_none")]
306        pub merge_args: Option<TaskMergeStrategy>,
307
308        /// The strategy to use when merging `deps` with an inherited task.
309        #[serde(skip_serializing_if = "Option::is_none")]
310        pub merge_deps: Option<TaskMergeStrategy>,
311
312        /// The strategy to use when merging `env` with an inherited task.
313        #[serde(skip_serializing_if = "Option::is_none")]
314        pub merge_env: Option<TaskMergeStrategy>,
315
316        /// The strategy to use when merging `inputs` with an inherited task.
317        #[serde(skip_serializing_if = "Option::is_none")]
318        pub merge_inputs: Option<TaskMergeStrategy>,
319
320        /// The strategy to use when merging `outputs` with an inherited task.
321        #[serde(skip_serializing_if = "Option::is_none")]
322        pub merge_outputs: Option<TaskMergeStrategy>,
323
324        /// The strategy to use when merging `toolchains` with an inherited task.
325        /// @since 2.0.0
326        #[serde(skip_serializing_if = "Option::is_none")]
327        pub merge_toolchains: Option<TaskMergeStrategy>,
328
329        /// Creates an exclusive lock on a virtual resource, preventing other
330        /// tasks using the same resource from running concurrently.
331        /// @since 1.24.0
332        #[serde(skip_serializing_if = "Option::is_none")]
333        pub mutex: Option<String>,
334
335        /// The operating system in which to only run this task on.
336        #[serde(skip_serializing_if = "Option::is_none")]
337        pub os: Option<OneOrMany<TaskOperatingSystem>>,
338
339        /// The style in which task output will be printed to the console.
340        #[setting(env = "MOON_OUTPUT_STYLE")]
341        #[serde(skip_serializing_if = "Option::is_none")]
342        pub output_style: Option<TaskOutputStyle>,
343
344        /// Marks the task as persistent (continuously running). This is ideal
345        /// for watchers, servers, or never-ending processes.
346        /// @since 1.6.0
347        #[serde(skip_serializing_if = "Option::is_none")]
348        pub persistent: Option<bool>,
349
350        /// Marks the task with a certain priority, which determines the order
351        /// in which it is ran within the action pipeline.
352        #[serde(skip_serializing_if = "Option::is_none")]
353        pub priority: Option<TaskPriority>,
354
355        /// The number of times a failing task will be retried to succeed.
356        #[setting(env = "MOON_RETRY_COUNT")]
357        #[serde(skip_serializing_if = "Option::is_none")]
358        pub retry_count: Option<u8>,
359
360        /// Runs direct task dependencies (via `deps`) in sequential order.
361        /// This _does not_ apply to indirect or transient dependencies.
362        #[serde(skip_serializing_if = "Option::is_none")]
363        pub run_deps_in_parallel: Option<bool>,
364
365        /// Whether to run the task in CI or not, when executing `moon ci`,
366        /// `moon check`, or `moon run`.
367        #[setting(rename = "runInCI")]
368        #[serde(skip_serializing_if = "Option::is_none")]
369        pub run_in_ci: Option<TaskOptionRunInCI>,
370
371        /// Runs the task from the workspace root, instead of the project root.
372        #[serde(skip_serializing_if = "Option::is_none")]
373        pub run_from_workspace_root: Option<bool>,
374
375        /// Runs the task within a shell. When not defined, runs the task
376        /// directly while relying on native `PATH` resolution.
377        #[serde(skip_serializing_if = "Option::is_none")]
378        pub shell: Option<bool>,
379
380        /// The maximum time in seconds that a task can run before being cancelled.
381        #[serde(skip_serializing_if = "Option::is_none")]
382        pub timeout: Option<u64>,
383
384        /// The shell to run the task in when on a Unix-based machine.
385        /// @since 1.21.0
386        #[serde(skip_serializing_if = "Option::is_none")]
387        pub unix_shell: Option<TaskUnixShell>,
388
389        /// The shell to run the task in when on a Windows machine.
390        /// @since 1.21.0
391        #[serde(skip_serializing_if = "Option::is_none")]
392        pub windows_shell: Option<TaskWindowsShell>,
393    }
394);