1
0
mirror of https://github.com/go-task/task.git synced 2024-12-04 10:24:45 +02:00

feat: option to ensure variable is within the list of values (#1827)

This commit is contained in:
Valentin Maerten 2024-10-18 18:16:57 +02:00 committed by GitHub
parent 9a7e79258c
commit a35910429c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 217 additions and 24 deletions

View File

@ -32,6 +32,7 @@ const (
CodeTaskCalledTooManyTimes CodeTaskCalledTooManyTimes
CodeTaskCancelled CodeTaskCancelled
CodeTaskMissingRequiredVars CodeTaskMissingRequiredVars
CodeTaskNotAllowedVars
) )
// TaskError extends the standard error interface with a Code method. This code will // TaskError extends the standard error interface with a Code method. This code will

View File

@ -158,3 +158,29 @@ func (err *TaskMissingRequiredVars) Error() string {
func (err *TaskMissingRequiredVars) Code() int { func (err *TaskMissingRequiredVars) Code() int {
return CodeTaskMissingRequiredVars return CodeTaskMissingRequiredVars
} }
type NotAllowedVar struct {
Value string
Enum []string
Name string
}
type TaskNotAllowedVars struct {
TaskName string
NotAllowedVars []NotAllowedVar
}
func (err *TaskNotAllowedVars) Error() string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
for _, s := range err.NotAllowedVars {
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
}
return builder.String()
}
func (err *TaskNotAllowedVars) Code() int {
return CodeTaskNotAllowedVars
}

View File

@ -1,6 +1,8 @@
package task package task
import ( import (
"slices"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@ -16,9 +18,19 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
} }
var missingVars []string var missingVars []string
var notAllowedValuesVars []errors.NotAllowedVar
for _, requiredVar := range t.Requires.Vars { for _, requiredVar := range t.Requires.Vars {
if !vars.Exists(requiredVar) { value, isString := vars.Get(requiredVar.Name).Value.(string)
missingVars = append(missingVars, requiredVar) if !vars.Exists(requiredVar.Name) {
missingVars = append(missingVars, requiredVar.Name)
} else {
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Name: requiredVar.Name,
})
}
} }
} }
@ -29,5 +41,12 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
} }
} }
if len(notAllowedValuesVars) > 0 {
return &errors.TaskNotAllowedVars{
TaskName: t.Name(),
NotAllowedVars: notAllowedValuesVars,
}
}
return nil return nil
} }

View File

@ -155,6 +155,39 @@ func TestVars(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestRequires(t *testing.T) {
const dir = "testdata/requires"
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
require.NoError(t, e.Setup())
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "missing-var"}), "task: Task \"missing-var\" cancelled because it is missing required variables: foo")
buff.Reset()
require.NoError(t, e.Setup())
vars := &ast.Vars{}
vars.Set("foo", ast.Var{Value: "bar"})
require.NoError(t, e.Run(context.Background(), &ast.Call{
Task: "missing-var",
Vars: vars,
}))
buff.Reset()
require.NoError(t, e.Setup())
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}), "task: Task \"validation-var\" cancelled because it is missing required variables:\n - foo has an invalid value : 'bar' (allowed values : [one two])")
buff.Reset()
require.NoError(t, e.Setup())
vars.Set("foo", ast.Var{Value: "one"})
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}))
buff.Reset()
}
func TestSpecialVars(t *testing.T) { func TestSpecialVars(t *testing.T) {
const dir = "testdata/special_vars" const dir = "testdata/special_vars"
const subdir = "testdata/special_vars/subdir" const subdir = "testdata/special_vars/subdir"

View File

@ -1,10 +1,15 @@
package ast package ast
import "github.com/go-task/task/v3/internal/deepcopy" import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
// Requires represents a set of required variables necessary for a task to run // Requires represents a set of required variables necessary for a task to run
type Requires struct { type Requires struct {
Vars []string Vars []*VarsWithValidation
} }
func (r *Requires) DeepCopy() *Requires { func (r *Requires) DeepCopy() *Requires {
@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
Vars: deepcopy.Slice(r.Vars), Vars: deepcopy.Slice(r.Vars),
} }
} }
type VarsWithValidation struct {
Name string
Enum []string
}
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
if v == nil {
return nil
}
return &VarsWithValidation{
Name: v.Name,
Enum: v.Enum,
}
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Name = cmd
v.Enum = nil
return nil
case yaml.MappingNode:
var vv struct {
Name string
Enum []string
}
if err := node.Decode(&vv); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Name = vv.Name
v.Enum = vv.Enum
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires")
}

