Compare commits

..

1 Commits

Author SHA1 Message Date
Valentin Maerten
24bbb61d8f feat: add command-level timeout support
Add a per-command `timeout` option that terminates a command once it
exceeds the given duration, preventing commands from hanging indefinitely
in a pipeline. Uses Go duration syntax (e.g. 30s, 5m, 1h30m) and applies
to both shell commands and task calls.

Closes #1569
2026-06-30 22:25:48 +02:00
15 changed files with 277 additions and 219 deletions

View File

@@ -19,21 +19,21 @@ jobs:
go-version: [1.25.10, 1.26.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: v2.12.2
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14

View File

@@ -18,7 +18,7 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.x

View File

@@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.x

View File

@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}

View File

@@ -2,7 +2,7 @@
# Runtimes
go = "1.26.4"
node = "24"
pnpm = "11.9.0"
pnpm = "11.8.0"
# Dev tools
golangci-lint = "2.12.2"

12
task.go
View File

@@ -376,12 +376,21 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
}
}
if cmd.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, cmd.Timeout)
defer cancel()
}
switch {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil && ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("task: [%s] command timeout exceeded (%s): %w", t.Name(), cmd.Timeout, err)
}
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
@@ -426,6 +435,9 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
}
if err != nil && ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("task: [%s] command timeout exceeded (%s): %w", t.Name(), cmd.Timeout, err)
}
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)

View File

@@ -2497,6 +2497,63 @@ func TestErrorCode(t *testing.T) {
}
}
func TestCommandTimeout(t *testing.T) {
t.Parallel()
const dir = "testdata/timeout"
tests := []struct {
name string
task string
expectError bool
errorContains string
}{
{
name: "timeout exceeded",
task: "timeout-exceeded",
expectError: true,
errorContains: "timeout exceeded",
},
{
name: "timeout not exceeded",
task: "timeout-not-exceeded",
expectError: false,
},
{
name: "no timeout",
task: "no-timeout",
expectError: false,
},
{
name: "multiple commands with timeout",
task: "multiple-cmds-timeout",
expectError: true,
errorContains: "timeout exceeded",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())
err := e.Run(t.Context(), &task.Call{Task: test.task})
if test.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), test.errorContains)
} else {
require.NoError(t, err)
}
})
}
}
func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // cannot run in parallel
const dir = "testdata/evaluate_symlinks_in_paths"
var buff bytes.Buffer

View File

