Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions errors/errors_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func (err *TaskCancelledNoTerminalError) Code() int {

type MissingVar struct {
Name string
Desc string
AllowedValues []string
}
type TaskMissingRequiredVarsError struct {
Expand All @@ -159,10 +160,14 @@ type TaskMissingRequiredVarsError struct {
}

func (v MissingVar) String() string {
if len(v.AllowedValues) == 0 {
return v.Name
s := v.Name
if v.Desc != "" {
s += fmt.Sprintf(" (%s)", v.Desc)
}
return fmt.Sprintf("%s (allowed values: %v)", v.Name, v.AllowedValues)
if len(v.AllowedValues) > 0 {
s += fmt.Sprintf(" (allowed values: %v)", v.AllowedValues)
}
return s
}

func (err *TaskMissingRequiredVarsError) Error() string {
Expand Down
16 changes: 16 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,22 @@ func TestRequires(t *testing.T) {
WithVar("ENV", "dev"),
WithRunError(),
)
NewExecutorTest(t,
WithName("missing var with desc"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("missing-var-with-desc"),
WithRunError(),
)
NewExecutorTest(t,
WithName("missing var with desc - provided"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("missing-var-with-desc"),
WithVar("FOO", "bar"),
)
}

// TODO: mock fs
Expand Down
34 changes: 23 additions & 11 deletions internal/input/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ type Prompter struct {
}

// Text prompts the user for a text value
func (p *Prompter) Text(varName string) (string, error) {
m := newTextModel(varName)
func (p *Prompter) Text(varName string, desc string) (string, error) {
m := newTextModel(varName, desc)

prog := tea.NewProgram(m,
tea.WithInput(p.Stdin),
Expand All @@ -51,12 +51,12 @@ func (p *Prompter) Text(varName string) (string, error) {
}

// Select prompts the user to select from a list of options
func (p *Prompter) Select(varName string, options []string) (string, error) {
func (p *Prompter) Select(varName string, options []string, desc string) (string, error) {
if len(options) == 0 {
return "", errors.New("no options provided")
}

m := newSelectModel(varName, options)
m := newSelectModel(varName, options, desc)

prog := tea.NewProgram(m,
tea.WithInput(p.Stdin),
Expand All @@ -77,23 +77,24 @@ func (p *Prompter) Select(varName string, options []string) (string, error) {
}

// Prompt prompts for a variable value, using Select if enum is provided, Text otherwise
func (p *Prompter) Prompt(varName string, enum []string) (string, error) {
func (p *Prompter) Prompt(varName string, enum []string, desc string) (string, error) {
if len(enum) > 0 {
return p.Select(varName, enum)
return p.Select(varName, enum, desc)
}
return p.Text(varName)
return p.Text(varName, desc)
}

// textModel is the Bubble Tea model for text input
type textModel struct {
varName string
desc string
textInput textinput.Model
value string
cancelled bool
done bool
}

func newTextModel(varName string) textModel {
func newTextModel(varName string, desc string) textModel {
ti := textinput.New()
ti.Placeholder = ""
ti.CharLimit = 256
Expand All @@ -102,6 +103,7 @@ func newTextModel(varName string) textModel {

return textModel{
varName: varName,
desc: desc,
textInput: ti,
}
}
Expand Down Expand Up @@ -135,22 +137,28 @@ func (m textModel) View() tea.View {
return tea.NewView("")
}

prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", m.varName))
label := m.varName
if m.desc != "" {
label = fmt.Sprintf("%s (%s)", m.varName, m.desc)
}
prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", label))
return tea.NewView(prompt + m.textInput.View() + "\n")
}

// selectModel is the Bubble Tea model for selection
type selectModel struct {
varName string
desc string
options []string
cursor int
cancelled bool
done bool
}

func newSelectModel(varName string, options []string) selectModel {
func newSelectModel(varName string, options []string, desc string) selectModel {
return selectModel{
varName: varName,
desc: desc,
options: options,
cursor: 0,
}
Expand Down Expand Up @@ -192,7 +200,11 @@ func (m selectModel) View() tea.View {

var b strings.Builder

b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", m.varName)))
label := m.varName
if m.desc != "" {
label = fmt.Sprintf("%s (%s)", m.varName, m.desc)
}
b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", label)))
b.WriteString("\n")

for i, opt := range m.options {
Expand Down
5 changes: 3 additions & 2 deletions requires.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (e *Executor) promptDepsVars(calls []*Call) error {
e.promptedVars = ast.NewVars()

for _, v := range varsMap {
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum), v.Desc)
if err != nil {
if errors.Is(err, input.ErrCancelled) {
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
Expand Down Expand Up @@ -120,7 +120,7 @@ func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
prompter := e.newPrompter()

for _, v := range missing {
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum), v.Desc)
if err != nil {
if errors.Is(err, input.ErrCancelled) {
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
Expand Down Expand Up @@ -168,6 +168,7 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
for i, v := range missing {
missingVars[i] = errors.MissingVar{
Name: v.Name,
Desc: v.Desc,
AllowedValues: getEnumValues(v.Enum),
}
}
Expand Down
4 changes: 4 additions & 0 deletions taskfile/ast/requires.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (e *Enum) UnmarshalYAML(node *yaml.Node) error {
type VarsWithValidation struct {
Name string
Enum *Enum
Desc string
}

func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
Expand All @@ -81,6 +82,7 @@ func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
return &VarsWithValidation{
Name: v.Name,
Enum: v.Enum.DeepCopy(),
Desc: v.Desc,
}
}

Expand All @@ -101,12 +103,14 @@ func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
var vv struct {
Name string
Enum *Enum
Desc string
}
if err := node.Decode(&vv); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Name = vv.Name
v.Enum = vv.Enum
v.Desc = vv.Desc
return nil
}

Expand Down
7 changes: 7 additions & 0 deletions testdata/requires/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,10 @@ tasks:
enum:
ref: .NONEXISTENT_VAR
cmd: echo "{{.ENV}}"

missing-var-with-desc:
requires:
vars:
- name: FOO
desc: The baa required for fuzz
cmd: echo "{{.FOO}}"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
task: Task "missing-var-with-desc" cancelled because it is missing required variables: FOO (The baa required for fuzz)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
task: [missing-var-with-desc] echo "bar"
bar
33 changes: 33 additions & 0 deletions website/src/docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,39 @@ tasks:
vars: [IMAGE_NAME, IMAGE_TAG]
```

### Adding descriptions to required variables

You can add a `desc` field to any required variable. The description is shown in
the error message when the variable is missing and in the interactive prompt
when using [interactive mode](#prompting-for-missing-variables-interactively).

```yaml
version: '3'

tasks:
release:
cmds:
- ./scripts/release.sh

requires:
vars:
- name: VERSION
desc: Semantic version to release, e.g. 1.4.2
```

If `VERSION` is not set, the error will include the description:

```
task: Task "release" cancelled because it is missing required variables:
VERSION (Semantic version to release, e.g. 1.4.2)
```

And in interactive mode, the description appears in the prompt:

```
? Enter value for VERSION (Semantic version to release, e.g. 1.4.2):
```

### Ensuring required variables have allowed values

If you want to ensure that a variable is set to one of a predefined set of valid
Expand Down
1 change: 1 addition & 0 deletions website/src/docs/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ tasks:
vars:
- API_KEY
- name: ENVIRONMENT
desc: The target deployment environment
enum: [development, staging, production]
- name: LOG_LEVEL
enum: [debug, info, warn, error]
Expand Down
4 changes: 4 additions & 0 deletions website/src/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,10 @@
"type": "object",
"properties": {
"name": { "type": "string" },
"desc": {
"type": "string",
"description": "A description of the variable, shown in error messages and interactive prompts when the variable is required but missing"
},
"enum": {
"oneOf": [
{ "type": "array", "items": { "type": "string" } },
Expand Down