Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- The `--summary` flag now displays `vars:` (both global and task-level),
`env:`, and `requires:` sections. Dynamic variables show their shell command
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
@vmaerten).

## v3.45.5 - 2025-11-11

Expand Down
5 changes: 3 additions & 2 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
newVar := templater.ReplaceVar(v, cache)
// If the variable should not be evaluated, but is nil, set it to an empty string
// This stops empty interface errors when using the templater to replace values later
// Preserve the Sh field so it can be displayed in summary
if !evaluateShVars && newVar.Value == nil {
result.Set(k, ast.Var{Value: ""})
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
return nil
}
// If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars {
result.Set(k, ast.Var{Value: newVar.Value})
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
return nil
}
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
Expand Down
24 changes: 24 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,30 @@ func TestAlias(t *testing.T) {
)
}

func TestSummaryWithVarsAndRequires(t *testing.T) {
t.Parallel()

// Test basic case from prompt.md - vars and requires
NewExecutorTest(t,
WithName("vars-and-requires"),
WithExecutorOptions(
task.WithDir("testdata/summary-vars-requires"),
task.WithSummary(true),
),
WithTask("mytask"),
)

// Test with shell variables
NewExecutorTest(t,
WithName("shell-vars"),
WithExecutorOptions(
task.WithDir("testdata/summary-vars-requires"),
task.WithSummary(true),
),
WithTask("with-sh-var"),
)
}

func TestLabel(t *testing.T) {
t.Parallel()

Expand Down
170 changes: 170 additions & 0 deletions internal/summary/summary.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package summary

import (
"fmt"
"os"
"strings"

"github.com/go-task/task/v3/internal/logger"
Expand Down Expand Up @@ -29,6 +31,9 @@ func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
func PrintTask(l *logger.Logger, t *ast.Task) {
printTaskName(l, t)
printTaskDescribingText(t, l)
printTaskVars(l, t)
printTaskEnv(l, t)
printTaskRequires(l, t)
printTaskDependencies(l, t)
printTaskAliases(l, t)
printTaskCommands(l, t)
Expand Down Expand Up @@ -118,3 +123,168 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
}
}
}

func printTaskVars(l *logger.Logger, t *ast.Task) {
if t.Vars == nil || t.Vars.Len() == 0 {
return
}

osEnvVars := getEnvVarNames()

taskfileEnvVars := make(map[string]bool)
if t.Env != nil {
for key := range t.Env.All() {
taskfileEnvVars[key] = true
}
}

hasNonEnvVars := false
for key := range t.Vars.All() {
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
hasNonEnvVars = true
break
}
}

if !hasNonEnvVars {
return
}

l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "vars:\n")

for key, value := range t.Vars.All() {
// Only display variables that are not from OS environment or Taskfile env
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
formattedValue := formatVarValue(value)
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
}
}
}

func printTaskEnv(l *logger.Logger, t *ast.Task) {
if t.Env == nil || t.Env.Len() == 0 {
return
}

envVars := getEnvVarNames()

hasNonEnvVars := false
for key := range t.Env.All() {
if !isEnvVar(key, envVars) {
hasNonEnvVars = true
break
}
}

if !hasNonEnvVars {
return
}

l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "env:\n")

for key, value := range t.Env.All() {
// Only display variables that are not from OS environment
if !isEnvVar(key, envVars) {
formattedValue := formatVarValue(value)
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
}
}
}

// formatVarValue formats a variable value based on its type.
// Handles static values, shell commands (sh:), references (ref:), and maps.
func formatVarValue(v ast.Var) string {
// Shell command - check this first before Value
// because dynamic vars may have both Sh and an empty Value
if v.Sh != nil {
return fmt.Sprintf("sh: %s", *v.Sh)
}

// Reference
if v.Ref != "" {
return fmt.Sprintf("ref: %s", v.Ref)
}

// Static value
if v.Value != nil {
// Check if it's a map or complex type
if m, ok := v.Value.(map[string]any); ok {
return formatMap(m, 4)
}
// Simple string value
return fmt.Sprintf(`"%v"`, v.Value)
}

return `""`
}

