Skip to content

Commit a359104

Browse files
authored
feat: option to ensure variable is within the list of values (#1827)
1 parent 9a7e792 commit a359104

File tree

9 files changed

+217
-24
lines changed

9 files changed

+217
-24
lines changed

errors/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
CodeTaskCalledTooManyTimes
3333
CodeTaskCancelled
3434
CodeTaskMissingRequiredVars
35+
CodeTaskNotAllowedVars
3536
)
3637

3738
// TaskError extends the standard error interface with a Code method. This code will

errors/errors_task.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,29 @@ func (err *TaskMissingRequiredVars) Error() string {
158158
func (err *TaskMissingRequiredVars) Code() int {
159159
return CodeTaskMissingRequiredVars
160160
}
161+
162+
type NotAllowedVar struct {
163+
Value string
164+
Enum []string
165+
Name string
166+
}
167+
168+
type TaskNotAllowedVars struct {
169+
TaskName string
170+
NotAllowedVars []NotAllowedVar
171+
}
172+
173+
func (err *TaskNotAllowedVars) Error() string {
174+
var builder strings.Builder
175+
176+
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
177+
for _, s := range err.NotAllowedVars {
178+
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
179+
}
180+
181+
return builder.String()
182+
}
183+
184+
func (err *TaskNotAllowedVars) Code() int {
185+
return CodeTaskNotAllowedVars
186+
}

requires.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package task
22

33
import (
4+
"slices"
5+
46
"github.com/go-task/task/v3/errors"
57
"github.com/go-task/task/v3/taskfile/ast"
68
)
@@ -16,9 +18,19 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
1618
}
1719

1820
var missingVars []string
21+
var notAllowedValuesVars []errors.NotAllowedVar
1922
for _, requiredVar := range t.Requires.Vars {
20-
if !vars.Exists(requiredVar) {
21-
missingVars = append(missingVars, requiredVar)
23+
value, isString := vars.Get(requiredVar.Name).Value.(string)
24+
if !vars.Exists(requiredVar.Name) {
25+
missingVars = append(missingVars, requiredVar.Name)
26+
} else {
27+
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
28+
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
29+
Value: value,
30+
Enum: requiredVar.Enum,
31+
Name: requiredVar.Name,
32+
})
33+
}
2234
}
2335
}
2436

@@ -29,5 +41,12 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
2941
}
3042
}
3143

44+
if len(notAllowedValuesVars) > 0 {
45+
return &errors.TaskNotAllowedVars{
46+
TaskName: t.Name(),
47+
NotAllowedVars: notAllowedValuesVars,
48+
}
49+
}
50+
3251
return nil
3352
}

task_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,39 @@ func TestVars(t *testing.T) {
155155
tt.Run(t)
156156
}
157157

158+
func TestRequires(t *testing.T) {
159+
const dir = "testdata/requires"
160+
161+
var buff bytes.Buffer
162+
e := &task.Executor{
163+
Dir: dir,
164+
Stdout: &buff,
165+
Stderr: &buff,
166+
}
167+
168+
require.NoError(t, e.Setup())
169+
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "missing-var"}), "task: Task \"missing-var\" cancelled because it is missing required variables: foo")
170+
buff.Reset()
171+
require.NoError(t, e.Setup())
172+
173+
vars := &ast.Vars{}
174+
vars.Set("foo", ast.Var{Value: "bar"})
175+
require.NoError(t, e.Run(context.Background(), &ast.Call{
176+
Task: "missing-var",
177+
Vars: vars,
178+
}))
179+
buff.Reset()
180+
181+
require.NoError(t, e.Setup())
182+
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])")
183+
buff.Reset()
184+
185+
require.NoError(t, e.Setup())
186+
vars.Set("foo", ast.Var{Value: "one"})
187+
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}))
188+
buff.Reset()
189+
}
190+
158191
func TestSpecialVars(t *testing.T) {
159192
const dir = "testdata/special_vars"
160193
const subdir = "testdata/special_vars/subdir"

taskfile/ast/requires.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package ast
22

3-
import "github.com/go-task/task/v3/internal/deepcopy"
3+
import (
4+
"gopkg.in/yaml.v3"
5+
6+
"github.com/go-task/task/v3/errors"
7+
"github.com/go-task/task/v3/internal/deepcopy"
8+
)
49

510
// Requires represents a set of required variables necessary for a task to run
611
type Requires struct {
7-
Vars []string
12+
Vars []*VarsWithValidation
813
}
914

1015
func (r *Requires) DeepCopy() *Requires {
@@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
1621
Vars: deepcopy.Slice(r.Vars),
1722
}
1823
}
24+
25+
type VarsWithValidation struct {
26+
Name string
27+
Enum []string
28+
}
29+
30+
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
31+
if v == nil {
32+
return nil
33+
}
34+
return &VarsWithValidation{
35+
Name: v.Name,
36+
Enum: v.Enum,
37+
}
38+
}
39+
40+
// UnmarshalYAML implements yaml.Unmarshaler interface.
41+
func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
42+
switch node.Kind {
43+
44+
case yaml.ScalarNode:
45+
var cmd string
46+
if err := node.Decode(&cmd); err != nil {
47+
return errors.NewTaskfileDecodeError(err, node)
48+
}
49+
v.Name = cmd
50+
v.Enum = nil
51+
return nil
52+
53+
case yaml.MappingNode:
54+
var vv struct {
55+
Name string
56+
Enum []string
57+
}
58+
if err := node.Decode(&vv); err != nil {
59+
return errors.NewTaskfileDecodeError(err, node)
60+
}
61+
v.Name = vv.Name
62+
v.Enum = vv.Enum
63+
return nil
64+
}
65+
66+
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires")
67+
}

