Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A way to enforce required variables #1204

Merged
merged 13 commits into from Jun 30, 2023
9 changes: 9 additions & 0 deletions docs/docs/api_reference.md
Expand Up @@ -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).
Expand Down Expand Up @@ -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. |
Expand Down Expand Up @@ -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 |
42 changes: 42 additions & 0 deletions docs/docs/usage.md
Expand Up @@ -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
Expand Down
36 changes: 33 additions & 3 deletions docs/static/schema.json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note some reformatting crept in when I saved the file due to my VSCode settings, but the changes looked pretty sensible So I kept them

Expand Up @@ -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"
}
}
},
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
}
}
}
}
},
Expand All @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions errors/errors.go
Expand Up @@ -23,6 +23,7 @@ const (
CodeTaskNameConflict
CodeTaskCalledTooManyTimes
CodeTaskCancelled
CodeTaskMissingRequiredVars
)

// TaskError extends the standard error interface with a Code method. This code will
Expand Down
18 changes: 18 additions & 0 deletions errors/errors_task.go
Expand Up @@ -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
}
35 changes: 35 additions & 0 deletions 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
}
4 changes: 4 additions & 0 deletions task.go
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions 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),
}
}
4 changes: 4 additions & 0 deletions taskfile/task.go
Expand Up @@ -17,6 +17,7 @@ type Task struct {
Desc string
Prompt string
Summary string
Requires *Requires
Aliases []string
Sources []string
Generates []string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions variables.go
Expand Up @@ -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 {
Expand Down