18
testdata/requires/Taskfile.yml vendored Normal file
View File

@ -0,0 +1,18 @@
version: '3'
tasks:
default:
- task: missing-var
missing-var:
requires:
vars:
- foo
cmd: echo "{{.foo}}"
validation-var:
requires:
vars:
- name: foo
enum: ['one', 'two']

View File

@ -62,25 +62,26 @@ four groups with the following ranges:
A full list of the exit codes and their descriptions can be found below: A full list of the exit codes and their descriptions can be found below:
| Code | Description | | Code | Description |
| ---- | ------------------------------------------------------------ | |------|---------------------------------------------------------------------|
| 0 | Success | | 0 | Success |
| 1 | An unknown error occurred | | 1 | An unknown error occurred |
| 100 | No Taskfile was found | | 100 | No Taskfile was found |
| 101 | A Taskfile already exists when trying to initialize one | | 101 | A Taskfile already exists when trying to initialize one |
| 102 | The Taskfile is invalid or cannot be parsed | | 102 | The Taskfile is invalid or cannot be parsed |
| 103 | A remote Taskfile could not be downloaded | | 103 | A remote Taskfile could not be downloaded |
| 104 | A remote Taskfile was not trusted by the user | | 104 | A remote Taskfile was not trusted by the user |
| 105 | A remote Taskfile was could not be fetched securely | | 105 | A remote Taskfile was could not be fetched securely |
| 106 | No cache was found for a remote Taskfile in offline mode | | 106 | No cache was found for a remote Taskfile in offline mode |
| 107 | No schema version was defined in the Taskfile | | 107 | No schema version was defined in the Taskfile |
| 200 | The specified task could not be found | | 200 | The specified task could not be found |
| 201 | An error occurred while executing a command inside of a task | | 201 | An error occurred while executing a command inside of a task |
| 202 | The user tried to invoke a task that is internal | | 202 | The user tried to invoke a task that is internal |
| 203 | There a multiple tasks with the same name or alias | | 203 | There a multiple tasks with the same name or alias |
| 204 | A task was called too many times | | 204 | A task was called too many times |
| 205 | A task was cancelled by the user | | 205 | A task was cancelled by the user |
| 206 | A task was not executed due to missing required variables | | 206 | A task was not executed due to missing required variables |
| 207 | A task was not executed due to a variable having an incorrect value |
These codes can also be found in the repository in These codes can also be found in the repository in
[`errors/errors.go`](https://github.com/go-task/task/blob/main/errors/errors.go). [`errors/errors.go`](https://github.com/go-task/task/blob/main/errors/errors.go).

View File

@ -1060,6 +1060,40 @@ tasks:
vars: [IMAGE_NAME, IMAGE_TAG] vars: [IMAGE_NAME, IMAGE_TAG]
``` ```
### Ensuring required variables have allowed values
If you want to ensure that a variable is set to one of a predefined set of valid values before executing a task, you can use requires.
This is particularly useful when there are strict requirements for what values a variable can take, and you want to provide clear feedback to the user when an invalid value is detected.
To use `requires`, you specify an array of allowed values in the vars sub-section under requires. Task will check if the variable is set to one of the allowed values.
If the variable does not match any of these values, the task will raise an error and stop execution.
This check applies both to user-defined variables and environment variables.
Example of using `requires`:
```yaml
version: '3'
tasks:
deploy:
cmds:
- echo "deploying to {{.ENV}}"
requires:
vars:
- name: ENV
enum: [dev, beta, prod]
```
If `ENV` is not one of 'dev', 'beta' or 'prod' an error will be raised.
:::note
This is supported only for string variables.
:::
## Variables ## Variables
Task allows you to set variables using the `vars` keyword. The following Task allows you to set variables using the `vars` keyword. The following

View File

@ -558,7 +558,19 @@
"description": "List of variables that must be defined for the task to run", "description": "List of variables that must be defined for the task to run",
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"enum": { "type": "array",
"items": { "type": "string" } }
},
"required": ["name", "enum"],
"additionalProperties": false
}
]
} }
} }
}, },