// formatMap formats a map value with proper indentation for YAML.
func formatMap(m map[string]any, indent int) string {
if len(m) == 0 {
return "{}"
}

var result strings.Builder
result.WriteString("\n")
spaces := strings.Repeat(" ", indent)

for k, v := range m {
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v))
}

return result.String()
}

func printTaskRequires(l *logger.Logger, t *ast.Task) {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return
}

l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "requires:\n")
l.Outf(logger.Default, " vars:\n")

for _, v := range t.Requires.Vars {
// If the variable has enum constraints, format accordingly
if len(v.Enum) > 0 {
l.Outf(logger.Yellow, " - %s:\n", v.Name)
l.Outf(logger.Yellow, " enum:\n")
for _, enumValue := range v.Enum {
l.Outf(logger.Yellow, " - %s\n", enumValue)
}
} else {
// Simple required variable
l.Outf(logger.Yellow, " - %s\n", v.Name)
}
}
}

func getEnvVarNames() map[string]bool {
envMap := make(map[string]bool)
for _, e := range os.Environ() {
parts := strings.SplitN(e, "=", 2)
if len(parts) > 0 {
envMap[parts[0]] = true
}
}
return envMap
}

// isEnvVar checks if a variable is from OS environment or auto-generated by Task.
func isEnvVar(key string, envVars map[string]bool) bool {
// Filter out auto-generated Task variables
if strings.HasPrefix(key, "TASK_") ||
strings.HasPrefix(key, "CLI_") ||
strings.HasPrefix(key, "ROOT_") ||
key == "TASK" ||
key == "TASKFILE" ||
key == "TASKFILE_DIR" ||
key == "USER_WORKING_DIR" ||
key == "ALIAS" ||
key == "MATCH" {
return true
}
return envVars[key]
}
21 changes: 21 additions & 0 deletions testdata/summary-vars-requires/Taskfile-with-env.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: 3

vars:
GLOBAL_VAR: "I am a global var"

env:
GLOBAL_ENV: "I am a global env"

tasks:
test-env:
desc: Task with vars and env
vars:
LOCAL_VAR: "I am a local var"
env:
LOCAL_ENV: "I am a local env"
DATABASE_URL: "postgres://localhost/mydb"
requires:
vars:
- API_KEY
cmds:
- echo "Testing env vars"
16 changes: 16 additions & 0 deletions testdata/summary-vars-requires/Taskfile-with-globals.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 3

vars:
GLOBAL_VAR: "I am global"
ANOTHER_GLOBAL: "Also global"

tasks:
test-globals:
desc: Task with global and local vars
vars:
LOCAL_VAR: "I am local"
requires:
vars:
- REQUIRED_VAR
cmds:
- echo {{ .GLOBAL_VAR }} {{ .LOCAL_VAR }}
36 changes: 36 additions & 0 deletions testdata/summary-vars-requires/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: 3

tasks:
mytask:
desc: It does things
summary: |
It does things and has optional and required variables.
vars:
OPTIONAL_VAR: "hello"
requires:
vars:
- REQUIRED_VAR
cmds:
- cmd: echo {{ .OPTIONAL_VAR }} {{ .REQUIRED_VAR }}

with-sh-var:
desc: Task with shell variable
vars:
DYNAMIC_VAR:
sh: echo "world"
STATIC_VAR: "hello"
cmds:
- echo {{ .DYNAMIC_VAR }}

no-vars:
desc: Task without variables
cmds:
- echo "no vars here"

only-requires:
desc: Task with only requires
requires:
vars:
- NEEDED_VAR
cmds:
- echo {{ .NEEDED_VAR }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
task: with-sh-var

Task with shell variable

vars:
DYNAMIC_VAR: sh: echo "world"
STATIC_VAR: "hello"

commands:
- echo
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
task: mytask

It does things and has optional and required variables.

vars:
OPTIONAL_VAR: "hello"

requires:
vars:
- REQUIRED_VAR

commands:
- echo hello
Loading