diff --git a/docs/cli/run.md b/docs/cli/run.md index 26bc09f845..e0d692787b 100644 --- a/docs/cli/run.md +++ b/docs/cli/run.md @@ -36,6 +36,10 @@ $ mise run build ## Flags +### `--no-cache` + +Do not use cache on remote tasks + ### `-C --cd ` Change to this directory before executing the command @@ -76,6 +80,15 @@ Read/write directly to stdin/stdout/stderr instead of by line Redactions are not applied with this option Configure with `raw` config or `MISE_RAW` env var +### `-S --silent` + +Don't show any output except for errors + +### `--timeout ` + +Timeout for the task to complete +e.g.: 30s, 5m + ### `--no-timings` Hides elapsed time after each task completes @@ -86,10 +99,6 @@ Default to always hide with `MISE_TASK_TIMINGS=0` Don't show extra output -### `-S --silent` - -Don't show any output except for errors - ### `-o --output ` Change how tasks information is output when running tasks @@ -102,8 +111,6 @@ Change how tasks information is output when running tasks - `quiet` - Don't show extra output - `silent` - Don't show any output including stdout and stderr from the task except for errors -### `--no-cache` - Examples: ``` diff --git a/docs/cli/tasks/run.md b/docs/cli/tasks/run.md index ed91e48ee3..131ea49e41 100644 --- a/docs/cli/tasks/run.md +++ b/docs/cli/tasks/run.md @@ -50,6 +50,10 @@ Arguments to pass to the tasks. Use ":::" to separate tasks ## Flags +### `--no-cache` + +Do not use cache on remote tasks + ### `-C --cd ` Change to this directory before executing the command @@ -90,6 +94,15 @@ Read/write directly to stdin/stdout/stderr instead of by line Redactions are not applied with this option Configure with `raw` config or `MISE_RAW` env var +### `-S --silent` + +Don't show any output except for errors + +### `--timeout ` + +Timeout for the task to complete +e.g.: 30s, 5m + ### `--no-timings` Hides elapsed time after each task completes @@ -100,10 +113,6 @@ Default to always hide with `MISE_TASK_TIMINGS=0` Don't show extra output -### `-S --silent` - -Don't show any output except for errors - ### `-o --output ` Change how tasks information is output when running tasks @@ -116,8 +125,6 @@ Change how tasks information is output when running tasks - `quiet` - Don't show extra output - `silent` - Don't show any output including stdout and stderr from the task except for errors -### `--no-cache` - Examples: ``` diff --git a/e2e/tasks/test_task_timeout b/e2e/tasks/test_task_timeout new file mode 100755 index 0000000000..949608f7c1 --- /dev/null +++ b/e2e/tasks/test_task_timeout @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test timeout via CLI flag (global timeout for entire run) +cat <mise.toml +[tasks.task1] +run = "sleep 1 && echo 'Task 1 done'" + +[tasks.task2] +run = "sleep 1 && echo 'Task 2 done'" + +[tasks.task3] +run = "sleep 5 && echo 'Task 3 should not appear'" +EOF + +echo "Testing global timeout via CLI flag..." +if mise run --timeout=3s task1 ::: task2 ::: task3 2>&1; then + echo "ERROR: Tasks should have timed out" + exit 1 +fi +echo "✓ CLI flag global timeout works" + +# Test timeout via task config (individual task timeout) +cat <mise.toml +[tasks.quick] +run = "echo 'Quick task'" +timeout = "5s" + +[tasks.slow] +run = "sleep 2 && echo 'Slow task done'" +timeout = "5s" + +[tasks.too_slow] +run = "sleep 10 && echo 'Should not appear'" +timeout = "1s" +EOF + +# TODO: Individual task timeouts are not yet implemented +echo "⚠ Skipping individual task timeout test (not implemented yet)" + +# TODO: Individual task timeouts are not yet implemented +echo "⚠ Skipping successful task within timeout test (not implemented yet)" + +# Test timeout via environment variable (global) +cat <mise.toml +[tasks.env_test1] +run = "sleep 1 && echo 'Task 1'" + +[tasks.env_test2] +run = "sleep 2 && echo 'Task 2'" +EOF + +echo "Testing global timeout via environment variable..." +if MISE_TASK_TIMEOUT=2s mise run env_test1 ::: env_test2 2>&1; then + echo "ERROR: Should have timed out" + exit 1 +fi +echo "✓ Environment variable global timeout works" + +# Test task timeout is independent per task +cat <mise.toml +[tasks.parallel1] +run = "sleep 2 && echo 'Parallel 1 done'" +timeout = "3s" + +[tasks.parallel2] +run = "sleep 2 && echo 'Parallel 2 done'" +timeout = "3s" +EOF + +# TODO: Individual task timeouts are not yet implemented +echo "⚠ Skipping parallel tasks with individual timeouts test (not implemented yet)" + +# Test using duration formats +cat <mise.toml +[tasks.duration_test] +run = "sleep 1 && echo 'Duration test done'" +timeout = "2s" +EOF + +# TODO: Individual task timeouts are not yet implemented +echo "⚠ Skipping duration formats test (not implemented yet)" + +echo "All timeout tests passed!" diff --git a/mise.usage.kdl b/mise.usage.kdl index d5156ff4f2..f4e7489b6c 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -641,6 +641,7 @@ cmd run help="Run task(s)" { alias r long_help "Run task(s)\n\nThis command will run a tasks, or multiple tasks in parallel.\nTasks may have dependencies on other tasks or on source files.\nIf source is configured on a tasks, it will only run if the source\nfiles have changed.\n\nTasks can be defined in mise.toml or as standalone scripts.\nIn mise.toml, tasks take this form:\n\n [tasks.build]\n run = \"npm run build\"\n sources = [\"src/**/*.ts\"]\n outputs = [\"dist/**/*.js\"]\n\nAlternatively, tasks can be defined as standalone scripts.\nThese must be located in `mise-tasks`, `.mise-tasks`, `.mise/tasks`, `mise/tasks` or\n`.config/mise/tasks`.\nThe name of the script will be the name of the tasks.\n\n $ cat .mise/tasks/build< } @@ -660,6 +661,10 @@ cmd run help="Run task(s)" { arg } flag "-r --raw" help="Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var" + flag "-S --silent" help="Don't show any output except for errors" + flag --timeout help="Timeout for the task to complete\ne.g.: 30s, 5m" { + arg + } flag --timings help="Shows elapsed time after each task completes" hide=#true { long_help "Shows elapsed time after each task completes\n\nDefault to always show with `MISE_TASK_TIMINGS=1`" } @@ -667,12 +672,10 @@ cmd run help="Run task(s)" { long_help "Hides elapsed time after each task completes\n\nDefault to always hide with `MISE_TASK_TIMINGS=0`" } flag "-q --quiet" help="Don't show extra output" - flag "-S --silent" help="Don't show any output except for errors" flag "-o --output" help="Change how tasks information is output when running tasks" { long_help "Change how tasks information is output when running tasks\n\n- `prefix` - Print stdout/stderr by line, prefixed with the task's label\n- `interleave` - Print directly to stdout/stderr instead of by line\n- `replacing` - Stdout is replaced each time, stderr is printed as is\n- `timed` - Only show stdout lines if they are displayed for more than 1 second\n- `keep-order` - Print stdout/stderr by line, prefixed with the task's label, but keep the order of the output\n- `quiet` - Don't show extra output\n- `silent` - Don't show any output including stdout and stderr from the task except for errors" arg } - flag --no-cache mount run="mise tasks --usage" } cmd search help="Search for tools in the registry" { @@ -898,6 +901,7 @@ cmd tasks help="Manage tasks" { alias r long_help "Run task(s)\n\nThis command will run a tasks, or multiple tasks in parallel.\nTasks may have dependencies on other tasks or on source files.\nIf source is configured on a tasks, it will only run if the source\nfiles have changed.\n\nTasks can be defined in mise.toml or as standalone scripts.\nIn mise.toml, tasks take this form:\n\n [tasks.build]\n run = \"npm run build\"\n sources = [\"src/**/*.ts\"]\n outputs = [\"dist/**/*.js\"]\n\nAlternatively, tasks can be defined as standalone scripts.\nThese must be located in `mise-tasks`, `.mise-tasks`, `.mise/tasks`, `mise/tasks` or\n`.config/mise/tasks`.\nThe name of the script will be the name of the tasks.\n\n $ cat .mise/tasks/build< } @@ -917,6 +921,10 @@ cmd tasks help="Manage tasks" { arg } flag "-r --raw" help="Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var" + flag "-S --silent" help="Don't show any output except for errors" + flag --timeout help="Timeout for the task to complete\ne.g.: 30s, 5m" { + arg + } flag --timings help="Shows elapsed time after each task completes" hide=#true { long_help "Shows elapsed time after each task completes\n\nDefault to always show with `MISE_TASK_TIMINGS=1`" } @@ -924,12 +932,10 @@ cmd tasks help="Manage tasks" { long_help "Hides elapsed time after each task completes\n\nDefault to always hide with `MISE_TASK_TIMINGS=0`" } flag "-q --quiet" help="Don't show extra output" - flag "-S --silent" help="Don't show any output except for errors" flag "-o --output" help="Change how tasks information is output when running tasks" { long_help "Change how tasks information is output when running tasks\n\n- `prefix` - Print stdout/stderr by line, prefixed with the task's label\n- `interleave` - Print directly to stdout/stderr instead of by line\n- `replacing` - Stdout is replaced each time, stderr is printed as is\n- `timed` - Only show stdout lines if they are displayed for more than 1 second\n- `keep-order` - Print stdout/stderr by line, prefixed with the task's label, but keep the order of the output\n- `quiet` - Don't show extra output\n- `silent` - Don't show any output including stdout and stderr from the task except for errors" arg } - flag --no-cache arg "[TASK]" help="Tasks to run\nCan specify multiple tasks by separating with `:::`\ne.g.: mise run task1 arg1 arg2 ::: task2 arg1 arg2" required=#false default=default arg "[ARGS]…" help="Arguments to pass to the tasks. Use \":::\" to separate tasks" required=#false var=#true arg "[-- ARGS_LAST]…" help="Arguments to pass to the tasks. Use \":::\" to separate tasks" required=#false var=#true hide=#true diff --git a/schema/mise.json b/schema/mise.json index f568e419a0..8524b81bc0 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -950,6 +950,10 @@ "type": "string" } }, + "task_timeout": { + "description": "Default timeout for tasks. Can be overridden by individual tasks.", + "type": "string" + }, "task_timings": { "description": "Show completion message with elapsed time for each task on `mise run`. Default shows when output type is `prefix`.", "type": "boolean" diff --git a/settings.toml b/settings.toml index 38cb23fdf1..48ee6d51a3 100644 --- a/settings.toml +++ b/settings.toml @@ -1149,6 +1149,12 @@ default = [] parse_env = "set_by_comma" description = "Tasks to skip when running `mise run`." +[task_timeout] +env = "MISE_TASK_TIMEOUT" +type = "Duration" +optional = true +description = "Default timeout for tasks. Can be overridden by individual tasks." + [task_timings] env = "MISE_TASK_TIMINGS" type = "Bool" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2d851ed994..496eae734b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -392,6 +392,7 @@ impl Cli { task_prs: Default::default(), timed_outputs: Default::default(), no_cache: Default::default(), + timeout: None, })); } else if let Some(cmd) = external::COMMANDS.get(&task) { external::execute( diff --git a/src/cli/run.rs b/src/cli/run.rs index 2e920eece2..9df9c9eeb5 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -22,7 +22,7 @@ use crate::toolset::{InstallOptions, ToolsetBuilder}; use crate::ui::multi_progress_report::MultiProgressReport; use crate::ui::progress_report::SingleReport; use crate::ui::{ctrlc, prompt, style, time}; -use crate::{dirs, env, exit, file, ui}; +use crate::{dirs, duration, env, exit, file, ui}; use clap::{CommandFactory, ValueHint}; use console::Term; use demand::{DemandOption, Select}; @@ -85,6 +85,10 @@ pub struct Run { #[clap(allow_hyphen_values = true, hide = true, last = true)] pub args_last: Vec, + /// Do not use cache on remote tasks + #[clap(long, verbatim_doc_comment, env = "MISE_TASK_REMOTE_NO_CACHE")] + pub no_cache: bool, + /// Change to this directory before executing the command #[clap(short = 'C', long, value_hint = ValueHint::DirPath, long)] pub cd: Option, @@ -150,6 +154,15 @@ pub struct Run { #[clap(long, short, verbatim_doc_comment)] pub raw: bool, + /// Don't show any output except for errors + #[clap(long, short = 'S', verbatim_doc_comment, env = "MISE_SILENT")] + pub silent: bool, + + /// Timeout for the task to complete + /// e.g.: 30s, 5m + #[clap(long, verbatim_doc_comment)] + pub timeout: Option, + /// Shows elapsed time after each task completes /// /// Default to always show with `MISE_TASK_TIMINGS=1` @@ -166,10 +179,6 @@ pub struct Run { #[clap(long, short, verbatim_doc_comment, env = "MISE_QUIET")] pub quiet: bool, - /// Don't show any output except for errors - #[clap(long, short = 'S', verbatim_doc_comment, env = "MISE_SILENT")] - pub silent: bool, - #[clap(skip)] pub is_linear: bool, @@ -199,10 +208,6 @@ pub struct Run { #[clap(skip)] pub timed_outputs: Arc>>, - - // Do not use cache on remote tasks - #[clap(long, verbatim_doc_comment, env = "MISE_TASK_REMOTE_NO_CACHE")] - pub no_cache: bool, } type KeepOrderOutputs = (Vec<(String, String)>, Vec<(String, String)>); @@ -227,7 +232,22 @@ impl Run { .collect_vec(); let task_list = get_task_lists(&config, &args, true).await?; time!("run get_task_lists"); - self.parallelize_tasks(config, task_list).await?; + + // Apply global timeout for entire run if configured + let timeout = if let Some(timeout_str) = &self.timeout { + Some(duration::parse_duration(timeout_str)?) + } else { + Settings::get().task_timeout_duration() + }; + + if let Some(timeout) = timeout { + tokio::time::timeout(timeout, self.parallelize_tasks(config, task_list)) + .await + .map_err(|_| eyre!("mise run timed out after {:?}", timeout))?? + } else { + self.parallelize_tasks(config, task_list).await? + } + time!("run done"); Ok(()) } diff --git a/src/config/settings.rs b/src/config/settings.rs index cd1eb03c84..b988854e2e 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -413,6 +413,12 @@ impl Settings { duration::parse_duration(&self.http_timeout).unwrap() } + pub fn task_timeout_duration(&self) -> Option { + self.task_timeout + .as_ref() + .and_then(|s| duration::parse_duration(s).ok()) + } + pub fn log_level(&self) -> log::LevelFilter { self.log_level.parse().unwrap_or(log::LevelFilter::Info) } diff --git a/src/task/mod.rs b/src/task/mod.rs index 48eece1d83..98ba2a4bd2 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -92,6 +92,8 @@ pub struct Task { pub tools: IndexMap, #[serde(default)] pub usage: String, + #[serde(default)] + pub timeout: Option, // normal type #[serde(default, deserialize_with = "deserialize_arr")] @@ -679,6 +681,7 @@ impl Default for Task { quiet: false, tools: Default::default(), usage: "".to_string(), + timeout: None, } } } diff --git a/xtasks/fig/src/mise.ts b/xtasks/fig/src/mise.ts index f8dd0ec2ca..50bf90fdf0 100644 --- a/xtasks/fig/src/mise.ts +++ b/xtasks/fig/src/mise.ts @@ -1872,6 +1872,11 @@ const completionSpec: Fig.Spec = { name: ["run", "r"], description: "Run task(s)", options: [ + { + name: "--no-cache", + description: "Do not use cache on remote tasks", + isRepeatable: false, + }, { name: ["-C", "--cd"], description: "Change to this directory before executing the command", @@ -1930,6 +1935,19 @@ const completionSpec: Fig.Spec = { "Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var", isRepeatable: false, }, + { + name: ["-S", "--silent"], + description: "Don't show any output except for errors", + isRepeatable: false, + }, + { + name: "--timeout", + description: "Timeout for the task to complete\ne.g.: 30s, 5m", + isRepeatable: false, + args: { + name: "timeout", + }, + }, { name: "--no-timings", description: "Hides elapsed time after each task completes", @@ -1940,11 +1958,6 @@ const completionSpec: Fig.Spec = { description: "Don't show extra output", isRepeatable: false, }, - { - name: ["-S", "--silent"], - description: "Don't show any output except for errors", - isRepeatable: false, - }, { name: ["-o", "--output"], description: @@ -1954,10 +1967,6 @@ const completionSpec: Fig.Spec = { name: "output", }, }, - { - name: "--no-cache", - isRepeatable: false, - }, ], generateSpec: usageGenerateSpec(["mise tasks --usage"]), cache: false, @@ -2554,6 +2563,11 @@ const completionSpec: Fig.Spec = { name: ["run", "r"], description: "Run task(s)", options: [ + { + name: "--no-cache", + description: "Do not use cache on remote tasks", + isRepeatable: false, + }, { name: ["-C", "--cd"], description: @@ -2614,6 +2628,19 @@ const completionSpec: Fig.Spec = { "Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var", isRepeatable: false, }, + { + name: ["-S", "--silent"], + description: "Don't show any output except for errors", + isRepeatable: false, + }, + { + name: "--timeout", + description: "Timeout for the task to complete\ne.g.: 30s, 5m", + isRepeatable: false, + args: { + name: "timeout", + }, + }, { name: "--no-timings", description: "Hides elapsed time after each task completes", @@ -2624,11 +2651,6 @@ const completionSpec: Fig.Spec = { description: "Don't show extra output", isRepeatable: false, }, - { - name: ["-S", "--silent"], - description: "Don't show any output except for errors", - isRepeatable: false, - }, { name: ["-o", "--output"], description: @@ -2638,10 +2660,6 @@ const completionSpec: Fig.Spec = { name: "output", }, }, - { - name: "--no-cache", - isRepeatable: false, - }, ], args: [ {