mirror of
https://github.com/go-task/task.git
synced 2026-06-22 04:05:53 +00:00
Compare commits
1 Commits
task-secre
...
fix/failfa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0431e4bf27 |
10
compiler.go
10
compiler.go
@@ -51,7 +51,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for k, v := range specialVars {
|
for k, v := range specialVars {
|
||||||
result.Set(k, ast.Var{Value: v, Secret: false})
|
result.Set(k, ast.Var{Value: v})
|
||||||
}
|
}
|
||||||
|
|
||||||
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
||||||
@@ -63,12 +63,12 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
|||||||
// This stops empty interface errors when using the templater to replace values later
|
// This stops empty interface errors when using the templater to replace values later
|
||||||
// Preserve the Sh field so it can be displayed in summary
|
// Preserve the Sh field so it can be displayed in summary
|
||||||
if !evaluateShVars && newVar.Value == nil {
|
if !evaluateShVars && newVar.Value == nil {
|
||||||
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh, Secret: v.Secret})
|
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// If the variable should not be evaluated and it is set, we can set it and return
|
// If the variable should not be evaluated and it is set, we can set it and return
|
||||||
if !evaluateShVars {
|
if !evaluateShVars {
|
||||||
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh, Secret: v.Secret})
|
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
|
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
|
||||||
@@ -77,7 +77,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
|||||||
}
|
}
|
||||||
// If the variable is already set, we can set it and return
|
// If the variable is already set, we can set it and return
|
||||||
if newVar.Value != nil || newVar.Sh == nil {
|
if newVar.Value != nil || newVar.Sh == nil {
|
||||||
result.Set(k, ast.Var{Value: newVar.Value, Secret: v.Secret})
|
result.Set(k, ast.Var{Value: newVar.Value})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// If the variable is dynamic, we need to resolve it first
|
// If the variable is dynamic, we need to resolve it first
|
||||||
@@ -85,7 +85,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
result.Set(k, ast.Var{Value: static, Secret: v.Secret})
|
result.Set(k, ast.Var{Value: static})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
executor_test.go
109
executor_test.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sebdah/goldie/v2"
|
"github.com/sebdah/goldie/v2"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -30,13 +31,15 @@ type (
|
|||||||
// gen:fixtures`.
|
// gen:fixtures`.
|
||||||
ExecutorTest struct {
|
ExecutorTest struct {
|
||||||
TaskTest
|
TaskTest
|
||||||
task string
|
task string
|
||||||
vars map[string]any
|
vars map[string]any
|
||||||
input string
|
input string
|
||||||
executorOpts []task.ExecutorOption
|
executorOpts []task.ExecutorOption
|
||||||
wantSetupError bool
|
wantSetupError bool
|
||||||
wantRunError bool
|
wantRunError bool
|
||||||
wantStatusError bool
|
wantStatusError bool
|
||||||
|
skipOutputFixture bool
|
||||||
|
maxDuration time.Duration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -113,6 +116,32 @@ func (opt *statusErrorTestOption) applyToExecutorTest(t *ExecutorTest) {
|
|||||||
t.wantStatusError = true
|
t.wantStatusError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithoutOutputFixture disables the stdout/stderr golden fixture comparison.
|
||||||
|
// Use for tasks with non-deterministic output by design (e.g. parallel deps
|
||||||
|
// cancelled mid-execution) where only the run error or timing matters.
|
||||||
|
func WithoutOutputFixture() ExecutorTestOption {
|
||||||
|
return &withoutOutputFixtureTestOption{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withoutOutputFixtureTestOption struct{}
|
||||||
|
|
||||||
|
func (opt *withoutOutputFixtureTestOption) applyToExecutorTest(t *ExecutorTest) {
|
||||||
|
t.skipOutputFixture = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxDuration asserts the run phase completes within d. Use to verify
|
||||||
|
// that failfast/cancellation kicks in promptly instead of waiting for deps
|
||||||
|
// to finish naturally.
|
||||||
|
func WithMaxDuration(d time.Duration) ExecutorTestOption {
|
||||||
|
return &maxDurationTestOption{d: d}
|
||||||
|
}
|
||||||
|
|
||||||
|
type maxDurationTestOption struct{ d time.Duration }
|
||||||
|
|
||||||
|
func (opt *maxDurationTestOption) applyToExecutorTest(t *ExecutorTest) {
|
||||||
|
t.maxDuration = opt.d
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
// writeFixtureErrRun is a wrapper for writing the output of an error during the
|
// writeFixtureErrRun is a wrapper for writing the output of an error during the
|
||||||
@@ -172,7 +201,9 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
|||||||
if err := e.Setup(); tt.wantSetupError {
|
if err := e.Setup(); tt.wantSetupError {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
tt.writeFixtureErrSetup(t, g, err)
|
tt.writeFixtureErrSetup(t, g, err)
|
||||||
tt.writeFixtureBuffer(t, g, buffer.buf)
|
if !tt.skipOutputFixture {
|
||||||
|
tt.writeFixtureBuffer(t, g, buffer.buf)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -190,10 +221,18 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
|||||||
|
|
||||||
// Run the task and check for errors
|
// Run the task and check for errors
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
if err := e.Run(ctx, call); tt.wantRunError {
|
start := time.Now()
|
||||||
|
err := e.Run(ctx, call)
|
||||||
|
if tt.maxDuration > 0 {
|
||||||
|
require.Less(t, time.Since(start), tt.maxDuration,
|
||||||
|
"task took too long — failfast/cancellation likely did not trigger")
|
||||||
|
}
|
||||||
|
if tt.wantRunError {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
tt.writeFixtureErrRun(t, g, err)
|
tt.writeFixtureErrRun(t, g, err)
|
||||||
tt.writeFixtureBuffer(t, g, buffer.buf)
|
if !tt.skipOutputFixture {
|
||||||
|
tt.writeFixtureBuffer(t, g, buffer.buf)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -206,7 +245,9 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tt.writeFixtureBuffer(t, g, buffer.buf)
|
if !tt.skipOutputFixture {
|
||||||
|
tt.writeFixtureBuffer(t, g, buffer.buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the test (with a name if it has one)
|
// Run the test (with a name if it has one)
|
||||||
@@ -283,45 +324,6 @@ func TestVars(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSecretVars(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("secret vars are masked in logs"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
),
|
|
||||||
WithTask("test-secret-masking"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("multiple secrets masked"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
),
|
|
||||||
WithTask("test-multiple-secrets"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("mixed secret and public vars"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
),
|
|
||||||
WithTask("test-mixed"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("deferred command with secrets"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
),
|
|
||||||
WithTask("test-deferred-secret"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("env secret limitation"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
),
|
|
||||||
WithTask("test-env-secret-limitation"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequires(t *testing.T) {
|
func TestRequires(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
NewExecutorTest(t,
|
NewExecutorTest(t,
|
||||||
@@ -1169,12 +1171,14 @@ func TestFailfast(t *testing.T) {
|
|||||||
|
|
||||||
NewExecutorTest(t,
|
NewExecutorTest(t,
|
||||||
WithName("default"),
|
WithName("default"),
|
||||||
|
WithVar("SLEEP", "sleep 5 && "),
|
||||||
WithExecutorOptions(
|
WithExecutorOptions(
|
||||||
task.WithDir("testdata/failfast/default"),
|
task.WithDir("testdata/failfast/default"),
|
||||||
task.WithSilent(true),
|
task.WithSilent(true),
|
||||||
task.WithFailfast(true),
|
task.WithFailfast(true),
|
||||||
),
|
),
|
||||||
WithPostProcessFn(PPSortedLines),
|
WithoutOutputFixture(),
|
||||||
|
WithMaxDuration(4*time.Second),
|
||||||
WithRunError(),
|
WithRunError(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1188,7 +1192,8 @@ func TestFailfast(t *testing.T) {
|
|||||||
task.WithDir("testdata/failfast/task"),
|
task.WithDir("testdata/failfast/task"),
|
||||||
task.WithSilent(true),
|
task.WithSilent(true),
|
||||||
),
|
),
|
||||||
WithPostProcessFn(PPSortedLines),
|
WithoutOutputFixture(),
|
||||||
|
WithMaxDuration(4*time.Second),
|
||||||
WithRunError(),
|
WithRunError(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
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 {
|
|
||||||
if vars == nil || vars.Len() == 0 {
|
|
||||||
return cmdTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a cache map with secrets masked
|
|
||||||
maskedVars := vars.DeepCopy()
|
|
||||||
for name, v := range maskedVars.All() {
|
|
||||||
if v.Secret {
|
|
||||||
// Replace secret value with mask
|
|
||||||
maskedVars.Set(name, ast.Var{
|
|
||||||
Value: "*****",
|
|
||||||
Secret: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the templater to resolve the template with masked secrets
|
|
||||||
cache := &Cache{Vars: maskedVars}
|
|
||||||
result := Replace(cmdTemplate, cache)
|
|
||||||
|
|
||||||
// If there was an error, return the original template
|
|
||||||
if cache.Err() != nil {
|
|
||||||
return cmdTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -121,15 +121,14 @@ func ReplaceVar(v ast.Var, cache *Cache) ast.Var {
|
|||||||
|
|
||||||
func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {
|
func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {
|
||||||
if v.Ref != "" {
|
if v.Ref != "" {
|
||||||
return ast.Var{Value: ResolveRef(v.Ref, cache), Secret: v.Secret}
|
return ast.Var{Value: ResolveRef(v.Ref, cache)}
|
||||||
}
|
}
|
||||||
return ast.Var{
|
return ast.Var{
|
||||||
Value: ReplaceWithExtra(v.Value, cache, extra),
|
Value: ReplaceWithExtra(v.Value, cache, extra),
|
||||||
Sh: ReplaceWithExtra(v.Sh, cache, extra),
|
Sh: ReplaceWithExtra(v.Sh, cache, extra),
|
||||||
Live: v.Live,
|
Live: v.Live,
|
||||||
Ref: v.Ref,
|
Ref: v.Ref,
|
||||||
Dir: v.Dir,
|
Dir: v.Dir,
|
||||||
Secret: v.Secret,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
task.go
4
task.go
@@ -349,8 +349,6 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
|
|||||||
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
|
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve template with secrets masked for logging
|
|
||||||
cmd.LogCmd = templater.MaskSecretsWithExtra(cmd.Cmd, vars, extra)
|
|
||||||
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
|
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
|
||||||
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
|
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
|
||||||
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
|
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
|
||||||
@@ -395,7 +393,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) {
|
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
|
||||||
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.LogCmd)
|
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Dry {
|
if e.Dry {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import (
|
|||||||
|
|
||||||
// Cmd is a task command
|
// Cmd is a task command
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
Cmd string // Resolved command (used for execution and fingerprinting)
|
Cmd string
|
||||||
LogCmd string // Command with secrets masked (used for logging)
|
|
||||||
Task string
|
Task string
|
||||||
For *For
|
For *For
|
||||||
If string
|
If string
|
||||||
@@ -29,7 +28,6 @@ func (c *Cmd) DeepCopy() *Cmd {
|
|||||||
}
|
}
|
||||||
return &Cmd{
|
return &Cmd{
|
||||||
Cmd: c.Cmd,
|
Cmd: c.Cmd,
|
||||||
LogCmd: c.LogCmd,
|
|
||||||
Task: c.Task,
|
Task: c.Task,
|
||||||
For: c.For.DeepCopy(),
|
For: c.For.DeepCopy(),
|
||||||
If: c.If,
|
If: c.If,
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
|
|
||||||
// Var represents either a static or dynamic variable.
|
// Var represents either a static or dynamic variable.
|
||||||
type Var struct {
|
type Var struct {
|
||||||
Value any
|
Value any
|
||||||
Live any
|
Live any
|
||||||
Sh *string
|
Sh *string
|
||||||
Ref string
|
Ref string
|
||||||
Dir string
|
Dir string
|
||||||
Secret bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||||
@@ -24,29 +23,21 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
key = node.Content[0].Value
|
key = node.Content[0].Value
|
||||||
}
|
}
|
||||||
switch key {
|
switch key {
|
||||||
case "sh", "ref", "map", "value":
|
case "sh", "ref", "map":
|
||||||
var m struct {
|
var m struct {
|
||||||
Sh *string
|
Sh *string
|
||||||
Ref string
|
Ref string
|
||||||
Map any
|
Map any
|
||||||
Value any
|
|
||||||
Secret bool
|
|
||||||
}
|
}
|
||||||
if err := node.Decode(&m); err != nil {
|
if err := node.Decode(&m); err != nil {
|
||||||
return errors.NewTaskfileDecodeError(err, node)
|
return errors.NewTaskfileDecodeError(err, node)
|
||||||
}
|
}
|
||||||
v.Sh = m.Sh
|
v.Sh = m.Sh
|
||||||
v.Ref = m.Ref
|
v.Ref = m.Ref
|
||||||
v.Secret = m.Secret
|
v.Value = m.Map
|
||||||
// Handle both "map" and "value" keys
|
|
||||||
if m.Map != nil {
|
|
||||||
v.Value = m.Map
|
|
||||||
} else if m.Value != nil {
|
|
||||||
v.Value = m.Value
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map", "value" or using a scalar value`, key)
|
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
var value any
|
var value any
|
||||||
|
|||||||
18
testdata/failfast/default/Taskfile.yaml
vendored
18
testdata/failfast/default/Taskfile.yaml
vendored
@@ -1,14 +1,20 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
SLEEP: ''
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
default:
|
default:
|
||||||
deps:
|
deps:
|
||||||
- dep1
|
- task: dep1
|
||||||
- dep2
|
vars: { SLEEP: '{{.SLEEP}}' }
|
||||||
- dep3
|
- task: dep2
|
||||||
|
vars: { SLEEP: '{{.SLEEP}}' }
|
||||||
|
- task: dep3
|
||||||
|
vars: { SLEEP: '{{.SLEEP}}' }
|
||||||
- dep4
|
- dep4
|
||||||
|
|
||||||
dep1: sleep 0.1 && echo 'dep1'
|
dep1: '{{.SLEEP}}echo ''dep1'''
|
||||||
dep2: sleep 0.2 && echo 'dep2'
|
dep2: '{{.SLEEP}}echo ''dep2'''
|
||||||
dep3: sleep 0.3 && echo 'dep3'
|
dep3: '{{.SLEEP}}echo ''dep3'''
|
||||||
dep4: exit 1
|
dep4: exit 1
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
6
testdata/failfast/task/Taskfile.yaml
vendored
6
testdata/failfast/task/Taskfile.yaml
vendored
@@ -9,7 +9,7 @@ tasks:
|
|||||||
- dep4
|
- dep4
|
||||||
failfast: true
|
failfast: true
|
||||||
|
|
||||||
dep1: sleep 0.1 && echo 'dep1'
|
dep1: sleep 5 && echo 'dep1'
|
||||||
dep2: sleep 0.2 && echo 'dep2'
|
dep2: sleep 6 && echo 'dep2'
|
||||||
dep3: sleep 0.3 && echo 'dep3'
|
dep3: sleep 7 && echo 'dep3'
|
||||||
dep4: exit 1
|
dep4: exit 1
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
65
testdata/secrets/Taskfile.yml
vendored
65
testdata/secrets/Taskfile.yml
vendored
@@ -1,65 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
# Public variable
|
|
||||||
APP_NAME: myapp
|
|
||||||
|
|
||||||
# Secret variable with value
|
|
||||||
API_KEY:
|
|
||||||
value: "secret-api-key-123"
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
# Secret variable from shell command
|
|
||||||
PASSWORD:
|
|
||||||
sh: "echo 'my-super-secret-password'"
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
# Non-secret variable
|
|
||||||
PUBLIC_URL: https://example.com
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
test-secret-masking:
|
|
||||||
desc: Test that secret variables are masked in logs
|
|
||||||
cmds:
|
|
||||||
- echo "Deploying {{.APP_NAME}} to {{.PUBLIC_URL}}"
|
|
||||||
- echo "Using API key {{.API_KEY}}"
|
|
||||||
- echo "Password is {{.PASSWORD}}"
|
|
||||||
- echo "Public app name is {{.APP_NAME}}"
|
|
||||||
|
|
||||||
test-multiple-secrets:
|
|
||||||
desc: Test multiple secrets in one command
|
|
||||||
cmds:
|
|
||||||
- echo "API={{.API_KEY}} PWD={{.PASSWORD}}"
|
|
||||||
|
|
||||||
test-mixed:
|
|
||||||
desc: Test mix of secret and public vars
|
|
||||||
vars:
|
|
||||||
LOCAL_SECRET:
|
|
||||||
value: "task-level-secret"
|
|
||||||
secret: true
|
|
||||||
cmds:
|
|
||||||
- echo "App={{.APP_NAME}} Secret={{.LOCAL_SECRET}} URL={{.PUBLIC_URL}}"
|
|
||||||
|
|
||||||
test-deferred-secret:
|
|
||||||
desc: Test that deferred commands mask secrets
|
|
||||||
vars:
|
|
||||||
DEFERRED_SECRET:
|
|
||||||
value: "deferred-secret-value"
|
|
||||||
secret: true
|
|
||||||
cmds:
|
|
||||||
- echo "Starting task"
|
|
||||||
- defer: echo "Cleanup with secret={{.DEFERRED_SECRET}} and app={{.APP_NAME}}"
|
|
||||||
- echo "Main command executed"
|
|
||||||
|
|
||||||
test-env-secret-limitation:
|
|
||||||
desc: Test showing that env vars with secret flag are NOT masked (limitation)
|
|
||||||
env:
|
|
||||||
SECRET_TOKEN:
|
|
||||||
value: "env-secret-token-123"
|
|
||||||
PUBLIC_ENV: "public-value"
|
|
||||||
cmds:
|
|
||||||
# Templates {{.VAR}} don't work with env - they're empty
|
|
||||||
- echo "Token via template is {{.SECRET_TOKEN}}"
|
|
||||||
# Shell $VAR works but is NOT masked (env vars not in template system)
|
|
||||||
- echo "Token via shell is $SECRET_TOKEN"
|
|
||||||
- echo "Public env is {{.PUBLIC_ENV}}"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
task: [test-deferred-secret] echo "Starting task"
|
|
||||||
Starting task
|
|
||||||
task: [test-deferred-secret] echo "Main command executed"
|
|
||||||
Main command executed
|
|
||||||
task: [test-deferred-secret] echo "Cleanup with secret=***** and app=myapp"
|
|
||||||
Cleanup with secret=deferred-secret-value and app=myapp
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
task: [test-env-secret-limitation] echo "Token via template is "
|
|
||||||
Token via template is
|
|
||||||
task: [test-env-secret-limitation] echo "Token via shell is $SECRET_TOKEN"
|
|
||||||
Token via shell is env-secret-token-123
|
|
||||||
task: [test-env-secret-limitation] echo "Public env is "
|
|
||||||
Public env is
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
task: [test-mixed] echo "App=myapp Secret=***** URL=https://example.com"
|
|
||||||
App=myapp Secret=task-level-secret URL=https://example.com
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
task: [test-multiple-secrets] echo "API=***** PWD=*****"
|
|
||||||
API=secret-api-key-123 PWD=my-super-secret-password
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
task: [test-secret-masking] echo "Deploying myapp to https://example.com"
|
|
||||||
Deploying myapp to https://example.com
|
|
||||||
task: [test-secret-masking] echo "Using API key *****"
|
|
||||||
Using API key secret-api-key-123
|
|
||||||
task: [test-secret-masking] echo "Password is *****"
|
|
||||||
Password is my-super-secret-password
|
|
||||||
task: [test-secret-masking] echo "Public app name is myapp"
|
|
||||||
Public app name is myapp
|
|
||||||
@@ -239,8 +239,6 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
|||||||
extra["KEY"] = keys[i]
|
extra["KEY"] = keys[i]
|
||||||
}
|
}
|
||||||
newCmd := cmd.DeepCopy()
|
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.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
|
||||||
newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
|
newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
|
||||||
newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
|
newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
|
||||||
@@ -256,8 +254,6 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newCmd := cmd.DeepCopy()
|
newCmd := cmd.DeepCopy()
|
||||||
// Resolve template with secrets masked for logging
|
|
||||||
newCmd.LogCmd = templater.MaskSecrets(cmd.Cmd, cache.Vars)
|
|
||||||
newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
|
newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
|
||||||
newCmd.Task = templater.Replace(cmd.Task, cache)
|
newCmd.Task = templater.Replace(cmd.Task, cache)
|
||||||
newCmd.If = templater.Replace(cmd.If, cache)
|
newCmd.If = templater.Replace(cmd.If, cache)
|
||||||
|
|||||||
@@ -1614,163 +1614,6 @@ tasks:
|
|||||||
map[a:1 b:2 c:3]
|
map[a:1 b:2 c:3]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Secret variables
|
|
||||||
|
|
||||||
Task supports marking variables as `secret` to prevent their values from being
|
|
||||||
displayed in command logs. When a variable is marked as secret, its value will
|
|
||||||
be replaced with `*****` in the task output logs.
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
|
|
||||||
**Security Notice**: This feature helps prevent accidental exposure of secrets
|
|
||||||
in logs, but is **not a substitute** for proper secret management practices.
|
|
||||||
|
|
||||||
**What this protects:**
|
|
||||||
|
|
||||||
- ✅ Secret values in console/terminal logs
|
|
||||||
- ✅ Secret values in CI/CD logs
|
|
||||||
- ✅ Accidental copy-paste of logs containing secrets
|
|
||||||
|
|
||||||
**What this does NOT protect:**
|
|
||||||
|
|
||||||
- ❌ Secrets visible in process inspection (e.g., `ps aux`)
|
|
||||||
- ❌ Secrets in shell history
|
|
||||||
- ❌ Secrets in command output (stdout/stderr)
|
|
||||||
|
|
||||||
Always use proper secret management tools (HashiCorp Vault, AWS Secrets
|
|
||||||
Manager, etc.) for production environments.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
To mark a variable as secret, add `secret: true` to the variable definition:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
value: 'sk-1234567890abcdef'
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
deploy:
|
|
||||||
cmds:
|
|
||||||
- curl -H "Authorization: {{.API_KEY}}" api.example.com
|
|
||||||
# Logged as: task: [deploy] curl -H "Authorization: *****" api.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Secret variables work with all variable types:
|
|
||||||
|
|
||||||
::: code-group
|
|
||||||
|
|
||||||
```yaml [Simple Value]
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
PASSWORD:
|
|
||||||
value: 'my-secret-password'
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
connect:
|
|
||||||
cmds:
|
|
||||||
- psql -U user -p {{.PASSWORD}} mydb
|
|
||||||
# Logged as: psql -U user -p ***** mydb
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml [Shell Command]
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
DB_PASSWORD:
|
|
||||||
sh: vault read -field=password secret/db
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
migrate:
|
|
||||||
cmds:
|
|
||||||
- psql -U admin -p {{.DB_PASSWORD}} mydb
|
|
||||||
# Password from vault is masked in logs
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml [Task-Level Secret]
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
PUBLIC_URL: https://example.com
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
deploy:
|
|
||||||
vars:
|
|
||||||
DEPLOY_TOKEN:
|
|
||||||
value: 'secret-token-123'
|
|
||||||
secret: true
|
|
||||||
cmds:
|
|
||||||
- echo "Deploying to {{.PUBLIC_URL}} with token {{.DEPLOY_TOKEN}}"
|
|
||||||
# Logged as: echo "Deploying to https://example.com with token *****"
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
Multiple secrets in the same command are all masked:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
value: 'api-key-123'
|
|
||||||
secret: true
|
|
||||||
PASSWORD:
|
|
||||||
value: 'password-456'
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
setup:
|
|
||||||
cmds:
|
|
||||||
- ./setup.sh --api {{.API_KEY}} --pwd {{.PASSWORD}}
|
|
||||||
# Logged as: ./setup.sh --api ***** --pwd *****
|
|
||||||
```
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
|
|
||||||
**Best practices for secret variables:**
|
|
||||||
|
|
||||||
1. **Use shell commands to load secrets**, not hardcoded values:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# ❌ BAD - Secret visible in Taskfile
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
value: 'hardcoded-secret'
|
|
||||||
secret: true
|
|
||||||
|
|
||||||
# ✅ GOOD - Secret loaded from external source
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
sh: vault kv get -field=api_key secret/myapp
|
|
||||||
secret: true
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Combine with environment variables:**
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
sh: echo $MY_API_KEY
|
|
||||||
secret: true
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Use .gitignore for secret files:**
|
|
||||||
|
|
||||||
If you use dotenv files, add them to `.gitignore`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
dotenv: ['.env.local'] # Load from .env.local (in .gitignore)
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Looping over values
|
## Looping over values
|
||||||
|
|
||||||
Task allows you to loop over certain values and execute a command for each.
|
Task allows you to loop over certain values and execute a command for each.
|
||||||
|
|||||||
@@ -379,33 +379,6 @@ vars:
|
|||||||
ttl: 3600
|
ttl: 3600
|
||||||
```
|
```
|
||||||
|
|
||||||
### Secret Variables (`secret`)
|
|
||||||
|
|
||||||
Mark variables as secret to mask their values in command logs.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
value: 'sk-1234567890abcdef'
|
|
||||||
secret: true # This variable will be masked in logs
|
|
||||||
|
|
||||||
DB_PASSWORD:
|
|
||||||
sh: vault read -field=password secret/db
|
|
||||||
secret: true # Works with dynamic variables too
|
|
||||||
```
|
|
||||||
|
|
||||||
When a variable is marked as `secret: true`, Task will replace its value with
|
|
||||||
`*****` in command logs. The actual command execution still receives the real
|
|
||||||
value.
|
|
||||||
|
|
||||||
::: info
|
|
||||||
|
|
||||||
For complete documentation on secret variables, including security
|
|
||||||
considerations and best practices, see the
|
|
||||||
[Secret variables](/docs/guide#secret-variables) section in the Guide.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### Variable Ordering
|
### Variable Ordering
|
||||||
|
|
||||||
Variables can reference previously defined variables:
|
Variables can reference previously defined variables:
|
||||||
|
|||||||
@@ -318,10 +318,6 @@
|
|||||||
"map": {
|
"map": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "The value will be treated as a literal map type and stored in the variable"
|
"description": "The value will be treated as a literal map type and stored in the variable"
|
||||||
},
|
|
||||||
"secret": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Marks the variable as secret. Secret values will be masked as ***** in command logs to prevent accidental exposure of sensitive information."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
Reference in New Issue
Block a user