mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	feat: option to ensure variable is within the list of values (#1827)
This commit is contained in:
		| @@ -32,6 +32,7 @@ const ( | ||||
| 	CodeTaskCalledTooManyTimes | ||||
| 	CodeTaskCancelled | ||||
| 	CodeTaskMissingRequiredVars | ||||
| 	CodeTaskNotAllowedVars | ||||
| ) | ||||
|  | ||||
| // 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 { | ||||
| 	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 | ||||
|  | ||||
| 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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										33
									
								
								task_test.go
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								task_test.go
									
									
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										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: | ||||
|  | ||||
| | Code | Description                                                  | | ||||
| | ---- | ------------------------------------------------------------ | | ||||
| | 0    | Success                                                      | | ||||
| | 1    | An unknown error occurred                                    | | ||||
| | 100  | No Taskfile was found                                        | | ||||
| | 101  | A Taskfile already exists when trying to initialize one      | | ||||
| | 102  | The Taskfile is invalid or cannot be parsed                  | | ||||
| | 103  | A remote Taskfile could not be downloaded                    | | ||||
| | 104  | A remote Taskfile was not trusted by the user                | | ||||
| | 105  | A remote Taskfile was could not be fetched securely          | | ||||
| | 106  | No cache was found for a remote Taskfile in offline mode     | | ||||
| | 107  | No schema version was defined in the Taskfile                | | ||||
| | 200  | The specified task could not be found                        | | ||||
| | 201  | An error occurred while executing a command inside of a task | | ||||
| | 202  | The user tried to invoke a task that is internal             | | ||||
| | 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    | | ||||
| | Code | Description                                                         | | ||||
| |------|---------------------------------------------------------------------| | ||||
| | 0    | Success                                                             | | ||||
| | 1    | An unknown error occurred                                           | | ||||
| | 100  | No Taskfile was found                                               | | ||||
| | 101  | A Taskfile already exists when trying to initialize one             | | ||||
| | 102  | The Taskfile is invalid or cannot be parsed                         | | ||||
| | 103  | A remote Taskfile could not be downloaded                           | | ||||
| | 104  | A remote Taskfile was not trusted by the user                       | | ||||
| | 105  | A remote Taskfile was could not be fetched securely                 | | ||||
| | 106  | No cache was found for a remote Taskfile in offline mode            | | ||||
| | 107  | No schema version was defined in the Taskfile                       | | ||||
| | 200  | The specified task could not be found                               | | ||||
| | 201  | An error occurred while executing a command inside of a task        | | ||||
| | 202  | The user tried to invoke a task that is internal                    | | ||||
| | 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           | | ||||
| | 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). | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user