mirror of
https://github.com/go-task/task.git
synced 2026-06-29 07:34:18 +00:00
Compare commits
28 Commits
fix/specia
...
task-secre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8545e02e5e | ||
|
|
1d9b3cb7db | ||
|
|
1a0f146888 | ||
|
|
b9b50ca79c | ||
|
|
3dcaa7db89 | ||
|
|
a72eb84c15 | ||
|
|
91b9e42f17 | ||
|
|
f1ab404fbb | ||
|
|
24a3ccdf42 | ||
|
|
b455286b63 | ||
|
|
81d621d8aa | ||
|
|
f50327f2a2 | ||
|
|
dd810d42e1 | ||
|
|
de1717b05f | ||
|
|
7766e8add5 | ||
|
|
e61d0f0f7c | ||
|
|
9c8799480d | ||
|
|
7f04f7057c | ||
|
|
48dd62cdc1 | ||
|
|
14e151ae9b | ||
|
|
4c2ed3e0dc | ||
|
|
32f237af7d | ||
|
|
ffbb9781c2 | ||
|
|
97f207972a | ||
|
|
9057728e15 | ||
|
|
dd06762d79 | ||
|
|
07b5a26aaf | ||
|
|
8bd982c702 |
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -23,10 +23,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{matrix.go-version}}
|
go-version: ${{matrix.go-version}}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
|
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
|
||||||
with:
|
with:
|
||||||
version: v2.12.2
|
version: v2.12.2
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: 3.14
|
python-version: 3.14
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: install check-jsonschema
|
- name: install check-jsonschema
|
||||||
run: python -m pip install 'check-jsonschema==0.27.3'
|
run: python -m pip install 'check-jsonschema==0.27.3'
|
||||||
|
|||||||
4
.github/workflows/pr-build.yml
vendored
4
.github/workflows/pr-build.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
if: contains(github.event.pull_request.labels.*.name, 'needs-build')
|
if: contains(github.event.pull_request.labels.*.name, 'needs-build')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: "1.26.x"
|
go-version: "1.26.x"
|
||||||
cache: true
|
cache: true
|
||||||
- uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
|
- uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
|
||||||
with:
|
with:
|
||||||
version: "~> v2"
|
version: "~> v2"
|
||||||
args: release --snapshot --clean --config .goreleaser-pr.yml
|
args: release --snapshot --clean --config .goreleaser-pr.yml
|
||||||
|
|||||||
4
.github/workflows/release-nightly.yml
vendored
4
.github/workflows/release-nightly.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
go-version: 1.26.x
|
go-version: 1.26.x
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
|
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
version: latest
|
version: latest
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
run: npm install -g npm@latest
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
- name: Install Task
|
- name: Install Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2
|
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
run_install: "true"
|
run_install: "true"
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
|
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
version: latest
|
version: latest
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ${{matrix.platform}}
|
runs-on: ${{matrix.platform}}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Set up Go ${{matrix.go-version}}
|
- name: Set up Go ${{matrix.go-version}}
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ nfpms:
|
|||||||
- src: completion/bash/task.bash
|
- src: completion/bash/task.bash
|
||||||
dst: /etc/bash_completion.d/task
|
dst: /etc/bash_completion.d/task
|
||||||
- src: completion/fish/task.fish
|
- src: completion/fish/task.fish
|
||||||
dst: /usr/share/fish/completions/task.fish
|
dst: /usr/share/fish/vendor_completions.d/task.fish
|
||||||
- src: completion/zsh/_task
|
- src: completion/zsh/_task
|
||||||
dst: /usr/local/share/zsh/site-functions/_task
|
dst: /usr/local/share/zsh/site-functions/_task
|
||||||
|
|
||||||
|
|||||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Fixed --interactive prompts for required vars sometimes appearing in a random
|
||||||
|
order. Prompts now follow the order the vars are declared in the Taskfile.
|
||||||
|
(#2871 by @caproven)
|
||||||
|
- Fixed Fish completions not being picked up correctly by installing them to
|
||||||
|
Fish's `vendor_completions.d` directory instead of `completions` (#2850, #2859
|
||||||
|
by @Legimity).
|
||||||
|
- PowerShell completions now work with aliases of the `task` command, not just
|
||||||
|
the `task` binary itself (#2852 by @kojiishi).
|
||||||
|
- Fixed task names containing certain characters (e.g. `\`, `_`, `^`) leaking
|
||||||
|
into checksum/timestamp filenames, breaking `sources:`/`generates:`
|
||||||
|
up-to-date detection (#2886 by @s3onghyun).
|
||||||
|
- Fixed `for: matrix:` loops using `ref:` rows producing wrong values when the
|
||||||
|
same task was run concurrently (e.g. by parallel `deps`) with different vars
|
||||||
|
(#2890, #2894 by @amitmishra11).
|
||||||
|
|
||||||
## v3.51.1 - 2026-05-16
|
## v3.51.1 - 2026-05-16
|
||||||
|
|
||||||
- A significant performance boost was achieved for large Taskfiles (monorepos)
|
- A significant performance boost was achieved for large Taskfiles (monorepos)
|
||||||
@@ -8,9 +25,9 @@
|
|||||||
cleaning `..` and `.` components (#2681, #2788 by @mateenanjum).
|
cleaning `..` and `.` components (#2681, #2788 by @mateenanjum).
|
||||||
- Added `joinEnv` function to join paths based on your oprating system: `;` for
|
- Added `joinEnv` function to join paths based on your oprating system: `;` for
|
||||||
Windows and `:` elsewhere, and `joinUrl` to join URL paths. Also, added two
|
Windows and `:` elsewhere, and `joinUrl` to join URL paths. Also, added two
|
||||||
new special variables: `FILE_PATH_SEPARATOR` which returns `\` on Windows
|
new special variables: `FILE_PATH_SEPARATOR` which returns `\` on Windows and
|
||||||
and `/` elsewhere, and `PATH_LIST_SEPARATOR` which returns `;` on Windows and
|
`/` elsewhere, and `PATH_LIST_SEPARATOR` which returns `;` on Windows and `:`
|
||||||
`:` elsewhere (#2406, #2408 by @solvingj).
|
elsewhere (#2406, #2408 by @solvingj).
|
||||||
- Update the shell interpreter with a regression fix (#2812, #2832 by
|
- Update the shell interpreter with a regression fix (#2812, #2832 by
|
||||||
@andreynering).
|
@andreynering).
|
||||||
- Fix potential panic with the shell interpreter (#2810 by @trulede).
|
- Fix potential panic with the shell interpreter (#2810 by @trulede).
|
||||||
@@ -26,13 +43,13 @@
|
|||||||
- Fixed watch mode ignoring SIGHUP signal, causing the watcher to exit instead
|
- Fixed watch mode ignoring SIGHUP signal, causing the watcher to exit instead
|
||||||
of restarting (#2764, #2642).
|
of restarting (#2764, #2642).
|
||||||
- Fixed a long time bug where the task wouldn't re-run as it should when using
|
- Fixed a long time bug where the task wouldn't re-run as it should when using
|
||||||
`method: timestamp` and the files listed on `generates:` were deleted.
|
`method: timestamp` and the files listed on `generates:` were deleted. This
|
||||||
This makes `method: timestamp` behaves the same as `method: checksum`
|
makes `method: timestamp` behaves the same as `method: checksum` (#1230, #2716
|
||||||
(#1230, #2716 by @drichardson).
|
by @drichardson).
|
||||||
|
|
||||||
## v3.49.1 - 2026-03-08
|
## v3.49.1 - 2026-03-08
|
||||||
|
|
||||||
* Reverted #2632 for now, which caused some regressions. That change will be
|
- Reverted #2632 for now, which caused some regressions. That change will be
|
||||||
reworked (#2720, #2722, #2723).
|
reworked (#2720, #2722, #2723).
|
||||||
|
|
||||||
## v3.49.0 - 2026-03-07
|
## v3.49.0 - 2026-03-07
|
||||||
|
|||||||
37
compiler.go
37
compiler.go
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/go-task/task/v3/internal/logger"
|
"github.com/go-task/task/v3/internal/logger"
|
||||||
"github.com/go-task/task/v3/internal/templater"
|
"github.com/go-task/task/v3/internal/templater"
|
||||||
"github.com/go-task/task/v3/internal/version"
|
"github.com/go-task/task/v3/internal/version"
|
||||||
"github.com/go-task/task/v3/taskfile"
|
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,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})
|
result.Set(k, ast.Var{Value: v, Secret: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
||||||
@@ -64,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})
|
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh, Secret: v.Secret})
|
||||||
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})
|
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh, Secret: v.Secret})
|
||||||
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
|
||||||
@@ -78,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})
|
result.Set(k, ast.Var{Value: newVar.Value, Secret: v.Secret})
|
||||||
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
|
||||||
@@ -86,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})
|
result.Set(k, ast.Var{Value: static, Secret: v.Secret})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,19 +201,10 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
|
|||||||
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
|
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
|
||||||
// across platforms. This prevents issues with backslashes being interpreted
|
// across platforms. This prevents issues with backslashes being interpreted
|
||||||
// as escape sequences when paths are used in shell commands on Windows.
|
// as escape sequences when paths are used in shell commands on Windows.
|
||||||
var rootTaskfile, rootDir string
|
|
||||||
if taskfile.IsRemoteEntrypoint(c.Entrypoint) {
|
|
||||||
rootTaskfile = c.Entrypoint
|
|
||||||
rootDir = ""
|
|
||||||
} else {
|
|
||||||
rootTaskfile = filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint))
|
|
||||||
rootDir = filepath.ToSlash(c.Dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
allVars := map[string]string{
|
allVars := map[string]string{
|
||||||
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
||||||
"ROOT_TASKFILE": rootTaskfile,
|
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
|
||||||
"ROOT_DIR": rootDir,
|
"ROOT_DIR": filepath.ToSlash(c.Dir),
|
||||||
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
|
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
|
||||||
"TASK_VERSION": version.GetVersion(),
|
"TASK_VERSION": version.GetVersion(),
|
||||||
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
|
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
|
||||||
@@ -222,22 +212,9 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
|
|||||||
}
|
}
|
||||||
if t != nil {
|
if t != nil {
|
||||||
allVars["TASK"] = t.Task
|
allVars["TASK"] = t.Task
|
||||||
if taskfile.IsRemoteEntrypoint(t.Location.Taskfile) {
|
|
||||||
allVars["TASKFILE"] = t.Location.Taskfile
|
|
||||||
allVars["TASKFILE_DIR"] = ""
|
|
||||||
switch {
|
|
||||||
case t.Dir == "":
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(c.UserWorkingDir)
|
|
||||||
case filepath.IsAbs(t.Dir):
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(t.Dir)
|
|
||||||
default:
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.UserWorkingDir, t.Dir))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
|
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
|
||||||
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
|
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
|
||||||
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
|
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
allVars["TASK"] = ""
|
allVars["TASK"] = ""
|
||||||
allVars["TASK_DIR"] = ""
|
allVars["TASK_DIR"] = ""
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/filepathext"
|
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetSpecialVarsRemote(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
uwd := t.TempDir()
|
|
||||||
uwdSlash := filepath.ToSlash(uwd)
|
|
||||||
localProj := filepath.Join(uwd, "proj")
|
|
||||||
localProjSlash := filepath.ToSlash(localProj)
|
|
||||||
localTaskfile := filepath.Join(localProj, "Taskfile.yml")
|
|
||||||
localTaskfileSlash := filepath.ToSlash(localTaskfile)
|
|
||||||
absTaskDir := filepath.Join(uwd, "opt", "work")
|
|
||||||
absTaskDirSlash := filepath.ToSlash(absTaskDir)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
entrypoint string
|
|
||||||
compilerDir string
|
|
||||||
taskDir string
|
|
||||||
taskfileLocation string
|
|
||||||
wantRootTaskfile string
|
|
||||||
wantRootDir string
|
|
||||||
wantTaskfile string
|
|
||||||
wantTaskfileDir string
|
|
||||||
wantTaskDir string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "local entrypoint, local task",
|
|
||||||
entrypoint: localTaskfile,
|
|
||||||
compilerDir: localProj,
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: localTaskfile,
|
|
||||||
wantRootTaskfile: localTaskfileSlash,
|
|
||||||
wantRootDir: localProjSlash,
|
|
||||||
wantTaskfile: localTaskfileSlash,
|
|
||||||
wantTaskfileDir: localProjSlash,
|
|
||||||
wantTaskDir: localProjSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https entrypoint, empty task.dir",
|
|
||||||
entrypoint: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: uwdSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https entrypoint, relative task.dir",
|
|
||||||
entrypoint: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: "subdir",
|
|
||||||
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: filepath.ToSlash(filepathext.SmartJoin(uwd, "subdir")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https entrypoint, absolute task.dir",
|
|
||||||
entrypoint: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: absTaskDir,
|
|
||||||
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: absTaskDirSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "git entrypoint",
|
|
||||||
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
wantRootTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: uwdSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "local root, remote included task",
|
|
||||||
entrypoint: localTaskfile,
|
|
||||||
compilerDir: localProj,
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: "https://taskfile.dev/included.yml",
|
|
||||||
wantRootTaskfile: localTaskfileSlash,
|
|
||||||
wantRootDir: localProjSlash,
|
|
||||||
wantTaskfile: "https://taskfile.dev/included.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: uwdSlash,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
c := &Compiler{
|
|
||||||
Dir: tt.compilerDir,
|
|
||||||
Entrypoint: tt.entrypoint,
|
|
||||||
UserWorkingDir: uwd,
|
|
||||||
}
|
|
||||||
task := &ast.Task{
|
|
||||||
Task: "mytask",
|
|
||||||
Dir: tt.taskDir,
|
|
||||||
Location: &ast.Location{Taskfile: tt.taskfileLocation},
|
|
||||||
}
|
|
||||||
|
|
||||||
vars, err := c.getSpecialVars(task, nil)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.wantRootTaskfile, vars["ROOT_TASKFILE"], "ROOT_TASKFILE")
|
|
||||||
assert.Equal(t, tt.wantRootDir, vars["ROOT_DIR"], "ROOT_DIR")
|
|
||||||
assert.Equal(t, tt.wantTaskfile, vars["TASKFILE"], "TASKFILE")
|
|
||||||
assert.Equal(t, tt.wantTaskfileDir, vars["TASKFILE_DIR"], "TASKFILE_DIR")
|
|
||||||
assert.Equal(t, tt.wantTaskDir, vars["TASK_DIR"], "TASK_DIR")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using namespace System.Management.Automation
|
using namespace System.Management.Automation
|
||||||
|
|
||||||
Register-ArgumentCompleter -CommandName task -ScriptBlock {
|
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique
|
||||||
|
|
||||||
|
Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock {
|
||||||
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
||||||
|
|
||||||
if ($commandName.StartsWith('-')) {
|
if ($commandName.StartsWith('-')) {
|
||||||
|
|||||||
@@ -283,6 +283,45 @@ 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,
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -4,11 +4,11 @@ go 1.25.10
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.1.0
|
charm.land/bubbles/v2 v2.1.0
|
||||||
charm.land/bubbletea/v2 v2.0.6
|
charm.land/bubbletea/v2 v2.0.7
|
||||||
charm.land/lipgloss/v2 v2.0.3
|
charm.land/lipgloss/v2 v2.0.4
|
||||||
github.com/Ladicle/tabwriter v1.0.0
|
github.com/Ladicle/tabwriter v1.0.0
|
||||||
github.com/Masterminds/semver/v3 v3.5.0
|
github.com/Masterminds/semver/v3 v3.5.0
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1
|
github.com/alecthomas/chroma/v2 v2.27.0
|
||||||
github.com/chainguard-dev/git-urls v1.0.2
|
github.com/chainguard-dev/git-urls v1.0.2
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||||
github.com/dominikbraun/graph v0.23.0
|
github.com/dominikbraun/graph v0.23.0
|
||||||
@@ -28,8 +28,8 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/zeebo/xxh3 v1.1.0
|
github.com/zeebo/xxh3 v1.1.0
|
||||||
go.yaml.in/yaml/v3 v3.0.4
|
go.yaml.in/yaml/v3 v3.0.4
|
||||||
golang.org/x/sync v0.20.0
|
golang.org/x/sync v0.21.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.44.0
|
||||||
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
|
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
|
||||||
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117
|
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117
|
||||||
)
|
)
|
||||||
@@ -69,7 +69,7 @@ require (
|
|||||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
@@ -77,7 +77,7 @@ require (
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||||
@@ -121,9 +121,9 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.55.0 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/api v0.271.0 // indirect
|
google.golang.org/api v0.271.0 // indirect
|
||||||
|
|||||||
42
go.sum
42
go.sum
@@ -4,8 +4,12 @@ charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
|||||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||||
charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
|
charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
|
||||||
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
|
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
|
||||||
|
charm.land/bubbletea/v2 v2.0.7 h1:7qw2tTAVar7m7klOPBYfTB0mniv/RuexsYwMRNxSeL0=
|
||||||
|
charm.land/bubbletea/v2 v2.0.7/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
|
||||||
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
|
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
|
||||||
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
|
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
|
||||||
|
charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q=
|
||||||
|
charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik=
|
||||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||||
@@ -36,16 +40,14 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
|
|||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||||
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
|
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
|
||||||
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
|
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
|
||||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
@@ -100,6 +102,8 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex
|
|||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
@@ -122,10 +126,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
|
||||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||||
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
|
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
|
||||||
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -144,8 +148,6 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
|||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
|
||||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
@@ -284,17 +286,23 @@ golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
|||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||||
|
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
@@ -323,7 +331,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE=
|
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE=
|
||||||
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo=
|
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo=
|
||||||
mvdan.cc/sh/v3 v3.13.2-0.20260503214111-9e7dd28c81cf h1:3mGRe/xSr7fd9m+c5FSab/CSCtADsbdMcyhYGdVR6DY=
|
|
||||||
mvdan.cc/sh/v3 v3.13.2-0.20260503214111-9e7dd28c81cf/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
|
|
||||||
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117 h1:BfzdGSjcnJBb8sPNLudpzTml8zFUxS1N0N/v9IIS6tQ=
|
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117 h1:BfzdGSjcnJBb8sPNLudpzTml8zFUxS1N0N/v9IIS6tQ=
|
||||||
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
|
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func (checker *ChecksumChecker) checksumFilePath(t *ast.Task) string {
|
|||||||
return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name()))
|
return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name()))
|
||||||
}
|
}
|
||||||
|
|
||||||
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
|
var checksumFilenameRegexp = regexp.MustCompile("[^[:alnum:]]")
|
||||||
|
|
||||||
// replaces invalid characters on filenames with "-"
|
// replaces invalid characters on filenames with "-"
|
||||||
func normalizeFilename(f string) string {
|
func normalizeFilename(f string) string {
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ func TestNormalizeFilename(t *testing.T) {
|
|||||||
{"foo/bar/baz", "foo-bar-baz"},
|
{"foo/bar/baz", "foo-bar-baz"},
|
||||||
{"foo@bar/baz", "foo-bar-baz"},
|
{"foo@bar/baz", "foo-bar-baz"},
|
||||||
{"foo1bar2baz3", "foo1bar2baz3"},
|
{"foo1bar2baz3", "foo1bar2baz3"},
|
||||||
|
{"foo\\bar", "foo-bar"},
|
||||||
|
{"foo_bar", "foo-bar"},
|
||||||
|
{"foo[bar]baz", "foo-bar-baz"},
|
||||||
|
{"foo^bar`baz", "foo-bar-baz"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
assert.Equal(t, test.Out, normalizeFilename(test.In))
|
assert.Equal(t, test.Out, normalizeFilename(test.In))
|
||||||
|
|||||||
70
internal/templater/secrets.go
Normal file
70
internal/templater/secrets.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -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)}
|
return ast.Var{Value: ResolveRef(v.Ref, cache), Secret: v.Secret}
|
||||||
}
|
}
|
||||||
return ast.Var{
|
return ast.Var{
|
||||||
Value: ReplaceWithExtra(v.Value, cache, extra),
|
Value: ReplaceWithExtra(v.Value, cache, extra),
|
||||||
@@ -140,6 +140,7 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
mise.toml
Normal file
12
mise.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[tools]
|
||||||
|
# Runtimes
|
||||||
|
go = "1.26.4"
|
||||||
|
node = "24"
|
||||||
|
pnpm = "11.8.0"
|
||||||
|
|
||||||
|
# Dev tools
|
||||||
|
golangci-lint = "2.12.2"
|
||||||
|
mockery = "3.7.1"
|
||||||
|
gotestsum = "latest"
|
||||||
|
goreleaser = "2"
|
||||||
|
"go:golang.org/x/exp/cmd/gorelease" = "latest"
|
||||||
12
requires.go
12
requires.go
@@ -3,6 +3,8 @@ package task
|
|||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/elliotchance/orderedmap/v3"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
"github.com/go-task/task/v3/internal/input"
|
"github.com/go-task/task/v3/internal/input"
|
||||||
"github.com/go-task/task/v3/internal/term"
|
"github.com/go-task/task/v3/internal/term"
|
||||||
@@ -32,7 +34,7 @@ func (e *Executor) promptDepsVars(calls []*Call) error {
|
|||||||
|
|
||||||
// Collect all missing vars from the dependency tree
|
// Collect all missing vars from the dependency tree
|
||||||
visited := make(map[string]bool)
|
visited := make(map[string]bool)
|
||||||
varsMap := make(map[string]*ast.VarsWithValidation)
|
varsMap := orderedmap.NewOrderedMap[string, *ast.VarsWithValidation]()
|
||||||
|
|
||||||
var collect func(call *Call) error
|
var collect func(call *Call) error
|
||||||
collect = func(call *Call) error {
|
collect = func(call *Call) error {
|
||||||
@@ -42,8 +44,8 @@ func (e *Executor) promptDepsVars(calls []*Call) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range getMissingRequiredVars(compiledTask) {
|
for _, v := range getMissingRequiredVars(compiledTask) {
|
||||||
if _, exists := varsMap[v.Name]; !exists {
|
if !varsMap.Has(v.Name) {
|
||||||
varsMap[v.Name] = v
|
varsMap.Set(v.Name, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +75,14 @@ func (e *Executor) promptDepsVars(calls []*Call) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(varsMap) == 0 {
|
if varsMap.Len() == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
prompter := e.newPrompter()
|
prompter := e.newPrompter()
|
||||||
e.promptedVars = ast.NewVars()
|
e.promptedVars = ast.NewVars()
|
||||||
|
|
||||||
for _, v := range varsMap {
|
for v := range varsMap.Values() {
|
||||||
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
|
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, input.ErrCancelled) {
|
if errors.Is(err, input.ErrCancelled) {
|
||||||
|
|||||||
4
task.go
4
task.go
@@ -349,6 +349,8 @@ 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)
|
||||||
@@ -393,7 +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) {
|
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.Cmd)
|
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.LogCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Dry {
|
if e.Dry {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
|
|
||||||
// Cmd is a task command
|
// Cmd is a task command
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
Cmd string
|
Cmd string // Resolved command (used for execution and fingerprinting)
|
||||||
|
LogCmd string // Command with secrets masked (used for logging)
|
||||||
Task string
|
Task string
|
||||||
For *For
|
For *For
|
||||||
If string
|
If string
|
||||||
@@ -28,6 +29,7 @@ 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,
|
||||||
|
|||||||
@@ -93,6 +93,21 @@ func (matrix *Matrix) DeepCopy() *Matrix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopy returns a copy of the MatrixRow. Without this, deepcopy.OrderedMap
|
||||||
|
// falls back to copying the *MatrixRow pointer as-is, so every "copy" of a
|
||||||
|
// Matrix would still share the same underlying rows - see #2890, where
|
||||||
|
// concurrent invocations of a task with a `ref:` matrix row raced on
|
||||||
|
// resolveMatrixRefs mutating that shared row.
|
||||||
|
func (row *MatrixRow) DeepCopy() *MatrixRow {
|
||||||
|
if row == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &MatrixRow{
|
||||||
|
Ref: row.Ref,
|
||||||
|
Value: deepcopy.Slice(row.Value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
|
func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
|
||||||
switch node.Kind {
|
switch node.Kind {
|
||||||
case yaml.MappingNode:
|
case yaml.MappingNode:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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 {
|
||||||
@@ -23,21 +24,29 @@ 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":
|
case "sh", "ref", "map", "value":
|
||||||
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
|
||||||
|
// 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:
|
default:
|
||||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
|
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map", "value" or using a scalar value`, key)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
var value any
|
var value any
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func NewNode(
|
|||||||
return node, err
|
return node, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsRemoteEntrypoint(entrypoint string) bool {
|
func isRemoteEntrypoint(entrypoint string) bool {
|
||||||
scheme, _ := getScheme(entrypoint)
|
scheme, _ := getScheme(entrypoint)
|
||||||
switch scheme {
|
switch scheme {
|
||||||
case "git", "http", "https":
|
case "git", "http", "https":
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (node *FileNode) Read() ([]byte, error) {
|
|||||||
|
|
||||||
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if IsRemoteEntrypoint(entrypoint) {
|
if isRemoteEntrypoint(entrypoint) {
|
||||||
return entrypoint, nil
|
return entrypoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
|
|||||||
|
|
||||||
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if IsRemoteEntrypoint(entrypoint) {
|
if isRemoteEntrypoint(entrypoint) {
|
||||||
return entrypoint, nil
|
return entrypoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
|
|||||||
|
|
||||||
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if IsRemoteEntrypoint(entrypoint) {
|
if isRemoteEntrypoint(entrypoint) {
|
||||||
return entrypoint, nil
|
return entrypoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
testdata/secrets/Taskfile.yml
vendored
Normal file
65
testdata/secrets/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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}}"
|
||||||
6
testdata/secrets/testdata/TestSecretVars-deferred_command_with_secrets.golden
vendored
Normal file
6
testdata/secrets/testdata/TestSecretVars-deferred_command_with_secrets.golden
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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
|
||||||
6
testdata/secrets/testdata/TestSecretVars-env_secret_limitation.golden
vendored
Normal file
6
testdata/secrets/testdata/TestSecretVars-env_secret_limitation.golden
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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
|
||||||
2
testdata/secrets/testdata/TestSecretVars-mixed_secret_and_public_vars.golden
vendored
Normal file
2
testdata/secrets/testdata/TestSecretVars-mixed_secret_and_public_vars.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
task: [test-mixed] echo "App=myapp Secret=***** URL=https://example.com"
|
||||||
|
App=myapp Secret=task-level-secret URL=https://example.com
|
||||||
2
testdata/secrets/testdata/TestSecretVars-multiple_secrets_masked.golden
vendored
Normal file
2
testdata/secrets/testdata/TestSecretVars-multiple_secrets_masked.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
task: [test-multiple-secrets] echo "API=***** PWD=*****"
|
||||||
|
API=secret-api-key-123 PWD=my-super-secret-password
|
||||||
8
testdata/secrets/testdata/TestSecretVars-secret_vars_are_masked_in_logs.golden
vendored
Normal file
8
testdata/secrets/testdata/TestSecretVars-secret_vars_are_masked_in_logs.golden
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
38
variables.go
38
variables.go
@@ -239,6 +239,8 @@ 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)
|
||||||
@@ -254,6 +256,8 @@ 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)
|
||||||
@@ -347,13 +351,14 @@ func itemsFromFor(
|
|||||||
var values []any // The list of values to loop over
|
var values []any // The list of values to loop over
|
||||||
// Get the list from a matrix
|
// Get the list from a matrix
|
||||||
if f.Matrix.Len() != 0 {
|
if f.Matrix.Len() != 0 {
|
||||||
if err := resolveMatrixRefs(f.Matrix, cache); err != nil {
|
resolvedMatrix, err := resolveMatrixRefs(f.Matrix, cache)
|
||||||
|
if err != nil {
|
||||||
return nil, nil, errors.TaskfileInvalidError{
|
return nil, nil, errors.TaskfileInvalidError{
|
||||||
URI: location.Taskfile,
|
URI: location.Taskfile,
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return asAnySlice(product(f.Matrix)), nil, nil
|
return asAnySlice(product(resolvedMatrix)), nil, nil
|
||||||
}
|
}
|
||||||
// Get the list from the explicit for list
|
// Get the list from the explicit for list
|
||||||
if len(f.List) > 0 {
|
if len(f.List) > 0 {
|
||||||
@@ -425,22 +430,43 @@ func itemsFromFor(
|
|||||||
return values, keys, nil
|
return values, keys, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) error {
|
// resolveMatrixRefs resolves any `ref:` rows in matrix and returns a new
|
||||||
|
// Matrix with those rows populated. It must not mutate the matrix passed in:
|
||||||
|
// that matrix is part of the shared, cached Task AST, and concurrent
|
||||||
|
// invocations of the same task (e.g. via parallel deps) call this with the
|
||||||
|
// same *ast.Matrix and would otherwise race on the row.Value assignment
|
||||||
|
// below, intermittently leaking a value resolved for one caller into another
|
||||||
|
// caller's expansion. See #2890.
|
||||||
|
func resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) (*ast.Matrix, error) {
|
||||||
if matrix.Len() == 0 {
|
if matrix.Len() == 0 {
|
||||||
return nil
|
return matrix, nil
|
||||||
}
|
}
|
||||||
|
hasRef := false
|
||||||
for _, row := range matrix.All() {
|
for _, row := range matrix.All() {
|
||||||
|
if row.Ref != "" {
|
||||||
|
hasRef = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRef {
|
||||||
|
return matrix, nil
|
||||||
|
}
|
||||||
|
resolved := matrix.DeepCopy()
|
||||||
|
for _, row := range resolved.All() {
|
||||||
if row.Ref != "" {
|
if row.Ref != "" {
|
||||||
v := templater.ResolveRef(row.Ref, cache)
|
v := templater.ResolveRef(row.Ref, cache)
|
||||||
|
if cache.Err() != nil {
|
||||||
|
return nil, cache.Err()
|
||||||
|
}
|
||||||
switch value := v.(type) {
|
switch value := v.(type) {
|
||||||
case []any:
|
case []any:
|
||||||
row.Value = value
|
row.Value = value
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("matrix reference %q must resolve to a list", row.Ref)
|
return nil, fmt.Errorf("matrix reference %q must resolve to a list", row.Ref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return resolved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveEnumRefs(requires *ast.Requires, cache *templater.Cache) error {
|
func resolveEnumRefs(requires *ast.Requires, cache *templater.Cache) error {
|
||||||
|
|||||||
45
variables_test.go
Normal file
45
variables_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/internal/templater"
|
||||||
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestResolveMatrixRefsDoesNotMutateInput is a regression test for #2890. The
|
||||||
|
// *ast.Matrix passed to resolveMatrixRefs is part of the shared, cached Task
|
||||||
|
// AST: the same *ast.Matrix is reused on every invocation of a task. If
|
||||||
|
// resolveMatrixRefs resolved `ref:` rows in place, concurrent invocations of
|
||||||
|
// the same task (e.g. via parallel deps) would race on that mutation and leak
|
||||||
|
// a value resolved for one caller into another caller's expansion.
|
||||||
|
//
|
||||||
|
// The invariant that prevents this is that resolveMatrixRefs must resolve into
|
||||||
|
// a copy and leave its input untouched, which this test asserts deterministically.
|
||||||
|
func TestResolveMatrixRefsDoesNotMutateInput(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
matrix := ast.NewMatrix(
|
||||||
|
&ast.MatrixElement{Key: "ARCH", Value: &ast.MatrixRow{Ref: ".ARCH_VAR"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
vars := ast.NewVars()
|
||||||
|
vars.Set("ARCH_VAR", ast.Var{Value: []any{"amd64"}})
|
||||||
|
cache := &templater.Cache{Vars: vars}
|
||||||
|
|
||||||
|
resolved, err := resolveMatrixRefs(matrix, cache)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The returned matrix has the ref resolved...
|
||||||
|
row, ok := resolved.Get("ARCH")
|
||||||
|
require.True(t, ok, "ARCH row missing from resolved matrix")
|
||||||
|
require.Equal(t, []any{"amd64"}, row.Value)
|
||||||
|
|
||||||
|
// ...but the shared input matrix must be left untouched.
|
||||||
|
orig, ok := matrix.Get("ARCH")
|
||||||
|
require.True(t, ok, "ARCH row missing from input matrix")
|
||||||
|
require.Nil(t, orig.Value, "input matrix was mutated: Ref rows must be resolved into a copy")
|
||||||
|
require.Equal(t, ".ARCH_VAR", orig.Ref, "input matrix Ref was altered")
|
||||||
|
}
|
||||||
@@ -23,5 +23,5 @@
|
|||||||
"vitepress-plugin-llms": "^1.9.1",
|
"vitepress-plugin-llms": "^1.9.1",
|
||||||
"vue": "^3.5.18"
|
"vue": "^3.5.18"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499"
|
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
|
||||||
}
|
}
|
||||||
|
|||||||
1156
website/pnpm-lock.yaml
generated
1156
website/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,19 @@ a human. Always remind contributors to disclose AI usage in their submissions.
|
|||||||
|
|
||||||
## 1. Setup
|
## 1. Setup
|
||||||
|
|
||||||
|
The easiest way to install everything you need to work on Task is
|
||||||
|
[mise][mise]. From the repository root, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise install
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the pinned versions of Go, Node.js, pnpm and the dev tools
|
||||||
|
(`golangci-lint`, `mockery`, `gotestsum`, `goreleaser` and `gorelease`)
|
||||||
|
declared in the `mise.toml` file.
|
||||||
|
|
||||||
|
If you'd rather install things manually, you'll need:
|
||||||
|
|
||||||
- **Go** - Task is written in [Go][go]. We always support the latest two major
|
- **Go** - Task is written in [Go][go]. We always support the latest two major
|
||||||
Go versions, so make sure your version is recent enough.
|
Go versions, so make sure your version is recent enough.
|
||||||
- **Node.js** - [Node.js][nodejs] is used to host Task's documentation server
|
- **Node.js** - [Node.js][nodejs] is used to host Task's documentation server
|
||||||
@@ -194,6 +207,7 @@ If you have questions, feel free to ask them in the `#help` forum channel on our
|
|||||||
[prettier]: https://prettier.io
|
[prettier]: https://prettier.io
|
||||||
[nodejs]: https://nodejs.org/en/
|
[nodejs]: https://nodejs.org/en/
|
||||||
[pnpm]: https://pnpm.io/
|
[pnpm]: https://pnpm.io/
|
||||||
|
[mise]: https://mise.jdx.dev
|
||||||
[vitepress]: https://vitepress.dev
|
[vitepress]: https://vitepress.dev
|
||||||
[json-schema]:
|
[json-schema]:
|
||||||
https://github.com/go-task/task/blob/main/website/src/public/schema.json
|
https://github.com/go-task/task/blob/main/website/src/public/schema.json
|
||||||
|
|||||||
@@ -190,21 +190,6 @@ includes:
|
|||||||
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
|
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Special Variables
|
|
||||||
|
|
||||||
The file-path [special variables](../reference/templating.md#file-paths) behave
|
|
||||||
differently when a Taskfile is loaded from a remote source, because there is no
|
|
||||||
local file or directory that corresponds 1:1 to the Taskfile:
|
|
||||||
|
|
||||||
| Variable | Value when loaded remotely |
|
|
||||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| `TASKFILE` / `ROOT_TASKFILE` | The original URL, unchanged |
|
|
||||||
| `TASKFILE_DIR` / `ROOT_DIR` | Empty string — a directory variable cannot point to a URL |
|
|
||||||
| `TASK_DIR` | Resolved against `USER_WORKING_DIR` (relative `dir:` → joined with `USER_WORKING_DIR`, empty `dir:` → `USER_WORKING_DIR`, absolute `dir:` → kept as-is) |
|
|
||||||
|
|
||||||
If a remote Taskfile includes a local Taskfile (or vice-versa), each variable
|
|
||||||
reflects the source of the Taskfile it refers to.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
### Automatic checksums
|
### Automatic checksums
|
||||||
|
|||||||
@@ -944,7 +944,7 @@ Also, `task --status [tasks]...` will exit with a non-zero
|
|||||||
|
|
||||||
`status` can be combined with the
|
`status` can be combined with the
|
||||||
[fingerprinting](#by-fingerprinting-locally-generated-files-and-their-sources)
|
[fingerprinting](#by-fingerprinting-locally-generated-files-and-their-sources)
|
||||||
to have a task run if either the the source/generated artifacts changes, or the
|
to have a task run if either the source/generated artifacts changes, or the
|
||||||
programmatic check fails:
|
programmatic check fails:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -1614,6 +1614,163 @@ 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.
|
||||||
|
|||||||
@@ -385,6 +385,33 @@ 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:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "Taskfile YAML Schema",
|
"title": "Taskfile YAML Schema",
|
||||||
"description": "Schema for Taskfile files.",
|
"description": "Schema for Taskfile files.",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
@@ -318,6 +318,10 @@
|
|||||||
"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