@@ -1,6 +1,8 @@
package ast
import (
"time"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
@@ -21,6 +23,7 @@ type Cmd struct {
IgnoreError bool
Defer bool
Platforms []*Platform
Timeout time.Duration
}
func (c *Cmd) DeepCopy() *Cmd {
@@ -40,6 +43,7 @@ func (c *Cmd) DeepCopy() *Cmd {
IgnoreError: c.IgnoreError,
Defer: c.Defer,
Platforms: deepcopy.Slice(c.Platforms),
Timeout: c.Timeout,
}
}
@@ -67,10 +71,20 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
IgnoreError bool `yaml:"ignore_error"`
Defer *Defer
Platforms []*Platform
Timeout string
}
if err := node.Decode(&cmdStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if cmdStruct.Timeout != "" {
timeout, err := time.ParseDuration(cmdStruct.Timeout)
if err != nil {
return errors.NewTaskfileDecodeError(err, node).WithMessage("invalid timeout format")
}
c.Timeout = timeout
}
if cmdStruct.Defer != nil {
// A deferred command

View File

@@ -98,9 +98,6 @@ func (vars *Vars) Values() iter.Seq[Var] {
// ToCacheMap converts Vars to an unordered map containing only the static
// variables
func (vars *Vars) ToCacheMap() (m map[string]any) {
if vars == nil || vars.om == nil {
return nil
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
m = make(map[string]any, vars.Len())

View File

@@ -1,55 +0,0 @@
package ast
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestVars_ToCacheMap(t *testing.T) {
t.Parallel()
t.Run("nil receiver returns nil", func(t *testing.T) {
t.Parallel()
var vars *Vars
assert.Nil(t, vars.ToCacheMap())
})
t.Run("empty vars returns empty map", func(t *testing.T) {
t.Parallel()
vars := NewVars()
m := vars.ToCacheMap()
assert.NotNil(t, m)
assert.Empty(t, m)
})
t.Run("static values are included", func(t *testing.T) {
t.Parallel()
vars := NewVars(
&VarElement{Key: "FOO", Value: Var{Value: "bar"}},
&VarElement{Key: "NUM", Value: Var{Value: 42}},
)
m := vars.ToCacheMap()
assert.Equal(t, map[string]any{"FOO": "bar", "NUM": 42}, m)
})
t.Run("live values take precedence over static values", func(t *testing.T) {
t.Parallel()
vars := NewVars(
&VarElement{Key: "FOO", Value: Var{Value: "bar", Live: "live-bar"}},
)
m := vars.ToCacheMap()
assert.Equal(t, map[string]any{"FOO": "live-bar"}, m)
})
t.Run("dynamic variables are excluded", func(t *testing.T) {
t.Parallel()
sh := "echo hello"
vars := NewVars(
&VarElement{Key: "STATIC", Value: Var{Value: "ok"}},
&VarElement{Key: "DYNAMIC", Value: Var{Sh: &sh}},
)
m := vars.ToCacheMap()
assert.Equal(t, map[string]any{"STATIC": "ok"}, m)
})
}

29
testdata/timeout/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
version: '3'
tasks:
timeout-exceeded:
desc: Command that should timeout
cmds:
- cmd: sleep 10
timeout: 1s
timeout-not-exceeded:
desc: Command that completes within timeout
cmds:
- cmd: echo "quick command"
timeout: 5s
no-timeout:
desc: Command with no timeout specified
cmds:
- echo "no timeout"
multiple-cmds-timeout:
desc: Multiple commands where one exceeds its timeout
cmds:
- cmd: echo "first"
timeout: 1s
- cmd: sleep 10
timeout: 1s
- cmd: echo "third"
timeout: 1s

View File

@@ -23,5 +23,5 @@
"vitepress-plugin-llms": "^1.9.1",
"vue": "^3.5.18"
},
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
}

275
website/pnpm-lock.yaml generated
View File

@@ -16,10 +16,10 @@ importers:
version: 24.13.2
netlify-cli:
specifier: ^26.0.0
version: 26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)(supports-color@10.2.2)
version: 26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)
prettier:
specifier: ^3.6.2
version: 3.9.4
version: 3.8.4
vitepress:
specifier: ^1.6.3
version: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3)
@@ -28,13 +28,13 @@ importers:
version: 1.7.5(vite@5.4.21(@types/node@24.13.2))
vitepress-plugin-llms:
specifier: ^1.9.1
version: 1.13.2
version: 1.13.1
vitepress-plugin-tabs:
specifier: ^0.9.0
version: 0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.39(typescript@5.9.3))
version: 0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.38(typescript@5.9.3))
vue:
specifier: ^3.5.18
version: 3.5.39(typescript@5.9.3)
version: 3.5.38(typescript@5.9.3)
packages:
@@ -1480,17 +1480,17 @@ packages:
vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25
'@vue/compiler-core@3.5.39':
resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==}
'@vue/compiler-core@3.5.38':
resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==}
'@vue/compiler-dom@3.5.39':
resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==}
'@vue/compiler-dom@3.5.38':
resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==}
'@vue/compiler-sfc@3.5.39':
resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==}
'@vue/compiler-sfc@3.5.38':
resolution: {integrity: sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==}
'@vue/compiler-ssr@3.5.39':
resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==}
'@vue/compiler-ssr@3.5.38':
resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==}
'@vue/devtools-api@7.7.7':
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
@@ -1501,25 +1501,25 @@ packages:
'@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/reactivity@3.5.39':
resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==}
'@vue/reactivity@3.5.38':
resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
'@vue/runtime-core@3.5.39':
resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==}
'@vue/runtime-core@3.5.38':
resolution: {integrity: sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==}
'@vue/runtime-dom@3.5.39':
resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==}
'@vue/runtime-dom@3.5.38':
resolution: {integrity: sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==}
'@vue/server-renderer@3.5.39':
resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==}
'@vue/server-renderer@3.5.38':
resolution: {integrity: sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==}
peerDependencies:
vue: 3.5.39
vue: 3.5.38
'@vue/shared@3.5.18':
resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==}
'@vue/shared@3.5.39':
resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==}
'@vue/shared@3.5.38':
resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==}
'@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
@@ -1853,8 +1853,8 @@ packages:
brace-expansion@2.1.1:
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
brace-expansion@5.0.7:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
@@ -3232,8 +3232,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
json-schema-ref-resolver@3.0.0:
@@ -3629,11 +3629,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanospinner@1.2.2:
resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==}
@@ -4015,10 +4010,6 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
@@ -4051,8 +4042,8 @@ packages:
resolution: {integrity: sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==}
engines: {node: '>= 0.6'}
prettier@3.9.4:
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
prettier@3.8.4:
resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==}
engines: {node: '>=14'}
hasBin: true
@@ -4980,8 +4971,8 @@ packages:
vite:
optional: true
vitepress-plugin-llms@1.13.2:
resolution: {integrity: sha512-2O4s0I5pjEZzgnoWgBPCZCyhah9FH5uQB6lGADazMoyF1URJshtG04ZnmX+cbmQmniN3T5JzdJO9B4q8JHDKOQ==}
vitepress-plugin-llms@1.13.1:
resolution: {integrity: sha512-m+rxyghF5INi8hBw0huFPx6+VvaX1tDGvw1H7FdXowaZJ3dcRY5ShgbmK1AQlmeOFMdd16H8WarhSHLPXF/2OA==}
engines: {node: '>=18'}
vitepress-plugin-tabs@0.9.0:
@@ -5002,8 +4993,8 @@ packages:
postcss:
optional: true
vue@3.5.39:
resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==}
vue@3.5.38:
resolution: {integrity: sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -5140,10 +5131,6 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
@@ -5912,7 +5899,7 @@ snapshots:
semver: 7.8.4
tmp-promise: 3.0.3
'@netlify/dev@4.18.7(rollup@4.46.2)(supports-color@10.2.2)':
'@netlify/dev@4.18.7(rollup@4.46.2)':
dependencies:
'@netlify/ai': 0.4.1
'@netlify/blobs': 10.7.9(supports-color@10.2.2)
@@ -5920,9 +5907,9 @@ snapshots:
'@netlify/database-dev': 0.10.1
'@netlify/dev-utils': 4.4.6
'@netlify/edge-functions-dev': 1.0.20
'@netlify/functions-dev': 1.3.0(rollup@4.46.2)(supports-color@10.2.2)
'@netlify/functions-dev': 1.3.0(rollup@4.46.2)
'@netlify/headers': 2.1.11
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9)
'@netlify/redirects': 3.1.13
'@netlify/runtime': 4.1.25
'@netlify/static': 3.1.10
@@ -5995,7 +5982,7 @@ snapshots:
dependencies:
'@netlify/types': 2.8.0
'@netlify/functions-dev@1.3.0(rollup@4.46.2)(supports-color@10.2.2)':
'@netlify/functions-dev@1.3.0(rollup@4.46.2)':
dependencies:
'@netlify/blobs': 10.7.9(supports-color@10.2.2)
'@netlify/dev-utils': 4.4.6
@@ -6003,7 +5990,7 @@ snapshots:
'@netlify/zip-it-and-ship-it': 14.7.1(rollup@4.46.2)(supports-color@10.2.2)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1(supports-color@10.2.2)
extract-zip: 2.0.1
is-stream: 4.0.1
jwt-decode: 4.0.0
lambda-local: 2.2.0
@@ -6055,9 +6042,9 @@ snapshots:
dependencies:
'@netlify/headers-parser': 9.0.3
'@netlify/images@1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))':
'@netlify/images@1.3.10(@netlify/blobs@10.7.9)':
dependencies:
ipx: 3.1.1(@netlify/blobs@10.7.9(supports-color@10.2.2))
ipx: 3.1.1(@netlify/blobs@10.7.9)
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -6436,7 +6423,7 @@ snapshots:
'@pnpm/network.ca-file': 1.0.2
config-chain: 1.1.13
'@pnpm/tabtab@0.5.4(supports-color@10.2.2)':
'@pnpm/tabtab@0.5.4':
dependencies:
debug: 4.4.3(supports-color@10.2.2)
enquirer: 2.4.1
@@ -6689,40 +6676,40 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.39(typescript@5.9.3))':
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.38(typescript@5.9.3))':
dependencies:
vite: 5.4.21(@types/node@24.13.2)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
'@vue/compiler-core@3.5.39':
'@vue/compiler-core@3.5.38':
dependencies:
'@babel/parser': 7.29.7
'@vue/shared': 3.5.39
'@vue/shared': 3.5.38
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.39':
'@vue/compiler-dom@3.5.38':
dependencies:
'@vue/compiler-core': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-core': 3.5.38
'@vue/shared': 3.5.38
'@vue/compiler-sfc@3.5.39':
'@vue/compiler-sfc@3.5.38':
dependencies:
'@babel/parser': 7.29.7
'@vue/compiler-core': 3.5.39
'@vue/compiler-dom': 3.5.39
'@vue/compiler-ssr': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-core': 3.5.38
'@vue/compiler-dom': 3.5.38
'@vue/compiler-ssr': 3.5.38
'@vue/shared': 3.5.38
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.16
postcss: 8.5.15
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.39':
'@vue/compiler-ssr@3.5.38':
dependencies:
'@vue/compiler-dom': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-dom': 3.5.38
'@vue/shared': 3.5.38
'@vue/devtools-api@7.7.7':
dependencies:
@@ -6742,38 +6729,38 @@ snapshots:
dependencies:
rfdc: 1.4.1
'@vue/reactivity@3.5.39':
'@vue/reactivity@3.5.38':
dependencies:
'@vue/shared': 3.5.39
'@vue/shared': 3.5.38
'@vue/runtime-core@3.5.39':
'@vue/runtime-core@3.5.38':
dependencies:
'@vue/reactivity': 3.5.39
'@vue/shared': 3.5.39
'@vue/reactivity': 3.5.38
'@vue/shared': 3.5.38
'@vue/runtime-dom@3.5.39':
'@vue/runtime-dom@3.5.38':
dependencies:
'@vue/reactivity': 3.5.39
'@vue/runtime-core': 3.5.39
'@vue/shared': 3.5.39
'@vue/reactivity': 3.5.38
'@vue/runtime-core': 3.5.38
'@vue/shared': 3.5.38
csstype: 3.2.3
'@vue/server-renderer@3.5.39(vue@3.5.39(typescript@5.9.3))':
'@vue/server-renderer@3.5.38(vue@3.5.38(typescript@5.9.3))':
dependencies:
'@vue/compiler-ssr': 3.5.39
'@vue/shared': 3.5.39
vue: 3.5.39(typescript@5.9.3)
'@vue/compiler-ssr': 3.5.38
'@vue/shared': 3.5.38
vue: 3.5.38(typescript@5.9.3)
'@vue/shared@3.5.18': {}
'@vue/shared@3.5.39': {}
'@vue/shared@3.5.38': {}
'@vueuse/core@12.8.2(typescript@5.9.3)':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 12.8.2
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -6781,7 +6768,7 @@ snapshots:
dependencies:
'@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
optionalDependencies:
focus-trap: 7.6.5
jwt-decode: 4.0.0
@@ -6792,7 +6779,7 @@ snapshots:
'@vueuse/shared@12.8.2(typescript@5.9.3)':
dependencies:
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -7071,7 +7058,7 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
body-parser@2.3.0(supports-color@10.2.2):
body-parser@2.3.0:
dependencies:
bytes: 3.1.2
content-type: 2.0.0
@@ -7102,7 +7089,7 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.7:
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.4
@@ -7498,7 +7485,7 @@ snapshots:
detective-vue2@2.3.0(supports-color@10.2.2)(typescript@5.9.3):
dependencies:
'@dependents/detective-less': 5.0.3
'@vue/compiler-sfc': 3.5.39
'@vue/compiler-sfc': 3.5.38
detective-es6: 5.0.2
detective-sass: 6.0.2
detective-scss: 5.0.2
@@ -7821,10 +7808,10 @@ snapshots:
dependencies:
on-headers: 1.1.0
express@5.2.1(supports-color@10.2.2):
express@5.2.1:
dependencies:
accepts: 2.0.0
body-parser: 2.3.0(supports-color@10.2.2)
body-parser: 2.3.0
content-disposition: 1.1.0
content-type: 1.0.5
cookie: 0.7.2
@@ -7834,7 +7821,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1(supports-color@10.2.2)
finalhandler: 2.1.1
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
@@ -7845,8 +7832,8 @@ snapshots:
proxy-addr: 2.0.7
qs: 6.15.2
range-parser: 1.2.1
router: 2.2.0(supports-color@10.2.2)
send: 1.2.1(supports-color@10.2.2)
router: 2.2.0
send: 1.2.1
serve-static: 2.2.1
statuses: 2.0.2
type-is: 2.1.0
@@ -7860,7 +7847,7 @@ snapshots:
extend@3.0.2: {}
extract-zip@2.0.1(supports-color@10.2.2):
extract-zip@2.0.1:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
get-stream: 5.2.0
@@ -7966,7 +7953,7 @@ snapshots:
filter-obj@6.1.0: {}
finalhandler@2.1.1(supports-color@10.2.2):
finalhandler@2.1.1:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
@@ -8006,7 +7993,7 @@ snapshots:
dependencies:
from2: 2.3.0
follow-redirects@1.16.0(debug@4.4.3(supports-color@10.2.2)):
follow-redirects@1.16.0(debug@4.4.3):
optionalDependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -8164,7 +8151,7 @@ snapshots:
gray-matter@4.0.3:
dependencies:
js-yaml: 3.15.0
js-yaml: 3.14.2
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
@@ -8259,21 +8246,21 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
http-proxy-middleware@3.0.7(supports-color@10.2.2):
http-proxy-middleware@3.0.7:
dependencies:
'@types/http-proxy': 1.17.17
debug: 4.4.3(supports-color@10.2.2)
http-proxy: 1.18.1(debug@4.4.3(supports-color@10.2.2))
http-proxy: 1.18.1(debug@4.4.3)
is-glob: 4.0.3
is-plain-object: 5.0.0
micromatch: 4.0.8
transitivePeerDependencies:
- supports-color
http-proxy@1.18.1(debug@4.4.3(supports-color@10.2.2)):
http-proxy@1.18.1(debug@4.4.3):
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.2.2))
follow-redirects: 1.16.0(debug@4.4.3)
requires-port: 1.0.0
transitivePeerDependencies:
- debug
@@ -8287,7 +8274,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@8.0.0(supports-color@10.2.2):
https-proxy-agent@8.0.0:
dependencies:
agent-base: 8.0.0
debug: 4.4.3(supports-color@10.2.2)
@@ -8368,7 +8355,7 @@ snapshots:
ipaddr.js@2.4.0: {}
ipx@3.1.1(@netlify/blobs@10.7.9(supports-color@10.2.2)):
ipx@3.1.1(@netlify/blobs@10.7.9):
dependencies:
'@fastify/accept-negotiator': 2.0.1
citty: 0.1.6
@@ -8384,7 +8371,7 @@ snapshots:
sharp: 0.34.5
svgo: 4.0.1
ufo: 1.6.4
unstorage: 1.17.5(@netlify/blobs@10.7.9(supports-color@10.2.2))
unstorage: 1.17.5(@netlify/blobs@10.7.9)
xss: 1.0.15
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -8611,7 +8598,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.0:
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
@@ -9045,7 +9032,7 @@ snapshots:
millify@6.1.0:
dependencies:
yargs: 17.7.3
yargs: 17.7.2
mime-db@1.54.0: {}
@@ -9063,7 +9050,7 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.7
brace-expansion: 5.0.6
minimatch@5.1.9:
dependencies:
@@ -9120,15 +9107,13 @@ snapshots:
nanoid@3.3.12: {}
nanoid@3.3.15: {}
nanospinner@1.2.2:
dependencies:
picocolors: 1.1.1
negotiator@1.0.0: {}
netlify-cli@26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)(supports-color@10.2.2):
netlify-cli@26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2):
dependencies:
'@fastify/static': 9.1.3
'@netlify/ai': 0.4.1
@@ -9137,19 +9122,19 @@ snapshots:
'@netlify/build': 35.15.0(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)
'@netlify/build-info': 10.5.1
'@netlify/config': 24.6.0
'@netlify/dev': 4.18.7(rollup@4.46.2)(supports-color@10.2.2)
'@netlify/dev': 4.18.7(rollup@4.46.2)
'@netlify/dev-utils': 4.4.6
'@netlify/edge-bundler': 14.10.3
'@netlify/edge-functions': 3.0.8
'@netlify/edge-functions-bootstrap': 2.17.1
'@netlify/headers-parser': 9.0.3
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9)
'@netlify/local-functions-proxy': 2.0.3
'@netlify/redirect-parser': 15.0.4
'@netlify/zip-it-and-ship-it': 14.7.1(rollup@4.46.2)(supports-color@10.2.2)
'@octokit/rest': 22.0.1
'@opentelemetry/api': 1.9.1
'@pnpm/tabtab': 0.5.4(supports-color@10.2.2)
'@pnpm/tabtab': 0.5.4
ansi-escapes: 7.3.0
ansi-to-html: 0.7.2
ascii-table: 0.0.9
@@ -9172,7 +9157,7 @@ snapshots:
envinfo: 7.21.0
etag: 1.8.1
execa: 5.1.1
express: 5.2.1(supports-color@10.2.2)
express: 5.2.1
express-logging: 1.1.1
fastest-levenshtein: 1.0.16
fastify: 5.8.5
@@ -9182,9 +9167,9 @@ snapshots:
get-port: 5.1.1
git-repo-info: 2.1.1
gitconfiglocal: 2.1.0
http-proxy: 1.18.1(debug@4.4.3(supports-color@10.2.2))
http-proxy-middleware: 3.0.7(supports-color@10.2.2)
https-proxy-agent: 8.0.0(supports-color@10.2.2)
http-proxy: 1.18.1(debug@4.4.3)
http-proxy-middleware: 3.0.7
https-proxy-agent: 8.0.0
inquirer: 8.2.7(@types/node@24.13.2)
inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.7(@types/node@24.13.2))
is-docker: 4.0.0
@@ -9632,12 +9617,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.16:
dependencies:
nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.1: {}
@@ -9674,7 +9653,7 @@ snapshots:
precond@0.2.3: {}
prettier@3.9.4: {}
prettier@3.8.4: {}
pretty-bytes@7.1.0: {}
@@ -9959,7 +9938,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.46.2
fsevents: 2.3.3
router@2.2.0(supports-color@10.2.2):
router@2.2.0:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
depd: 2.0.0
@@ -10033,7 +10012,7 @@ snapshots:
semver@7.8.4: {}
send@1.2.1(supports-color@10.2.2):
send@1.2.1:
dependencies:
debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
@@ -10054,7 +10033,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1(supports-color@10.2.2)
send: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -10611,7 +10590,7 @@ snapshots:
unpipe@1.0.0: {}
unstorage@1.17.5(@netlify/blobs@10.7.9(supports-color@10.2.2)):
unstorage@1.17.5(@netlify/blobs@10.7.9):
dependencies:
anymatch: 3.1.3
chokidar: 5.0.0
@@ -10677,7 +10656,7 @@ snapshots:
vite@5.4.21(@types/node@24.13.2):
dependencies:
esbuild: 0.21.5
postcss: 8.5.16
postcss: 8.5.15
rollup: 4.46.2
optionalDependencies:
'@types/node': 24.13.2
@@ -10691,7 +10670,7 @@ snapshots:
optionalDependencies:
vite: 5.4.21(@types/node@24.13.2)
vitepress-plugin-llms@1.13.2:
vitepress-plugin-llms@1.13.1:
dependencies:
gray-matter: 4.0.3
markdown-it: 14.2.0
@@ -10710,10 +10689,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
vitepress-plugin-tabs@0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.39(typescript@5.9.3)):
vitepress-plugin-tabs@0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.38(typescript@5.9.3)):
dependencies:
vitepress: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3):
dependencies:
@@ -10724,7 +10703,7 @@ snapshots:
'@shikijs/transformers': 2.5.0
'@shikijs/types': 2.5.0
'@types/markdown-it': 14.1.2
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.39(typescript@5.9.3))
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.38(typescript@5.9.3))
'@vue/devtools-api': 7.7.7
'@vue/shared': 3.5.18
'@vueuse/core': 12.8.2(typescript@5.9.3)
@@ -10734,7 +10713,7 @@ snapshots:
minisearch: 7.1.2
shiki: 2.5.0
vite: 5.4.21(@types/node@24.13.2)
vue: 3.5.39(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
optionalDependencies:
postcss: 8.5.15
transitivePeerDependencies:
@@ -10764,13 +10743,13 @@ snapshots:
- typescript
- universal-cookie
vue@3.5.39(typescript@5.9.3):
vue@3.5.38(typescript@5.9.3):
dependencies:
'@vue/compiler-dom': 3.5.39
'@vue/compiler-sfc': 3.5.39
'@vue/runtime-dom': 3.5.39
'@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@5.9.3))
'@vue/shared': 3.5.39
'@vue/compiler-dom': 3.5.38
'@vue/compiler-sfc': 3.5.38
'@vue/runtime-dom': 3.5.38
'@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@5.9.3))
'@vue/shared': 3.5.38
optionalDependencies:
typescript: 5.9.3
@@ -10933,16 +10912,6 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yargs@17.7.3:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yauzl@2.10.0:
dependencies:
buffer-crc32: 0.2.13

View File

@@ -798,6 +798,7 @@ tasks:
platforms: [linux, darwin]
set: [errexit]
shopt: [globstar]
timeout: 5m
```
### Task References
@@ -914,6 +915,24 @@ tasks:
if: '[ "{{.ITEM}}" != "b" ]'
```
### Command Timeouts
Use `timeout` to limit how long a command may run. The value uses Go duration
syntax (e.g. `30s`, `5m`, `1h30m`).
```yaml
tasks:
deploy:
cmds:
- cmd: npm run build
timeout: 5m
- cmd: ./deploy.sh
timeout: 30m
```
When a command exceeds its timeout, it is terminated and the task fails with an
error, preventing commands from hanging indefinitely in a pipeline.
## Shell Options
### Set Options

View File

@@ -352,6 +352,10 @@
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
},
"timeout": {
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
"type": "string"
}
},
"additionalProperties": false,
@@ -393,6 +397,10 @@
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
},
"timeout": {
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
"type": "string"
}
},
"additionalProperties": false,
@@ -445,6 +453,10 @@
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"$ref": "#/definitions/platforms"
},
"timeout": {
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
"type": "string"
}
},
"additionalProperties": false,
@@ -475,6 +487,10 @@
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
},
"timeout": {
"description": "Maximum duration the command is allowed to run before being terminated. Supports Go duration syntax (e.g., '5m', '30s', '1h').",
"type": "string"
}
},
"additionalProperties": false,