mirror of
https://github.com/go-task/task.git
synced 2026-06-30 08:04:28 +00:00
feat: do not log secret variables (#2514)
This commit is contained in:
@@ -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 {
|
||||
|
||||
61
internal/templater/secrets.go
Normal file
61
internal/templater/secrets.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user