diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 9f8489ad..61e54aac 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -76,6 +76,7 @@ A full list of the exit codes and their descriptions can be found below: | 203 | There a multiple tasks with the same name or alias | | 204 | A task was called too many times | | 205 | A task was cancelled by the user | +| 206 | A task was not executed due to missing required variables | These codes can also be found in the repository in [`errors/errors.go`](https://github.com/go-task/task/blob/main/errors/errors.go). @@ -219,7 +220,9 @@ vars: | `sources` | `[]string` | | A list of sources to check before running this task. Relevant for `checksum` and `timestamp` methods. Can be file paths or star globs. | | `generates` | `[]string` | | A list of files meant to be generated by this task. Relevant for `timestamp` method. Can be file paths or star globs. | | `status` | `[]string` | | A list of commands to check if this task should run. The task is skipped otherwise. This overrides `method`, `sources` and `generates`. | +| `requires` | `[]string` | | A list of variables which should be set if this task is to run, if any of these variables are unset the task will error and not run. | | `preconditions` | [`[]Precondition`](#precondition) | | A list of commands to check if this task should run. If a condition is not met, the task will error. | +| `requires` | [`Requires`](#requires) | | A list of required variables which should be set if this task is to run, if any variables listed are unset the task will error and not run. | | `dir` | `string` | | The directory in which this task should run. Defaults to the current working directory. | | `vars` | [`map[string]Variable`](#variable) | | A set of variables that can be used in the task. | | `env` | [`map[string]Variable`](#variable) | | A set of environment variables that will be made available to shell commands. | @@ -322,3 +325,9 @@ tasks: ``` ::: + +#### Requires + +| Attribute | Type | Default | Description | +| --------- | ---------- | ------- | -------------------------------------------------------------------------------------------------- | +| `vars` | `[]string` | | List of variable or environment variable names that must be set if this task is to execute and run | diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 309de84c..3c1f35e7 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -876,6 +876,48 @@ tasks: - sleep 5 # long operation like installing packages ``` +### Ensuring required variables are set + +If you want to check that certain variables are set before running a task then +you can use `requires`. This is useful when might not be clear to users which +variables are needed, or if you want clear message about what is required. Also +some tasks could have dangerous side effects if run with un-set variables. + +Using `requires` you specify an array of strings in the `vars` sub-section +under `requires`, these strings are variable names which are checked prior to +running the task. If any variables are un-set the the task will error and not +run. + +Environmental variables are also checked. + +Syntax: + +```yaml +requires: + vars: [] # Array of strings +``` + +:::note + +Variables set to empty zero length strings, will pass the `requires` check. + +::: + +Example of using `requires`: + +```yaml +version: '3' + +tasks: + docker-build: + cmds: + - 'docker build . -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}}' + + # Make sure these variables are set before running + requires: + vars: [IMAGE_NAME, IMAGE_TAG] +``` + ## Variables When doing interpolation of variables, Task will look for the below. They are diff --git a/docs/static/schema.json b/docs/static/schema.json index 8a82d83f..f4e7ee07 100644 --- a/docs/static/schema.json +++ b/docs/static/schema.json @@ -184,6 +184,10 @@ "items": { "type": "string" } + }, + "requires": { + "description": "A list of variables which should be set if this task is to run, if any of these variables are unset the task will error and not run", + "$ref": "#/definitions/3/requires_obj" } } }, @@ -208,7 +212,21 @@ }, "set": { "type": "string", - "enum": ["allexport", "a", "errexit", "e", "noexec", "n", "noglob", "f", "nounset", "u", "xtrace", "x", "pipefail"] + "enum": [ + "allexport", + "a", + "errexit", + "e", + "noexec", + "n", + "noglob", + "f", + "nounset", + "u", + "xtrace", + "x", + "pipefail" + ] }, "shopt": { "type": "string", @@ -352,6 +370,18 @@ } } } + }, + "requires_obj": { + "type": "object", + "properties": { + "vars": { + "description": "List of variables that must be defined for the task to run", + "type": "array", + "items": { + "type": "string" + } + } + } } } }, @@ -375,8 +405,8 @@ "output": { "description": "Defines how the STDOUT and STDERR are printed when running tasks in parallel. The interleaved output prints lines in real time (default). The group output will print the entire output of a command once, after it finishes, so you won't have live feedback for commands that take a long time to run. The prefix output will prefix every line printed by a command with [task-name] as the prefix, but you can customize the prefix for a command with the prefix: attribute.", "anyOf": [ - {"$ref": "#/definitions/3/outputString"}, - {"$ref": "#/definitions/3/outputObject"} + { "$ref": "#/definitions/3/outputString" }, + { "$ref": "#/definitions/3/outputObject" } ] }, "method": { diff --git a/errors/errors.go b/errors/errors.go index 9c43987f..78dd72fb 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -23,6 +23,7 @@ const ( CodeTaskNameConflict CodeTaskCalledTooManyTimes CodeTaskCancelled + CodeTaskMissingRequiredVars ) // TaskError extends the standard error interface with a Code method. This code will diff --git a/errors/errors_task.go b/errors/errors_task.go index 24416a66..354b4d1e 100644 --- a/errors/errors_task.go +++ b/errors/errors_task.go @@ -130,3 +130,21 @@ func (err *TaskCancelledNoTerminalError) Error() string { func (err *TaskCancelledNoTerminalError) Code() int { return CodeTaskCancelled } + +// TaskMissingRequiredVars is returned when a task is missing required variables. +type TaskMissingRequiredVars struct { + TaskName string + MissingVars []string +} + +func (err *TaskMissingRequiredVars) Error() string { + return fmt.Sprintf( + `task: Task %q cancelled because it is missing required variables: %s`, + err.TaskName, + strings.Join(err.MissingVars, ", "), + ) +} + +func (err *TaskMissingRequiredVars) Code() int { + return CodeTaskMissingRequiredVars +} diff --git a/requires.go b/requires.go new file mode 100644 index 00000000..876796e7 --- /dev/null +++ b/requires.go @@ -0,0 +1,35 @@ +package task + +import ( + "context" + + "github.com/go-task/task/v3/errors" + "github.com/go-task/task/v3/taskfile" +) + +func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *taskfile.Task, call taskfile.Call) error { + if t.Requires == nil || len(t.Requires.Vars) == 0 { + return nil + } + + vars, err := e.Compiler.GetVariables(t, call) + if err != nil { + return err + } + + var missingVars []string + for _, requiredVar := range t.Requires.Vars { + if !vars.Exists(requiredVar) { + missingVars = append(missingVars, requiredVar) + } + } + + if len(missingVars) > 0 { + return &errors.TaskMissingRequiredVars{ + TaskName: t.Name(), + MissingVars: missingVars, + } + } + + return nil +} diff --git a/task.go b/task.go index 9bd8c8c0..c76ca85e 100644 --- a/task.go +++ b/task.go @@ -186,6 +186,10 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { return err } + if err := e.areTaskRequiredVarsSet(ctx, t, call); err != nil { + return err + } + preCondMet, err := e.areTaskPreconditionsMet(ctx, t) if err != nil { return err diff --git a/taskfile/requires.go b/taskfile/requires.go new file mode 100644 index 00000000..d7bd2904 --- /dev/null +++ b/taskfile/requires.go @@ -0,0 +1,18 @@ +package taskfile + +import "github.com/go-task/task/v3/internal/deepcopy" + +// Requires represents a set of required variables necessary for a task to run +type Requires struct { + Vars []string +} + +func (r *Requires) DeepCopy() *Requires { + if r == nil { + return nil + } + + return &Requires{ + Vars: deepcopy.Slice(r.Vars), + } +} diff --git a/taskfile/task.go b/taskfile/task.go index f82ced19..56e9bc0e 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -17,6 +17,7 @@ type Task struct { Desc string Prompt string Summary string + Requires *Requires Aliases []string Sources []string Generates []string @@ -99,6 +100,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { IgnoreError bool `yaml:"ignore_error"` Run string Platforms []*Platform + Requires *Requires } if err := node.Decode(&task); err != nil { return err @@ -135,6 +137,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error { t.IgnoreError = task.IgnoreError t.Run = task.Run t.Platforms = task.Platforms + t.Requires = task.Requires return nil } @@ -178,6 +181,7 @@ func (t *Task) DeepCopy() *Task { IncludedTaskfile: t.IncludedTaskfile.DeepCopy(), Platforms: deepcopy.Slice(t.Platforms), Location: t.Location.DeepCopy(), + Requires: t.Requires.DeepCopy(), } return c } diff --git a/variables.go b/variables.go index fec752de..5962d6e4 100644 --- a/variables.go +++ b/variables.go @@ -68,6 +68,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf IncludedTaskfileVars: origTask.IncludedTaskfileVars, Platforms: origTask.Platforms, Location: origTask.Location, + Requires: origTask.Requires, } new.Dir, err = execext.Expand(new.Dir) if err != nil {