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:
parent
9a7e79258c
commit
a35910429c
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
23
requires.go
23
requires.go
@ -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
|
||||||
}
|
}
|
||||||
|
33
task_test.go
33
task_test.go
@ -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"
|
||||||
|
@ -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
18
testdata/requires/Taskfile.yml
vendored
Normal 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']
|
@ -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).
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user