testdata/requires/Taskfile.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: '3'
2+
3+
tasks:
4+
default:
5+
- task: missing-var
6+
7+
missing-var:
8+
requires:
9+
vars:
10+
- foo
11+
cmd: echo "{{.foo}}"
12+
13+
14+
validation-var:
15+
requires:
16+
vars:
17+
- name: foo
18+
enum: ['one', 'two']

website/docs/reference/cli.mdx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,26 @@ four groups with the following ranges:
6262

6363
A full list of the exit codes and their descriptions can be found below:
6464

65-
| Code | Description |
66-
| ---- | ------------------------------------------------------------ |
67-
| 0 | Success |
68-
| 1 | An unknown error occurred |
69-
| 100 | No Taskfile was found |
70-
| 101 | A Taskfile already exists when trying to initialize one |
71-
| 102 | The Taskfile is invalid or cannot be parsed |
72-
| 103 | A remote Taskfile could not be downloaded |
73-
| 104 | A remote Taskfile was not trusted by the user |
74-
| 105 | A remote Taskfile was could not be fetched securely |
75-
| 106 | No cache was found for a remote Taskfile in offline mode |
76-
| 107 | No schema version was defined in the Taskfile |
77-
| 200 | The specified task could not be found |
78-
| 201 | An error occurred while executing a command inside of a task |
79-
| 202 | The user tried to invoke a task that is internal |
80-
| 203 | There a multiple tasks with the same name or alias |
81-
| 204 | A task was called too many times |
82-
| 205 | A task was cancelled by the user |
83-
| 206 | A task was not executed due to missing required variables |
65+
| Code | Description |
66+
|------|---------------------------------------------------------------------|
67+
| 0 | Success |
68+
| 1 | An unknown error occurred |
69+
| 100 | No Taskfile was found |
70+
| 101 | A Taskfile already exists when trying to initialize one |
71+
| 102 | The Taskfile is invalid or cannot be parsed |
72+
| 103 | A remote Taskfile could not be downloaded |
73+
| 104 | A remote Taskfile was not trusted by the user |
74+
| 105 | A remote Taskfile was could not be fetched securely |
75+
| 106 | No cache was found for a remote Taskfile in offline mode |
76+
| 107 | No schema version was defined in the Taskfile |
77+
| 200 | The specified task could not be found |
78+
| 201 | An error occurred while executing a command inside of a task |
79+
| 202 | The user tried to invoke a task that is internal |
80+
| 203 | There a multiple tasks with the same name or alias |
81+
| 204 | A task was called too many times |
82+
| 205 | A task was cancelled by the user |
83+
| 206 | A task was not executed due to missing required variables |
84+
| 207 | A task was not executed due to a variable having an incorrect value |
8485

8586
These codes can also be found in the repository in
8687
[`errors/errors.go`](https://github.com/go-task/task/blob/main/errors/errors.go).

website/docs/usage.mdx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,40 @@ tasks:
10601060
vars: [IMAGE_NAME, IMAGE_TAG]
10611061
```
10621062

1063+
### Ensuring required variables have allowed values
1064+
1065+
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.
1066+
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.
1067+
1068+
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.
1069+
If the variable does not match any of these values, the task will raise an error and stop execution.
1070+
1071+
This check applies both to user-defined variables and environment variables.
1072+
1073+
Example of using `requires`:
1074+
1075+
```yaml
1076+
version: '3'
1077+
1078+
tasks:
1079+
deploy:
1080+
cmds:
1081+
- echo "deploying to {{.ENV}}"
1082+
1083+
requires:
1084+
vars:
1085+
- name: ENV
1086+
enum: [dev, beta, prod]
1087+
```
1088+
1089+
If `ENV` is not one of 'dev', 'beta' or 'prod' an error will be raised.
1090+
1091+
:::note
1092+
1093+
This is supported only for string variables.
1094+
1095+
:::
1096+
10631097
## Variables
10641098

10651099
Task allows you to set variables using the `vars` keyword. The following

website/static/schema.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,19 @@
558558
"description": "List of variables that must be defined for the task to run",
559559
"type": "array",
560560
"items": {
561-
"type": "string"
561+
"oneOf": [
562+
{ "type": "string" },
563+
{
564+
"type": "object",
565+
"properties": {
566+
"name": { "type": "string" },
567+
"enum": { "type": "array",
568+
"items": { "type": "string" } }
569+
},
570+
"required": ["name", "enum"],
571+
"additionalProperties": false
572+
}
573+
]
562574
}
563575
}
564576
},

0 commit comments

Comments
 (0)