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
CodeTaskCancelled
CodeTaskMissingRequiredVars
CodeTaskNotAllowedVars
)
// 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 {
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
import (
"slices"
"github.com/go-task/task/v3/errors"
"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 notAllowedValuesVars []errors.NotAllowedVar
for _, requiredVar := range t.Requires.Vars {
if !vars.Exists(requiredVar) {
missingVars = append(missingVars, requiredVar)
value, isString := vars.Get(requiredVar.Name).Value.(string)
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
}

View File

@ -155,6 +155,39 @@ func TestVars(t *testing.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) {
const dir = "testdata/special_vars"
const subdir = "testdata/special_vars/subdir"

View File

@ -1,10 +1,15 @@
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
type Requires struct {
Vars []string
Vars []*VarsWithValidation
}
func (r *Requires) DeepCopy() *Requires {
@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
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

@ -63,7 +63,7 @@ four groups with the following ranges:
A full list of the exit codes and their descriptions can be found below:
| Code | Description |
| ---- | ------------------------------------------------------------ |
|------|---------------------------------------------------------------------|
| 0 | Success |
| 1 | An unknown error occurred |
| 100 | No Taskfile was found |
@ -81,6 +81,7 @@ A full list of the exit codes and their descriptions can be found below:
| 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 |
| 207 | A task was not executed due to a variable having an incorrect value |
These codes can also be found in the repository in
[`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]
```
### 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
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",
"type": "array",
"items": {
"type": "string"
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"enum": { "type": "array",
"items": { "type": "string" } }
},
"required": ["name", "enum"],
"additionalProperties": false
}
]
}
}
},