feat: do not log secret variables (#2514)

This commit is contained in:
Valentin Maerten
2026-06-29 14:50:08 +02:00
committed by GitHub
parent c73d53f4e9
commit 6abbbcb265
21 changed files with 557 additions and 38 deletions

View File

@@ -117,7 +117,12 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
isCommand := c.Cmd != ""
l.Outf(logger.Default, " - ")
if isCommand {
l.Outf(logger.Yellow, "%s\n", c.Cmd)
// Use the masked command so secret values are not leaked in summaries
logCmd := c.LogCmd
if logCmd == "" {
logCmd = c.Cmd
}
l.Outf(logger.Yellow, "%s\n", logCmd)
} else {
l.Outf(logger.Green, "Task: %s\n", c.Task)
}
@@ -196,6 +201,11 @@ func printTaskEnv(l *logger.Logger, t *ast.Task) {
// 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 {
// Never expose secret variables in the summary, whatever their type
if v.Secret {
return "*****"
}
// Shell command - check this first before Value
// because dynamic vars may have both Sh and an empty Value
if v.Sh != nil {

View File

@@ -0,0 +1,61 @@
package templater
import (
"github.com/go-task/task/v3/taskfile/ast"
)
// MaskSecrets replaces template placeholders with their values, masking secrets.
// This function uses the Go templater to resolve all variables ({{.VAR}}) while
// masking secret ones as "*****".
func MaskSecrets(cmdTemplate string, vars *ast.Vars) string {
return MaskSecretsWithExtra(cmdTemplate, vars, nil)
}
// MaskSecretsWithExtra is like MaskSecrets but also resolves extra variables (e.g., loop vars).
func MaskSecretsWithExtra(cmdTemplate string, vars *ast.Vars, extra map[string]any) string {
if vars == nil {
vars = ast.NewVars()
}
// Fast path: if there are no secrets to mask, resolve the template directly
// without the extra DeepCopy + masking pass.
if !hasSecrets(vars) {
cache := &Cache{Vars: vars}
result := ReplaceWithExtra(cmdTemplate, cache, extra)
if cache.Err() != nil {
return cmdTemplate
}
return result
}
// Create a copy with secret values masked, leaving the originals untouched.
maskedVars := vars.DeepCopy()
for name, v := range maskedVars.All() {
if v.Secret {
maskedVars.Set(name, ast.Var{
Value: "*****",
Secret: true,
})
}
}
cache := &Cache{Vars: maskedVars}
result := ReplaceWithExtra(cmdTemplate, cache, extra)
// If there was an error, return the original template
if cache.Err() != nil {
return cmdTemplate
}
return result
}
// hasSecrets reports whether any variable is marked as secret.
func hasSecrets(vars *ast.Vars) bool {
for _, v := range vars.All() {
if v.Secret {
return true
}
}
return false
}

View File

@@ -132,14 +132,15 @@ func ReplaceVar(v ast.Var, cache *Cache) ast.Var {
func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {
if v.Ref != "" {
return ast.Var{Value: ResolveRef(v.Ref, cache)}
return ast.Var{Value: ResolveRef(v.Ref, cache), Secret: v.Secret}
}
return ast.Var{
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Secret: v.Secret,
}
}