feat(task): add timeout support for task execution#6216
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR adds configurable timeout support for mise task execution, allowing tasks to be automatically terminated if they exceed a specified duration. The implementation provides three configuration methods with a clear priority hierarchy.
- Added timeout field to Task struct and corresponding CLI flag support
- Implemented timeout logic in CmdLineRunner with cross-platform process termination
- Added comprehensive e2e tests covering all timeout scenarios and priority ordering
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/task/mod.rs | Added optional timeout field to Task struct with serialization support |
| src/cmd.rs | Implemented timeout logic in CmdLineRunner with process termination handling |
| src/cli/run.rs | Added CLI timeout flag, timeout parsing function, and priority resolution logic |
| src/cli/mod.rs | Updated CLI default initialization to include timeout field |
| settings.toml | Added MISE_TASK_TIMEOUT environment variable configuration |
| schema/mise.json | Updated JSON schema to include task_timeout setting |
| e2e/tasks/test_task_timeout | Added comprehensive e2e tests for all timeout scenarios |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
src/cmd.rs
Outdated
| } | ||
| #[cfg(windows)] | ||
| { | ||
| use std::os::windows::process::CommandExt; |
There was a problem hiding this comment.
The use std::os::windows::process::CommandExt import is unused in this context. The CommandExt trait is typically used for process creation configuration, but here you're only using std::process::Command::new() which doesn't require this trait.
| use std::os::windows::process::CommandExt; |
src/cmd.rs
Outdated
| #[cfg(windows)] | ||
| { | ||
| // On Windows, we can't easily create an ExitStatus, so we'll just error out | ||
| return Err(eyre::eyre!("Process terminated without exit status")); |
There was a problem hiding this comment.
The error message is unclear about the cause. Consider updating to 'Process was terminated due to timeout without providing an exit status' to better explain why this condition occurred.
| return Err(eyre::eyre!("Process terminated without exit status")); | |
| return Err(eyre::eyre!("Process was terminated due to timeout without providing an exit status")); |
2861fdb to
85011d1
Compare
Add configurable timeout support for tasks with three methods: - CLI flag: --timeout for mise run command - Task configuration: timeout property in mise.toml - Environment variable: MISE_TASK_TIMEOUT Priority order (highest to lowest): 1. CLI flag (--timeout) 2. Task config (timeout in mise.toml) 3. Environment variable (MISE_TASK_TIMEOUT) Supported timeout formats: - 30 or 30s - 30 seconds - 5m - 5 minutes - 1h - 1 hour - 500ms - 500 milliseconds Tasks that exceed the timeout are terminated with SIGTERM/SIGKILL and report an error. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
612c746 to
2712504
Compare
2712504 to
d91defe
Compare
|
bugbot run |
| #[serde(default)] | ||
| pub usage: String, | ||
| #[serde(default)] | ||
| pub timeout: Option<String>, |
There was a problem hiding this comment.
Bug: Task-Specific Timeouts Not Applied
The timeout field in the Task struct isn't applied during individual task execution. While global timeouts are handled, task.timeout is never used by the CmdLineRunner or other execution methods, making task-specific timeouts non-functional.
Additional Locations (1)
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.9.4 x -- echo |
18.8 ± 0.5 | 18.0 | 21.5 | 1.00 |
mise x -- echo |
19.0 ± 0.9 | 18.1 | 30.7 | 1.01 ± 0.06 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.9.4 env |
18.4 ± 0.6 | 17.6 | 22.7 | 1.00 |
mise env |
18.7 ± 0.9 | 17.6 | 23.4 | 1.02 ± 0.06 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.9.4 hook-env |
17.8 ± 0.4 | 17.1 | 20.5 | 1.00 |
mise hook-env |
17.9 ± 0.6 | 17.1 | 21.6 | 1.01 ± 0.04 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.9.4 ls |
16.4 ± 0.5 | 15.7 | 19.0 | 1.00 |
mise ls |
16.7 ± 0.7 | 15.9 | 20.3 | 1.02 ± 0.05 |
xtasks/test/perf
| Command | mise-2025.9.4 | mise | Variance |
|---|---|---|---|
| install (cached) | 165ms | ✅ 103ms | +60% |
| ls (cached) | 62ms | 62ms | +0% |
| bin-paths (cached) | 66ms | 66ms | +0% |
| task-ls (cached) | 473ms | 475ms | +0% |
✅ Performance improvement: install cached is 60%
### 🚀 Features - **(task)** add timeout support for task execution by @jdx in [#6216](#6216) - **(task)** sub-tasks in run lists by @jdx in [#6212](#6212) ### Chore - fix npm publish action by @jdx in [14f4b09](14f4b09) - fix cloudflare release action by @jdx in [00afa25](00afa25) - fix git-cliff for release notes by @jdx in [15a9aed](15a9aed) Co-authored-by: mise-en-dev <release@mise.jdx.dev>
## [2025.9.5](https://github.com/jdx/mise/compare/v2025.9.4..v2025.9.5) - 2025-09-06 ### 🚀 Features - **(task)** add timeout support for task execution by @jdx in [#6216](jdx/mise#6216) - **(task)** sub-tasks in run lists by @jdx in [#6212](jdx/mise#6212) ### Chore - fix npm publish action by @jdx in [14f4b09](jdx/mise@14f4b09) - fix cloudflare release action by @jdx in [00afa25](jdx/mise@00afa25) - fix git-cliff for release notes by @jdx in [15a9aed](jdx/mise@15a9aed) ## [2025.9.4](https://github.com/jdx/mise/compare/v2025.9.3..v2025.9.4) - 2025-09-06 ### Chore - fix git-cliff on release by @jdx in [3c388f2](jdx/mise@3c388f2) ## [2025.9.3](https://github.com/jdx/mise/compare/v2025.9.2..v2025.9.3) - 2025-09-06 ### 🚀 Features - **(backend)** improve http error when platform url missing; list available platforms by @jdx in [#6200](jdx/mise#6200) - **(cli)** support scoped packages for all backend types by @earlgray283 in [#6213](jdx/mise#6213) - **(http)** add URL replacement feature for HTTP requests by @ThomasSteinbach in [#6207](jdx/mise#6207) ### 🐛 Bug Fixes - **(backend)** preserve arch underscores in platform keys by @jdx in [#6202](jdx/mise#6202) - **(task)** resolve hanging issue with multiple depends_post by @jdx in [#6206](jdx/mise#6206) - couldn't download node binary in Alpine, even if it exists in the mirror url by @Hazer in [#5972](jdx/mise#5972) - **breaking** use config_root for env._.path by @jdx in [#6204](jdx/mise#6204) - bugfix for paths that include spaces by @karim-elkholy in [#6210](jdx/mise#6210) ### 📚 Documentation - improve release notes generation by @jdx in [#6197](jdx/mise#6197) - fix release changelog contributor reporting by @jdx in [#6201](jdx/mise#6201) ### Chore - use fine-grained gh token by @jdx in [#6208](jdx/mise#6208) - use settings.local.json for claude config by @jdx in [fd0fba9](jdx/mise@fd0fba9) ### New Contributors - @ThomasSteinbach made their first contribution in [#6207](jdx/mise#6207) - @earlgray283 made their first contribution in [#6213](jdx/mise#6213) - @karim-elkholy made their first contribution in [#6210](jdx/mise#6210) - @Hazer made their first contribution in [#5972](jdx/mise#5972) ## [2025.9.2](https://github.com/jdx/mise/compare/v2025.9.1..v2025.9.2) - 2025-09-05 ### 🐛 Bug Fixes - **(ci)** set required environment variables for npm publishing by @jdx in [#6189](jdx/mise#6189) - **(release)** clean up extra newlines in release notes formatting by @jdx in [#6190](jdx/mise#6190) - **(release)** add proper newline after New Contributors section in cliff template by @jdx in [#6194](jdx/mise#6194) - **(release)** fix changelog formatting to remove extra blank lines by @jdx in [#6195](jdx/mise#6195) - **(release)** restore proper newline after New Contributors section by @jdx in [#6196](jdx/mise#6196) ### 🚜 Refactor - **(ci)** split release workflow into separate specialized workflows by @jdx in [#6193](jdx/mise#6193) ### Chore - **(release)** require GitHub Actions environment for release-plz script by @jdx in [#6191](jdx/mise#6191) ## [2025.9.1](https://github.com/jdx/mise/compare/v2025.9.0..v2025.9.1) - 2025-09-05 ### 🐛 Bug Fixes - python nested venv path order by @elvismacak in [#6124](jdx/mise#6124) - resolve immutable release workflow and VERSION file timing issues by @jdx in [#6187](jdx/mise#6187) ### New Contributors - @elvismacak made their first contribution in [#6124](jdx/mise#6124) ## [2025.9.0](https://github.com/jdx/mise/compare/v2025.8.21..v2025.9.0) - 2025-09-05 ### 🚀 Features - allow set/unset backend aliases by @roele in [#6172](jdx/mise#6172) ### 🐛 Bug Fixes - **(aqua)** respect order of asset_strs by @risu729 in [#6143](jdx/mise#6143) - **(java)** treat freebsd as linux (assuming linux compatability) by @roele in [#6161](jdx/mise#6161) - **(nushell/windows)** Fix $env.PATH getting converted to a string by @zackyancey in [#6157](jdx/mise#6157) - **(sync)** create uv_versions_path dir if it doesn't exist by @risu729 in [#6142](jdx/mise#6142) - **(ubi)** show relevent error messages for v-prefixed tags by @risu729 in [#6183](jdx/mise#6183) - remove nodejs/golang alias migrate code by @risu729 in [#6141](jdx/mise#6141) - mise activate not working on powershell v5 by @L0RD-ZER0 in [#6168](jdx/mise#6168) ### 📚 Documentation - **(task)** remove word "additional" to avoid confusions by @risu729 in [#6159](jdx/mise#6159) ### Chore - update Cargo.lock by @risu729 in [#6184](jdx/mise#6184) ### New Contributors - @zackyancey made their first contribution in [#6157](jdx/mise#6157)
This enforces timeout behavior to the following precedence: - `task.timeout` applies per task, - `[settings].task_timeout` provides the default for tasks that don’t set one - `--timeout` remains a run-wide ceiling for the entire mise run invocation With this change, this now behaves as expected: ```toml [settings] task_timeout = "500ms" [tasks.slow] run = "sleep 5 && echo done" timeout = "60s" ``` `mise run slow` is allowed to run up to 60s (task-level override), while `mise run --timeout=1s` slow still fails after 1s because the CLI timeout is a global run-wide ceiling. This builds directly on the timeout model introduced in jdx#6216.
This enforces timeout behavior to the following precedence: - `task.timeout` applies per task, - `[settings].task_timeout` provides the default for tasks that don’t set one - `--timeout` remains a run-wide ceiling for the entire mise run invocation With this change, this now behaves as expected: ```toml [settings] task_timeout = "500ms" [tasks.slow] run = "sleep 5 && echo done" timeout = "60s" ``` `mise run slow` is allowed to run up to 60s (task-level override), while `mise run --timeout=1s` slow still fails after 1s because the CLI timeout is a global run-wide ceiling. This builds directly on the timeout model introduced in jdx#6216.
This enforces timeout behavior to the following precedence: - `task.timeout` applies per task, - `[settings].task_timeout` provides the default for tasks that don’t set one - `--timeout` remains a run-wide ceiling for the entire mise run invocation With this change, this now behaves as expected: ```toml [settings] task_timeout = "500ms" [tasks.slow] run = "sleep 5 && echo done" timeout = "60s" ``` `mise run slow` is allowed to run up to 60s (task-level override), while `mise run --timeout=1s slow` still fails after 1s because the CLI timeout is a global run-wide ceiling. This builds directly on the timeout model introduced in jdx#6216.
Per-task `timeout` fields were added in jdx#6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean
Per-task `timeout` fields were added in jdx#6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean
Per-task `timeout` fields were added in jdx#6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean
Per-task `timeout` fields were added in jdx#6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean
Per-task `timeout` fields were added in jdx#6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean
## Summary Per-task `timeout` fields were added in #6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ## Example Usage ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` ## Key changes - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios ## Testing - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean ## LLM use for this PR I initially built this towards a different idea (that `timeout`s win, not the global one) but scrapped it after a chat on Discord. All implementation was done with Opus 4.6 and Codex 5.3, with them reviewing each other's code a few times. Eventually I settled on this implementation, trying to keep the changeset as minimal as possible for review. This is my first public Rust PR; I don't feel super confident in the implementation, but I'm happy to take feedback and go back to the drawing boards with the LLMs if there's a better way to do this. 👍 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## Summary Per-task `timeout` fields were added in jdx#6216 but never enforced at execution time. This PR completes the implementation by adding native timeout support to `CmdLineRunner::execute()`. ## Example Usage ```toml [settings] task_timeout = "30s" [tasks.deploy] run = "npm run deploy" timeout = "5m" [tasks.quick] run = "echo 'done'" timeout = "10s" ``` ```bash mise run deploy # killed after 30s (global wins, it's lower) mise run quick # killed after 10s (task wins, it's lower) mise run --timeout=1m deploy # killed after 30s (global setting still wins) ``` ## Key changes - Added `timeout` field and timer thread to `CmdLineRunner::execute()` - Modified `exec_program` to compute effective timeout from task config and global setting - Enabled all TODO-skipped e2e timeout test scenarios ## Testing - [x] `mise test:e2e test_task_timeout` — all 6 scenarios pass - [x] `cargo clippy --all-features` — clean ## LLM use for this PR I initially built this towards a different idea (that `timeout`s win, not the global one) but scrapped it after a chat on Discord. All implementation was done with Opus 4.6 and Codex 5.3, with them reviewing each other's code a few times. Eventually I settled on this implementation, trying to keep the changeset as minimal as possible for review. This is my first public Rust PR; I don't feel super confident in the implementation, but I'm happy to take feedback and go back to the drawing boards with the LLMs if there's a better way to do this. 👍 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Summary
This PR adds comprehensive timeout support for
mise runwith three configuration methods:MISE_TASK_TIMEOUT- Sets a global timeout for entiremise runexecution--timeout- Sets a global timeout for entiremise runexecution (overrides env var)timeoutfield in task definition - Sets timeout for individual tasksImplementation Details
mise runexecution usingtokio::time::timeout. If multiple tasks are running, this timeout applies to the total execution time.CmdLineRunnerwhich handles process termination when the timeout is exceeded.duration::parse_durationfunction for parsing duration strings (supports formats like "30s", "5m", "1h")type = "Duration"with a helper method for conversionKey Changes
timeoutfield toTaskstruct--timeoutCLI flag tomise runcommandMISE_TASK_TIMEOUTto settings.toml as Duration typeexec_programto pass task timeout toCmdLineRunnerTesting
All timeout scenarios are tested:
Example Usage
Fixes: Implements timeout support for task execution as requested
🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com