diff --git a/CHANGELOG.md b/CHANGELOG.md index d55e76dd..04a4ecc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ website (#1198 by @pd93). - Deprecated `version: 2` schema. This will be removed in the next major release (#1197, #1198, #1199 by @pd93). +- Added a new `prompt:` prop to set a warning prompt to be shown before running + a potential dangurous task (#100, #1163 by @MaxCheetham, + [Documentation](https://taskfile.dev/usage/#warning-prompts)) ## v3.25.0 - 2023-05-22 diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 7f06a532..a9b124de 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -44,7 +44,7 @@ If `--` is given, all remaning arguments will be assigned to a special | | `--output-group-error-only` | `bool` | `false` | Swallow command output on zero exit code. | | `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. | | `-s` | `--silent` | `bool` | `false` | Disables echoing. | -| `-y` | `--yes` | `bool` | `false` | Assume "yes" as answer to all prompts. | +| `-y` | `--yes` | `bool` | `false` | Assume "yes" as answer to all prompts. | | | `--status` | `bool` | `false` | Exits with non-zero exit code if any of the given tasks is not up-to-date. | | | `--summary` | `bool` | `false` | Show summary about a task. | | `-t` | `--taskfile` | `string` | `Taskfile.yml` or `Taskfile.yaml` | | diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 81fd40e3..1d339110 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -1216,7 +1216,9 @@ tasks: Warning Prompts to prompt a user for confirmation before a task is executed. -Below is an example using `prompt` with a dangerous command, that is called between two safe commands +Below is an example using `prompt` with a dangerous command, that is called +between two safe commands: + ```yaml version: '3' @@ -1229,46 +1231,47 @@ tasks: not-dangerous: cmds: - - echo 'not dangerous command.' + - echo 'not dangerous command' another-not-dangerous: cmds: - - echo 'another not dangerous command.' + - echo 'another not dangerous command' dangerous: - prompt: This is a dangerous command.. Do you want to continue? + prompt: This is a dangerous command... Do you want to continue? cmds: - - echo 'dangerous command.' - + - echo 'dangerous command' ``` + ```bash ❯ task dangerous -task: "This is a dangerous command.. Do you want to continue?" [y/N] +task: "This is a dangerous command... Do you want to continue?" [y/N] ``` -### Prompt behaviour - -Warning prompts are called before executing a task. If a prompt is denied Task will exit with [Exit code](api_reference.md#exit-codes) 205. If approved, Task will continue as normal. +Warning prompts are called before executing a task. If a prompt is denied Task +will exit with [exit code](api_reference.md#exit-codes) 205. If approved, Task +will continue as normal. ```bash -❯ taskd --dir ./testdata/prompt example -task: [not-dangerous] echo 'not dangerous command.' -not dangerous command. -task: "This is a dangerous command.. Do you want to continue?" [y/N] +❯ task example +not dangerous command +task: "This is a dangerous command. Do you want to continue?" [y/N] y -task: [dangerous] echo 'dangerous command.' -dangerous command. -task: [another-not-dangerous] echo 'another not dangerous command.' -another not dangerous command. +dangerous command +another not dangerous command ``` -### Skipping Warning Prompts - -To skip warning prompts automatically, you can use the [-y | --yes] option when calling the task. By including this option, all warnings, will be automatically confirmed, and no prompts will be shows. - +To skip warning prompts automatically, you can use the `--yes` (alias `-y`) +option when calling the task. By including this option, all warnings, will be +automatically confirmed, and no prompts will be shown. +:::caution +Tasks with prompts always fail by default on non-terminal environments, like a +CI, where an `stdin` won't be available for the user to answer. In cases like, +use `--yes` (`-y`) to force all tasks with a prompt to run. +::: ## Silent mode diff --git a/errors/errors_task.go b/errors/errors_task.go index b3fcaeaa..24416a66 100644 --- a/errors/errors_task.go +++ b/errors/errors_task.go @@ -99,18 +99,34 @@ func (err *TaskCalledTooManyTimesError) Code() int { return CodeTaskCalledTooManyTimes } -// TaskCancelledError is returned when the user does not accept an optional prompt to continue. -type TaskCancelledError struct { +// TaskCancelledByUserError is returned when the user does not accept an optional prompt to continue. +type TaskCancelledByUserError struct { TaskName string } -func (err *TaskCancelledError) Error() string { +func (err *TaskCancelledByUserError) Error() string { return fmt.Sprintf( - `task: %q Cancelled by user`, + `task: Task "%q" cancelled by user`, err.TaskName, ) } -func (err *TaskCancelledError) Code() int { +func (err *TaskCancelledByUserError) Code() int { + return CodeTaskCancelled +} + +// TaskCancelledNoTerminalError is returned when trying to run a task with a prompt in a non-terminal environment. +type TaskCancelledNoTerminalError struct { + TaskName string +} + +func (err *TaskCancelledNoTerminalError) Error() string { + return fmt.Sprintf( + `task: Task "%q" cancelled because it has a prompt and the environment is not a terminal. Use --yes (-y) to run anyway.`, + err.TaskName, + ) +} + +func (err *TaskCancelledNoTerminalError) Code() int { return CodeTaskCancelled } diff --git a/go.mod b/go.mod index 21f2342d..a3d76b0a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/exp v0.0.0-20230212135524-a684f29349b6 golang.org/x/sync v0.2.0 + golang.org/x/term v0.3.0 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/sh/v3 v3.6.0 ) @@ -26,6 +27,5 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.3.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/internal/term/term.go b/internal/term/term.go new file mode 100644 index 00000000..122d56c3 --- /dev/null +++ b/internal/term/term.go @@ -0,0 +1,11 @@ +package term + +import ( + "os" + + "golang.org/x/term" +) + +func IsTerminal() bool { + return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) +} diff --git a/task.go b/task.go index 4e4a731d..5c564cc9 100644 --- a/task.go +++ b/task.go @@ -23,6 +23,7 @@ import ( "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/summary" "github.com/go-task/task/v3/internal/templater" + "github.com/go-task/task/v3/internal/term" "github.com/go-task/task/v3/taskfile" "github.com/sajari/fuzzy" @@ -59,6 +60,7 @@ type Executor struct { Color bool Concurrency int Interval time.Duration + AssumesTerm bool Stdin io.Reader Stdout io.Writer @@ -102,7 +104,6 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { } return &errors.TaskInternalError{TaskName: call.Task} } - } if e.Summary { @@ -148,10 +149,13 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { release := e.acquireConcurrencyLimit() defer release() - // check if the given task has a warning prompt if t.Prompt != "" && !e.AssumeYes { + if !e.AssumesTerm && !term.IsTerminal() { + return &errors.TaskCancelledNoTerminalError{TaskName: call.Task} + } e.Logger.Outf(logger.Yellow, "task: %q [y/N]\n", t.Prompt) + reader := bufio.NewReader(e.Stdin) userInput, err := reader.ReadString('\n') if err != nil { @@ -160,7 +164,7 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { userInput = strings.ToLower(strings.TrimSpace(userInput)) if !shouldPromptContinue(userInput) { - return &errors.TaskCancelledError{TaskName: call.Task} + return &errors.TaskCancelledByUserError{TaskName: call.Task} } } diff --git a/task_test.go b/task_test.go index bee3a506..e76dc1a0 100644 --- a/task_test.go +++ b/task_test.go @@ -680,9 +680,10 @@ func TestPromptInSummary(t *testing.T) { inBuff.Write([]byte(test.input)) e := task.Executor{ - Dir: dir, - Stdin: &inBuff, - Stdout: &outBuff, + Dir: dir, + Stdin: &inBuff, + Stdout: &outBuff, + AssumesTerm: true, } require.NoError(t, e.Setup()) @@ -690,9 +691,9 @@ func TestPromptInSummary(t *testing.T) { if test.wantError { require.Error(t, err) - return + } else { + require.NoError(t, err) } - require.NoError(t, err) }) } } @@ -705,9 +706,10 @@ func TestPromptWithIndirectTask(t *testing.T) { inBuff.Write([]byte("y\n")) e := task.Executor{ - Dir: dir, - Stdin: &inBuff, - Stdout: &outBuff, + Dir: dir, + Stdin: &inBuff, + Stdout: &outBuff, + AssumesTerm: true, } require.NoError(t, e.Setup()) diff --git a/testdata/prompt/Taskfile.yml b/testdata/prompt/Taskfile.yml index 947c2270..4fdd4dde 100644 --- a/testdata/prompt/Taskfile.yml +++ b/testdata/prompt/Taskfile.yml @@ -1,4 +1,5 @@ version: 3 + tasks: foo: prompt: Do you want to continue? @@ -10,7 +11,6 @@ tasks: - task: show-prompt show-prompt: - summary: some text for the summary prompt: Do you want to continue? cmds: - echo 'show-prompt'