refactor: compute masked command at compile time

Move secret masking from runtime (task.go) to compile time (variables.go).
This avoids recalculating variables on each log.

- Add MaskSecretsWithExtra for loop vars and deferred commands
- Rename CmdTemplate to LogCmd (clearer intent)
- Simplify logging in runCommand
This commit is contained in:
Valentin Maerten
2026-02-01 16:32:07 +01:00
parent e55c1a6e0f
commit 31f297bd8c
4 changed files with 42 additions and 14 deletions

View File

@@ -35,3 +35,36 @@ func MaskSecrets(cmdTemplate string, vars *ast.Vars) string {
return result
}
// 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.Len() == 0 {
// Still need to resolve extra vars even if no vars
cache := &Cache{Vars: ast.NewVars()}
result := ReplaceWithExtra(cmdTemplate, cache, extra)
if cache.Err() != nil {
return cmdTemplate
}
return result
}
// Create a cache map with secrets masked
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 cache.Err() != nil {
return cmdTemplate
}
return result
}

14
task.go
View File

@@ -349,8 +349,8 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
}
// Save template before resolving for secret masking in logs
cmd.CmdTemplate = cmd.Cmd
// Resolve template with secrets masked for logging
cmd.LogCmd = templater.MaskSecretsWithExtra(cmd.Cmd, vars, extra)
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
@@ -395,15 +395,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
}
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
// Get runtime vars for masking
varsForMasking, err := e.Compiler.FastGetVariables(t, call)
if err != nil {
return fmt.Errorf("task: failed to get variables: %w", err)
}
// Mask secret variables in the command template before logging
cmdToLog := templater.MaskSecrets(cmd.CmdTemplate, varsForMasking)
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmdToLog)
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.LogCmd)
}
if e.Dry {

View File

@@ -10,7 +10,7 @@ import (
// Cmd is a task command
type Cmd struct {
Cmd string // Resolved command (used for execution and fingerprinting)
CmdTemplate string // Original template before variable resolution (used for secret masking)
LogCmd string // Command with secrets masked (used for logging)
Task string
For *For
If string
@@ -29,7 +29,7 @@ func (c *Cmd) DeepCopy() *Cmd {
}
return &Cmd{
Cmd: c.Cmd,
CmdTemplate: c.CmdTemplate,
LogCmd: c.LogCmd,
Task: c.Task,
For: c.For.DeepCopy(),
If: c.If,

View File

@@ -228,6 +228,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
extra["KEY"] = keys[i]
}
newCmd := cmd.DeepCopy()
// Resolve template with secrets masked + loop vars for logging
newCmd.LogCmd = templater.MaskSecretsWithExtra(cmd.Cmd, cache.Vars, extra)
newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
@@ -243,7 +245,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
continue
}
newCmd := cmd.DeepCopy()
newCmd.CmdTemplate = cmd.Cmd
// Resolve template with secrets masked for logging
newCmd.LogCmd = templater.MaskSecrets(cmd.Cmd, cache.Vars)
newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
newCmd.Task = templater.Replace(cmd.Task, cache)
newCmd.If = templater.Replace(cmd.If, cache)