mirror of
https://github.com/go-task/task.git
synced 2026-06-29 15:44:30 +00:00
Compare commits
1 Commits
task-secre
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b903c772 |
69
.github/workflows/pr-build.yml
vendored
69
.github/workflows/pr-build.yml
vendored
@@ -1,69 +0,0 @@
|
|||||||
name: PR Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [labeled, synchronize]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: contains(github.event.pull_request.labels.*.name, 'needs-build')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
||||||
with:
|
|
||||||
go-version: "1.26.x"
|
|
||||||
cache: true
|
|
||||||
- uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
|
|
||||||
with:
|
|
||||||
version: "~> v2"
|
|
||||||
args: release --snapshot --clean --config .goreleaser-pr.yml
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: task_linux_amd64
|
|
||||||
path: dist/task_linux_amd64.tar.gz
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: task_linux_arm64
|
|
||||||
path: dist/task_linux_arm64.tar.gz
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: task_darwin_amd64
|
|
||||||
path: dist/task_darwin_amd64.tar.gz
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: task_darwin_arm64
|
|
||||||
path: dist/task_darwin_arm64.tar.gz
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: task_windows_amd64
|
|
||||||
path: dist/task_windows_amd64.zip
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: checksums
|
|
||||||
path: dist/task_checksums.txt
|
|
||||||
- uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
|
||||||
id: find-comment
|
|
||||||
with:
|
|
||||||
token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body-includes: "📦 Build artifacts ready!"
|
|
||||||
- uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
|
||||||
with:
|
|
||||||
token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
|
||||||
body: |
|
|
||||||
## 📦 Build artifacts ready!
|
|
||||||
|
|
||||||
Download binaries from [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
|
|
||||||
|
|
||||||
Available platforms: Linux, macOS, Windows (amd64, arm64)
|
|
||||||
edit-mode: replace
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- binary: task
|
|
||||||
main: ./cmd/task
|
|
||||||
goos: [windows, darwin, linux]
|
|
||||||
goarch: [amd64, arm64]
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- "-s -w"
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
|
|
||||||
files:
|
|
||||||
- README.md
|
|
||||||
- LICENSE
|
|
||||||
- completion/**/*
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
formats: [zip]
|
|
||||||
|
|
||||||
snapshot:
|
|
||||||
version_template: 'pr-{{ .ShortCommit }}'
|
|
||||||
|
|
||||||
checksum:
|
|
||||||
name_template: 'task_checksums.txt'
|
|
||||||
17
compiler.go
17
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,12 +184,7 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string,
|
|||||||
result = strings.TrimSuffix(result, "\n")
|
result = strings.TrimSuffix(result, "\n")
|
||||||
|
|
||||||
c.dynamicCache[*v.Sh] = result
|
c.dynamicCache[*v.Sh] = result
|
||||||
// Never print the resolved value of a secret variable, even in verbose mode
|
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
|
||||||
logResult := result
|
|
||||||
if v.Secret {
|
|
||||||
logResult = "*****"
|
|
||||||
}
|
|
||||||
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, logResult)
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,68 +283,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"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("secret vars are masked in summary"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
task.WithSummary(true),
|
|
||||||
),
|
|
||||||
WithTask("test-secret-masking"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("dynamic secret masked in verbose"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
task.WithVerbose(true),
|
|
||||||
),
|
|
||||||
WithTask("test-dynamic-secret-verbose"),
|
|
||||||
)
|
|
||||||
NewExecutorTest(t,
|
|
||||||
WithName("secret key order independent"),
|
|
||||||
WithExecutorOptions(
|
|
||||||
task.WithDir("testdata/secrets"),
|
|
||||||
),
|
|
||||||
WithTask("test-secret-key-order"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequires(t *testing.T) {
|
func TestRequires(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
NewExecutorTest(t,
|
NewExecutorTest(t,
|
||||||
|
|||||||
@@ -117,12 +117,7 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
|
|||||||
isCommand := c.Cmd != ""
|
isCommand := c.Cmd != ""
|
||||||
l.Outf(logger.Default, " - ")
|
l.Outf(logger.Default, " - ")
|
||||||
if isCommand {
|
if isCommand {
|
||||||
// Use the masked command so secret values are not leaked in summaries
|
l.Outf(logger.Yellow, "%s\n", c.Cmd)
|
||||||
logCmd := c.LogCmd
|
|
||||||
if logCmd == "" {
|
|
||||||
logCmd = c.Cmd
|
|
||||||
}
|
|
||||||
l.Outf(logger.Yellow, "%s\n", logCmd)
|
|
||||||
} else {
|
} else {
|
||||||
l.Outf(logger.Green, "Task: %s\n", c.Task)
|
l.Outf(logger.Green, "Task: %s\n", c.Task)
|
||||||
}
|
}
|
||||||
@@ -201,11 +196,6 @@ func printTaskEnv(l *logger.Logger, t *ast.Task) {
|
|||||||
// formatVarValue formats a variable value based on its type.
|
// formatVarValue formats a variable value based on its type.
|
||||||
// Handles static values, shell commands (sh:), references (ref:), and maps.
|
// Handles static values, shell commands (sh:), references (ref:), and maps.
|
||||||
func formatVarValue(v ast.Var) string {
|
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
|
// Shell command - check this first before Value
|
||||||
// because dynamic vars may have both Sh and an empty Value
|
// because dynamic vars may have both Sh and an empty Value
|
||||||
if v.Sh != nil {
|
if v.Sh != nil {
|
||||||
|
|||||||
@@ -1,61 +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 {
|
|
||||||
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,7 +132,7 @@ 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),
|
||||||
@@ -140,7 +140,6 @@ func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var
|
|||||||
Live: v.Live,
|
Live: v.Live,
|
||||||
Ref: v.Ref,
|
Ref: v.Ref,
|
||||||
Dir: v.Dir,
|
Dir: v.Dir,
|
||||||
Secret: v.Secret,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
task.go
6
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)
|
||||||
@@ -390,12 +388,12 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
|
|||||||
return err
|
return err
|
||||||
case cmd.Cmd != "":
|
case cmd.Cmd != "":
|
||||||
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
|
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
|
||||||
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.LogCmd)
|
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
@@ -13,49 +13,32 @@ type Var struct {
|
|||||||
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 {
|
||||||
switch node.Kind {
|
switch node.Kind {
|
||||||
case yaml.MappingNode:
|
case yaml.MappingNode:
|
||||||
|
key := "<none>"
|
||||||
|
if len(node.Content) > 0 {
|
||||||
|
key = node.Content[0].Value
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
// Validate the keys regardless of their order: every key must be known
|
|
||||||
// and at least one type-defining key must be present. "secret" is a
|
|
||||||
// modifier, not a type, so it can appear in any position.
|
|
||||||
hasType := false
|
|
||||||
for i := 0; i+1 < len(node.Content); i += 2 {
|
|
||||||
switch node.Content[i].Value {
|
|
||||||
case "sh", "ref", "map", "value":
|
|
||||||
hasType = true
|
|
||||||
case "secret":
|
|
||||||
// modifier, not a type
|
|
||||||
default:
|
|
||||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map", "value" or using a scalar value`, node.Content[i].Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasType {
|
|
||||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`a variable must define one of "sh", "ref", "map", "value" or be a scalar value`)
|
|
||||||
}
|
|
||||||
v.Sh = m.Sh
|
v.Sh = m.Sh
|
||||||
v.Ref = m.Ref
|
v.Ref = m.Ref
|
||||||
v.Secret = m.Secret
|
|
||||||
// Handle both "map" and "value" keys
|
|
||||||
if m.Map != nil {
|
|
||||||
v.Value = m.Map
|
v.Value = m.Map
|
||||||
} else if m.Value != nil {
|
|
||||||
v.Value = m.Value
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
|
default:
|
||||||
|
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
|
||||||
if err := node.Decode(&value); err != nil {
|
if err := node.Decode(&value); err != nil {
|
||||||
|
|||||||
83
testdata/secrets/Taskfile.yml
vendored
83
testdata/secrets/Taskfile.yml
vendored
@@ -1,83 +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-dynamic-secret-verbose:
|
|
||||||
desc: Test that dynamic (sh) secrets are masked even in verbose logs
|
|
||||||
cmds:
|
|
||||||
- echo "Password is {{.PASSWORD}}"
|
|
||||||
|
|
||||||
test-secret-key-order:
|
|
||||||
desc: Test that "secret" may be declared before the value/sh key
|
|
||||||
vars:
|
|
||||||
SECRET_FIRST:
|
|
||||||
secret: true
|
|
||||||
value: "order-independent-secret"
|
|
||||||
SH_SECRET_FIRST:
|
|
||||||
secret: true
|
|
||||||
sh: "echo 'sh-order-independent-secret'"
|
|
||||||
cmds:
|
|
||||||
- echo "Value={{.SECRET_FIRST}} Sh={{.SH_SECRET_FIRST}}"
|
|
||||||
|
|
||||||
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"
|
|
||||||
secret: true
|
|
||||||
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,5 +0,0 @@
|
|||||||
task: dynamic variable: "echo 'my-super-secret-password'" result: "*****"
|
|
||||||
task: "test-dynamic-secret-verbose" started
|
|
||||||
task: [test-dynamic-secret-verbose] echo "Password is *****"
|
|
||||||
Password is my-super-secret-password
|
|
||||||
task: "test-dynamic-secret-verbose" finished
|
|
||||||
@@ -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,2 +0,0 @@
|
|||||||
task: [test-secret-key-order] echo "Value=***** Sh=*****"
|
|
||||||
Value=order-independent-secret Sh=sh-order-independent-secret
|
|
||||||
@@ -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
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
task: test-secret-masking
|
|
||||||
|
|
||||||
Test that secret variables are masked in logs
|
|
||||||
|
|
||||||
vars:
|
|
||||||
APP_NAME: "myapp"
|
|
||||||
API_KEY: *****
|
|
||||||
PASSWORD: *****
|
|
||||||
PUBLIC_URL: "https://example.com"
|
|
||||||
|
|
||||||
commands:
|
|
||||||
- echo "Deploying myapp to https://example.com"
|
|
||||||
- echo "Using API key *****"
|
|
||||||
- echo "Password is *****"
|
|
||||||
- echo "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,198 +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)
|
|
||||||
- ❌ Secret values copied into derived (non-secret) variables
|
|
||||||
|
|
||||||
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)
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
|
|
||||||
**Secrets are not propagated to derived variables.** The `secret` flag only
|
|
||||||
masks the variable it is set on. A non-secret variable that references a secret
|
|
||||||
will expose the resolved value in logs:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
|
|
||||||
vars:
|
|
||||||
API_KEY:
|
|
||||||
value: 'secret-api-key-123'
|
|
||||||
secret: true
|
|
||||||
HEADER:
|
|
||||||
value: 'Bearer {{.API_KEY}}' # ❌ not marked as secret
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
call:
|
|
||||||
cmds:
|
|
||||||
- curl -H "{{.HEADER}}" api.example.com
|
|
||||||
# Logged as: curl -H "Bearer secret-api-key-123" api.example.com (LEAK)
|
|
||||||
```
|
|
||||||
|
|
||||||
Mark every variable that carries a secret value as `secret: true`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
vars:
|
|
||||||
HEADER:
|
|
||||||
value: 'Bearer {{.API_KEY}}'
|
|
||||||
secret: true # ✅ masked
|
|
||||||
```
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -385,33 +385,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,13 +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"
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"description": "A literal value assigned to the variable. Useful together with other keys such as 'secret'"
|
|
||||||
},
|
|
||||||
"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