Compare commits

...

64 Commits

Author SHA1 Message Date
Pete Davison
a61f8ade36 feat: update changelog for #2847 2026-06-29 18:27:59 +00:00
Valentin Maerten
9910c33557 fix(remote): define special variables behavior (#2847) 2026-06-29 19:25:25 +01:00
Markus
b93897a6c3 docs: clarify join argument order in templating reference (#2887)
Co-authored-by: Markus Stark <markusstark@MacBook-Air-von-Markus.local>
2026-06-29 17:08:44 +02:00
Valentin Maerten
7fa9d657cd feat(completion): complete task aliases in zsh (#2865) 2026-06-29 14:21:47 +00:00
kjasn
1c743de2b7 feat: Add temp dir option (#2891)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-06-29 14:13:37 +00:00
Valentin Maerten
f9e52fab40 chore: changelog for #2873 2026-06-29 15:50:50 +02:00
renovate[bot]
80693c1208 chore(deps): update pnpm/action-setup digest to 0ebf471 (#2888) 2026-06-29 15:50:06 +02:00
Or Carmi
d6cc0ce070 feat: configure output flags via environment variables (#2873)
Co-authored-by: Or Carmi <ocarmi@paloaltonetworks.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-06-29 13:48:37 +00:00
Valentin Maerten
d4dc3dc448 chore: changelog for #2773 2026-06-29 15:42:22 +02:00
Valentin Maerten
616433df75 feat: add use_gitignore option to exclude ignored files from sources/generates (#2773) 2026-06-29 15:39:33 +02:00
Valentin Maerten
a03aa7ba69 chore: changelog for #2514 2026-06-29 14:53:00 +02:00
Valentin Maerten
6abbbcb265 feat: do not log secret variables (#2514) 2026-06-29 14:50:08 +02:00
renovate[bot]
c73d53f4e9 chore(deps): update actions/checkout action to v7 (#2896) 2026-06-29 10:06:34 +02:00
Valentin Maerten
d0b903c772 ci: remove needs-build PR workflow (#2895) 2026-06-28 20:16:31 +00:00
Valentin Maerten
1d9b3cb7db chore: changelog for #2894 2026-06-28 22:03:20 +02:00
Amit Mishra
1a0f146888 fix: don't mutate shared matrix AST when resolving ref: rows (#2894)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-06-28 20:03:11 +00:00
Vessel9817
b9b50ca79c fix: $schema according to schemastore standards (#2893) 2026-06-28 19:18:31 +00:00
Valentin Maerten
3dcaa7db89 chore: changelog for #2886 2026-06-21 16:25:14 +02:00
renovate[bot]
a72eb84c15 chore(deps): update all non-major dependencies (#2869) 2026-06-21 16:24:20 +02:00
SEONGHYUN HONG
91b9e42f17 fix: sanitize all invalid characters in checksum filenames (#2886) 2026-06-21 16:23:45 +02:00
SEONGHYUN HONG
f1ab404fbb docs: fix duplicated word in guide (#2885)
Signed-off-by: s3onghyun <s3onghyun.hong@gmail.com>
2026-06-21 14:19:20 +00:00
Valentin Maerten
24a3ccdf42 chore: changelog for #2871 2026-06-07 16:39:02 +02:00
Christian Provenzano
b455286b63 fix: deterministic ordering of required var prompts (#2871) 2026-06-07 16:36:58 +02:00
Valentin Maerten
81d621d8aa chore: add mise config for dev environment setup (#2863)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2026-06-07 14:46:01 +02:00
dependabot[bot]
f50327f2a2 chore(deps): bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /website (#2846)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-31 11:13:28 +02:00
Valentin Maerten
dd810d42e1 chore: changelog for #2852 2026-05-31 11:08:42 +02:00
Koji Ishii
de1717b05f feat(completion): ps completion supports aliases (#2852)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-31 09:06:39 +00:00
Valentin Maerten
7766e8add5 chore: changelog for #2859 2026-05-31 11:03:32 +02:00
Zhaowei Li
e61d0f0f7c fix: install fish completions to vendor directory (#2859) 2026-05-31 11:01:13 +02:00
renovate[bot]
9c8799480d chore(deps): update goreleaser/goreleaser-action digest to 5daf1e9 (#2857)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-31 08:47:42 +00:00
renovate[bot]
7f04f7057c chore(deps): update all non-major dependencies (#2858)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-31 10:38:31 +02:00
renovate[bot]
48dd62cdc1 chore(deps): update go-task/setup-task digest to 01a4adf (#2848) 2026-05-18 15:37:33 +02:00
renovate[bot]
b27a6653f6 chore(deps): update all non-major dependencies (#2816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-17 12:40:51 +02:00
renovate[bot]
76269b03c4 chore(deps): update pnpm to v11 (#2844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-17 12:20:33 +02:00
renovate[bot]
ed492ac862 chore(deps): update pnpm/action-setup digest to 0e279bb (#2843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 11:59:56 +02:00
dependabot[bot]
1d385ad5b4 chore(deps): bump vite from 5.4.19 to 5.4.21 in /website (#2841)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 11:59:42 +02:00
Andrey Nering
2eff3545d9 ci(goreleaser): add missing env to workflow 2026-05-16 18:09:51 -03:00
Andrey Nering
bd79c29a39 v3.51.1 2026-05-16 17:51:05 -03:00
Andrey Nering
136e0dae89 ci: fix goreleaser 2026-05-16 17:50:34 -03:00
Andrey Nering
41a2137044 v3.51.0 2026-05-16 17:27:25 -03:00
imgbot[bot]
aea7f7a713 docs: optimize images (#2690) 2026-05-16 17:23:46 -03:00
renovate[bot]
c3e16b0a68 chore(deps): update mvdan.cc/sh/v3 digest to f5c6e27 (#2834) 2026-05-16 17:20:20 -03:00
renovate[bot]
19699a1f0e chore(deps): update pnpm/action-setup digest to 739bfe4 (#2835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 16:27:48 +02:00
Timothy Rule
9e3ff27ba1 fix: avoid panic after calling WordsSeq (#2810) 2026-05-09 21:42:00 +00:00
renovate[bot]
de05ebc503 chore(deps): update goreleaser/goreleaser-action digest to 1a80836 (#2815) 2026-05-09 21:34:39 +00:00
Andrey Nering
6e154f2b71 docs(changelog): add entry for #2832 2026-05-09 18:28:46 -03:00
Andrey Nering
9ba1e566a9 fix: update mvdan.cc/sh to fix regression
* Closes #2812
* Closes #2818
* Ref https://github.com/mvdan/sh/pull/1330
2026-05-09 18:28:46 -03:00
Andrey Nering
1b53d425ee ci(govulncheck): update golang.org/x/net 2026-05-09 11:45:53 -03:00
Andrey Nering
7b3559ce4c ci: fix build and lint builds 2026-05-09 11:45:53 -03:00
Andrey Nering
43dbeb4260 docs(changelog): add entry for #2406 #2408 2026-05-09 11:25:49 -03:00
Jerry Wiltse
d9e0e04725 feat: add joinEnv and joinUrl string functions and 2 new system vars (#2408) 2026-05-09 14:19:33 +00:00
Andrey Nering
629a10813f chore: update minimal go version to go 1.25.10 2026-05-09 11:19:02 -03:00
Andrey Nering
dc64864643 docs: add security > threat model page (#2830) 2026-05-09 13:59:36 +00:00
renovate[bot]
5f78da2d0a chore(deps): update dependency netlify-cli to v26 (#2823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 03:03:32 +02:00
renovate[bot]
7915b78ac2 chore(deps): update pnpm/action-setup action to v6 (#2824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 03:03:13 +02:00
Andrey Nering
3441bf522e docs(changelog): add entry for #2820 2026-05-01 10:03:53 -03:00
Roman Dahm
46f5db22c8 perf(templater): skip template engine for strings without {{ (#2820) 2026-05-01 09:57:16 -03:00
Ludovic Fernandez
ecffcc720f chore(golangci-lint): improve and clean configuration (#2808) 2026-04-23 21:13:15 -03:00
Valentin Maerten
be35b3af75 feat(website): highlight big orgs / projects that use Task (#2803) 2026-04-22 10:02:06 +02:00
renovate[bot]
3aaedc790d chore(deps): update goreleaser/goreleaser-action digest to e24998b (#2804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 22:49:24 +02:00
renovate[bot]
0b19d973ac chore(deps): update all non-major dependencies (#2805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 22:08:05 +02:00
Andreas **Felix** Häberle
70b6cd8ee0 docs: add call internal task within a task example (#2789)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-04-20 21:58:42 +02:00
Valentin Maerten
1eb5720e7e chore: changelog for #2788 2026-04-20 21:53:50 +02:00
Mateen Anjum
1b06da16f6 feat(templater): add absPath template function (#2788)
Signed-off-by: Mateen Anjum <mateenali66@gmail.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-04-20 21:50:03 +02:00
136 changed files with 5120 additions and 1724 deletions

View File

@@ -16,19 +16,19 @@ jobs:
name: Lint
strategy:
matrix:
go-version: [1.25.x, 1.26.x]
go-version: [1.25.10, 1.26.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
with:
version: v2.11.4
version: v2.12.2
lint-jsonschema:
runs-on: ubuntu-latest
@@ -37,7 +37,7 @@ jobs:
with:
python-version: 3.14
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'

View File

@@ -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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@ec59f474b9834571250b370d4735c50f8e2d1e29 # 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

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -23,7 +23,7 @@ jobs:
go-version: 1.26.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
with:
distribution: goreleaser-pro
version: latest

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
@@ -23,7 +23,7 @@ jobs:
with:
go-version: 1.26.x
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
@@ -32,22 +32,23 @@ jobs:
run: npm install -g npm@latest
- name: Install Task
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
package_json_file: "website/package.json"
run_install: "true"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
with:
distribution: goreleaser-pro
version: latest
args: release --clean --draft
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GH_GORELEASER_TOKEN: ${{secrets.GH_GORELEASER_TOKEN}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}

View File

@@ -17,12 +17,12 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: [1.25.x, 1.26.x]
go-version: [1.25.10, 1.26.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0

View File

@@ -2,33 +2,16 @@ version: "2"
formatters:
enable:
- gofmt
- gofumpt
- goimports
- gci
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
gofumpt:
module-path: github.com/go-task/task/v3
goimports:
local-prefixes:
- github.com/go-task
gci:
sections:
- standard
- default
- prefix(github.com/go-task)
- localmodule
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
linters:
enable:
@@ -36,6 +19,7 @@ linters:
- gosec
- mirror
- misspell
- modernize
- noctx
- paralleltest
- thelper
@@ -56,13 +40,8 @@ linters:
- pkg: errors
desc: Use github.com/go-task/task/v3/errors instead
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$

View File

@@ -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'

View File

@@ -76,7 +76,7 @@ nfpms:
- src: completion/bash/task.bash
dst: /etc/bash_completion.d/task
- 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
dst: /usr/local/share/zsh/site-functions/_task
@@ -89,7 +89,7 @@ brews:
repository:
owner: go-task
name: homebrew-tap
token: "{{secrets.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
test: system "#{bin}/task", "--help"
install: |-
bin.install "task"
@@ -131,7 +131,7 @@ winget:
owner: go-task
name: winget-pkgs
branch: 'task-{{.Version}}'
token: "{{secrets.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
pull_request:
enabled: true
draft: false

View File

@@ -5,6 +5,12 @@
"Taskfile.yaml",
"taskfile.yml",
"taskfile.yaml"
],
"./website/src/public/schema-taskrc.json": [
".taskrc.yml",
".taskrc.yaml",
"taskrc.yml",
"taskrc.yaml"
]
},
"gopls": {

View File

@@ -1,5 +1,53 @@
# 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 and namespace aliases not being completed by the Zsh completion. A
`show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
- 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).
- Added a `secret: true` flag for variables that masks their value in logs,
`task --summary`, and command output (#2514 by @vmaerten).
- Added the `use_gitignore` setting (global or per-task) to skip files matched
by your `.gitignore` when fingerprinting `sources`/`generates` and when
watching (#2773 by @vmaerten).
- Added support for configuring output flags (`--output`,
`--output-group-begin`, `--output-group-end`, `--output-group-error-only`) via
the `TASK_OUTPUT*` environment variables (#2873 by @liiight).
- Added a `--temp-dir` flag (with `TASK_TEMP_DIR` env var and `temp-dir` taskrc
config) to customise the directory where Task stores temporary files such as
checksums. Relative paths are resolved against the root Taskfile (#2891 by
@kjasn).
- Defined environment variable behavior for remote taskfiles (#2267, #2847 by
@vmaerten).
## v3.51.1 - 2026-05-16
- A significant performance boost was achieved for large Taskfiles (monorepos)
by skipping templating altogether when the string is static (#2820 by @romnn).
- Added `absPath` template function that resolves a path to its absolute form,
cleaning `..` and `.` components (#2681, #2788 by @mateenanjum).
- Added `joinEnv` function to join paths based on your oprating system: `;` for
Windows and `:` elsewhere, and `joinUrl` to join URL paths. Also, added two
new special variables: `FILE_PATH_SEPARATOR` which returns `\` on Windows and
`/` elsewhere, and `PATH_LIST_SEPARATOR` which returns `;` on Windows and `:`
elsewhere (#2406, #2408 by @solvingj).
- Update the shell interpreter with a regression fix (#2812, #2832 by
@andreynering).
- Fix potential panic with the shell interpreter (#2810 by @trulede).
## v3.50.0 - 2026-04-13
- Added `enum.ref` support in `requires`: enum constraints can now reference
@@ -11,13 +59,13 @@
- Fixed watch mode ignoring SIGHUP signal, causing the watcher to exit instead
of restarting (#2764, #2642).
- 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.
This makes `method: timestamp` behaves the same as `method: checksum`
(#1230, #2716 by @drichardson).
`method: timestamp` and the files listed on `generates:` were deleted. This
makes `method: timestamp` behaves the same as `method: checksum` (#1230, #2716
by @drichardson).
## 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).
## v3.49.0 - 2026-03-07

View File

@@ -15,6 +15,7 @@ import (
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -51,7 +52,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
return nil, err
}
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 {
@@ -63,12 +64,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
// Preserve the Sh field so it can be displayed in summary
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
}
// If the variable should not be evaluated and it is set, we can set it and return
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
}
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
@@ -77,7 +78,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 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
}
// If the variable is dynamic, we need to resolve it first
@@ -85,7 +86,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
if err != nil {
return err
}
result.Set(k, ast.Var{Value: static})
result.Set(k, ast.Var{Value: static, Secret: v.Secret})
return nil
}
}
@@ -184,7 +185,12 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string,
result = strings.TrimSuffix(result, "\n")
c.dynamicCache[*v.Sh] = result
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
// Never print the resolved value of a secret variable, even in verbose mode
logResult := result
if v.Secret {
logResult = "*****"
}
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, logResult)
return result, nil
}
@@ -201,18 +207,42 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
// across platforms. This prevents issues with backslashes being interpreted
// 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{
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
"ROOT_DIR": filepath.ToSlash(c.Dir),
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
"TASK_VERSION": version.GetVersion(),
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": rootTaskfile,
"ROOT_DIR": rootDir,
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
"TASK_VERSION": version.GetVersion(),
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
"FILE_PATH_SEPARATOR": string(os.PathSeparator),
}
if t != nil {
allVars["TASK"] = t.Task
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
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["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
}
} else {
allVars["TASK"] = ""
allVars["TASK_DIR"] = ""

135
compiler_internal_test.go Normal file
View File

@@ -0,0 +1,135 @@
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")
})
}
}

View File

@@ -1,6 +1,8 @@
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)
if ($commandName.StartsWith('-')) {

View File

@@ -13,9 +13,9 @@ function __task_is_experiment_enabled() {
# Listing commands from Taskfile.yml
function __task_list() {
local -a scripts cmd
local -a scripts cmd task_aliases match mbegin mend
local -i enabled=0
local taskfile item task desc
local taskfile item task desc task_alias
cmd=($TASK_CMD)
taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
@@ -50,14 +50,31 @@ function __task_list() {
local show_desc
zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false
# Read zstyle show-aliases option (default = true via -T)
local show_aliases
zstyle -T ":completion:${curcontext}:" show-aliases && show_aliases=true || show_aliases=false
for item in "${(@)${(f)output}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
# Extract the aliases listed in the trailing "(aliases: a, b)" column.
# NB: `aliases` is a reserved zsh parameter, so use a different name.
task_aliases=()
if [[ "$show_aliases" == "true" && "$item" == (#b)*'(aliases: '(*)')' ]]; then
task_aliases=( "${(@s:, :)match[1]}" )
fi
if [[ "$show_desc" == "true" ]]; then
local desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
for task_alias in $task_aliases; do
scripts+=( "${task_alias//:/\\:}:$desc (alias of $task)" )
done
else
scripts+=( "$task" )
for task_alias in $task_aliases; do
scripts+=( "$task_alias" )
done
fi
done

View File

@@ -29,6 +29,7 @@ type (
Dir string
Entrypoint string
TempDir TempDir
TempDirPath string
Force bool
ForceAll bool
Insecure bool
@@ -165,6 +166,22 @@ func (o *tempDirOption) ApplyToExecutor(e *Executor) {
e.TempDir = o.tempDir
}
// WithTempDirPath sets an unresolved path used to build [Executor.TempDir]
// during [Executor.Setup]. Relative paths are resolved from the root Taskfile
// directory. Use [WithTempDir] when the remote and fingerprint directories have
// already been resolved.
func WithTempDirPath(path string) ExecutorOption {
return &tempDirPathOption{path: path}
}
type tempDirPathOption struct {
path string
}
func (o *tempDirPathOption) ApplyToExecutor(e *Executor) {
e.TempDirPath = o.path
}
// WithForce ensures that the [Executor] always runs a task, even when
// fingerprinting or prompts would normally stop it.
func WithForce(force bool) ExecutorOption {

View File

@@ -283,6 +283,68 @@ 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) {
t.Parallel()
NewExecutorTest(t,

43
go.mod
View File

@@ -1,37 +1,37 @@
module github.com/go-task/task/v3
go 1.25.8
go 1.25.10
require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.2
charm.land/lipgloss/v2 v2.0.2
charm.land/bubbletea/v2 v2.0.7
charm.land/lipgloss/v2 v2.0.4
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/Masterminds/semver/v3 v3.5.0
github.com/alecthomas/chroma/v2 v2.27.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.19.0
github.com/fsnotify/fsnotify v1.9.0
github.com/fsnotify/fsnotify v1.10.1
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-getter v1.8.6
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/puzpuzpuz/xsync/v4 v4.4.0
github.com/puzpuzpuz/xsync/v4 v4.5.0
github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.8.0
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/zeebo/xxh3 v1.1.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.20.0
golang.org/x/term v0.42.0
golang.org/x/sync v0.21.0
golang.org/x/term v0.44.0
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
mvdan.cc/sh/v3 v3.13.1
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117
)
require (
@@ -68,16 +68,16 @@ require (
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dlclark/regexp2/v2 v2.2.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/protoc-gen-validate v1.3.0 // indirect
@@ -92,12 +92,12 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
@@ -120,11 +120,12 @@ require (
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect

100
go.sum
View File

@@ -2,10 +2,14 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
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.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/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/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
@@ -36,12 +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/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
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/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/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.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
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/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@@ -92,12 +98,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
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/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/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/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@@ -118,8 +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.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/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
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/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -138,8 +148,8 @@ 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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
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/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/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -181,8 +191,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -192,14 +202,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
@@ -214,8 +224,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -272,27 +282,29 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
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/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
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.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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
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/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/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
@@ -319,5 +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=
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/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
mvdan.cc/sh/v3 v3.13.1/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/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=

View File

@@ -81,7 +81,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
traverseFunc = func(copy, v reflect.Value) error {
switch v.Kind() {
case reflect.Ptr:
case reflect.Pointer:
// Unwrap the pointer
originalValue := v.Elem()
// If the pointer is nil, do nothing

View File

@@ -127,7 +127,10 @@ func ExpandFields(s string) ([]string, error) {
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
for w := range p.WordsSeq(strings.NewReader(s)) {
for w, err := range p.WordsSeq(strings.NewReader(s)) {
if err != nil {
return nil, err
}
words = append(words, w)
}
cfg := &expand.Config{

View File

@@ -0,0 +1,211 @@
package fingerprint
import (
"bufio"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/go-task/task/v3/internal/gitignore"
)
type linesCacheEntry struct {
mtime time.Time
lines []string
}
var (
gitignoreLinesCache sync.Map // dir -> linesCacheEntry, invalidated by mtime
repoRootCache sync.Map // dir -> repo root (or "" when not in a repo)
)
// findRepoRoot returns the first ancestor of dir containing a .git entry, or
// ("", false) when dir is not inside a git repository.
func findRepoRoot(dir string) (string, bool) {
if v, ok := repoRootCache.Load(dir); ok {
root := v.(string)
return root, root != ""
}
current := dir
for {
if _, err := os.Stat(filepath.Join(current, ".git")); err == nil {
repoRootCache.Store(dir, current)
return current, true
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
repoRootCache.Store(dir, "")
return "", false
}
// filterGitignored marks entries matching gitignore rules as excluded (false).
// All .gitignore files from the repo root down to each candidate file's
// directory feed a single matcher so that precedence and cross-file negations
// (`!pattern`) resolve correctly.
func filterGitignored(files map[string]bool, dir string) map[string]bool {
if len(files) == 0 {
return files
}
absDir, err := filepath.Abs(dir)
if err != nil {
return files
}
repoRoot, ok := findRepoRoot(absDir)
if !ok {
return files
}
// Every directory from the repo root down to each candidate file's dir, so
// nested .gitignore files reached by deep globs are included too.
dirSet := make(map[string]struct{})
for path, included := range files {
if !included {
continue
}
absPath, err := filepath.Abs(path)
if err != nil {
continue
}
d := filepath.Dir(absPath)
if !withinRepo(repoRoot, d) {
continue
}
for {
dirSet[d] = struct{}{}
if d == repoRoot {
break
}
parent := filepath.Dir(d)
if parent == d {
break
}
d = parent
}
}
// Shallow dirs first (lower priority): the matcher scans patterns last to
// first, so deeper rules win and can negate shallower ones.
dirs := make([]string, 0, len(dirSet))
for d := range dirSet {
dirs = append(dirs, d)
}
sort.Slice(dirs, func(i, j int) bool {
di := strings.Count(dirs[i], string(filepath.Separator))
dj := strings.Count(dirs[j], string(filepath.Separator))
if di != dj {
return di < dj
}
return dirs[i] < dirs[j]
})
var patterns []gitignore.Pattern
for _, d := range dirs {
lines := readGitignoreLines(d)
if len(lines) == 0 {
continue
}
// domain scopes each pattern to its .gitignore subtree (go-git semantics).
var domain []string
if rel, err := filepath.Rel(repoRoot, d); err == nil && rel != "." {
domain = strings.Split(filepath.ToSlash(rel), "/")
}
for _, line := range lines {
patterns = append(patterns, gitignore.ParsePattern(line, domain))
}
}
if len(patterns) == 0 {
return files
}
matcher := gitignore.NewMatcher(patterns)
for path, included := range files {
if !included {
continue
}
absPath, err := filepath.Abs(path)
if err != nil {
continue
}
relPath, err := filepath.Rel(repoRoot, absPath)
if err != nil || relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) {
continue
}
if ignored(matcher, strings.Split(filepath.ToSlash(relPath), "/")) {
files[path] = false
}
}
return files
}
// ignored honors Git's rule that a file under an ignored directory cannot be
// re-included by a deeper negation: if any ancestor directory is ignored, so is
// the file. Otherwise the file's own verdict applies (isDir=false).
func ignored(matcher gitignore.Matcher, segments []string) bool {
for i := 1; i < len(segments); i++ {
if matcher.Match(segments[:i], true) {
return true
}
}
return matcher.Match(segments, false)
}
func withinRepo(repoRoot, p string) bool {
rel, err := filepath.Rel(repoRoot, p)
if err != nil {
return false
}
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
}
// readGitignoreLines returns the .gitignore lines in dir (nil if none), cached
// per directory and invalidated by mtime so watch mode picks up edits.
func readGitignoreLines(dir string) []string {
path := filepath.Join(dir, ".gitignore")
info, err := os.Stat(path)
if err != nil {
return nil
}
mtime := info.ModTime()
if v, ok := gitignoreLinesCache.Load(dir); ok {
entry := v.(linesCacheEntry)
if entry.mtime.Equal(mtime) {
return entry.lines
}
}
lines := parseGitignoreLines(path)
gitignoreLinesCache.Store(dir, linesCacheEntry{mtime: mtime, lines: lines})
return lines
}
func parseGitignoreLines(path string) []string {
f, err := os.Open(path)
if err != nil {
return nil
}
defer f.Close()
var lines []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimRight(scanner.Text(), "\r")
if line != "" && !strings.HasPrefix(line, "#") {
lines = append(lines, line)
}
}
// On a scan error (e.g. an over-long line) keep what was parsed rather than
// dropping the whole file.
return lines
}

View File

@@ -0,0 +1,254 @@
package fingerprint
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/taskfile/ast"
)
func initGitRepo(t *testing.T, dir string) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
}
func TestGlobsWithGitignore(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("ignored"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "also-included.txt"), []byte("also included"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
filesWithout, err := Globs(dir, globs, false)
require.NoError(t, err)
filesWith, err := Globs(dir, globs, true)
require.NoError(t, err)
hasLog := false
for _, f := range filesWithout {
if filepath.Base(f) == "ignored.log" {
hasLog = true
break
}
}
assert.True(t, hasLog, "ignored.log should be present without gitignore filter")
hasLog = false
for _, f := range filesWith {
if filepath.Base(f) == "ignored.log" {
hasLog = true
break
}
}
assert.False(t, hasLog, "ignored.log should be excluded with gitignore filter")
txtCount := 0
for _, f := range filesWith {
if filepath.Ext(f) == ".txt" {
txtCount++
}
}
assert.Equal(t, 2, txtCount, "both .txt files should remain")
}
func TestGlobsWithGitignoreParentDirIgnored(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
buildDir := filepath.Join(dir, "build")
require.NoError(t, os.MkdirAll(buildDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(buildDir, "keep.txt"), []byte("keep"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(buildDir, "other.txt"), []byte("other"), 0o644))
// Git cannot re-include a file under an ignored directory.
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("build/\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(buildDir, ".gitignore"), []byte("!keep.txt\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./**/*"},
}
files, err := Globs(dir, globs, true)
require.NoError(t, err)
for _, f := range files {
base := filepath.Base(f)
assert.NotEqual(t, "keep.txt", base, "keep.txt must stay excluded under ignored build/")
assert.NotEqual(t, "other.txt", base, "other.txt must stay excluded under ignored build/")
}
}
func TestGlobsWithGitignoreNested(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
subDir := filepath.Join(dir, "sub")
require.NoError(t, os.MkdirAll(subDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "build.out"), []byte("build"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(subDir, globs, true)
require.NoError(t, err)
for _, f := range files {
assert.NotEqual(t, "build.out", filepath.Base(f), "build.out should be excluded by nested .gitignore")
}
}
func TestGlobsWithGitignoreCrossFileNegation(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
subDir := filepath.Join(dir, "sub")
require.NoError(t, os.MkdirAll(subDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "debug.log"), []byte("debug"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "other.log"), []byte("other"), 0o644))
// Root ignores all *.log; a nested .gitignore re-includes debug.log.
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("!debug.log\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(subDir, globs, true)
require.NoError(t, err)
hasDebug, hasOther := false, false
for _, f := range files {
switch filepath.Base(f) {
case "debug.log":
hasDebug = true
case "other.log":
hasOther = true
}
}
assert.True(t, hasDebug, "debug.log should be re-included by the nested negation")
assert.False(t, hasOther, "other.log should remain excluded by the root *.log rule")
}
func TestGlobsWithGitignoreDeepGlob(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
subDir := filepath.Join(dir, "sub")
require.NoError(t, os.MkdirAll(subDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "gen.out"), []byte("gen"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./**/*"},
}
files, err := Globs(dir, globs, true)
require.NoError(t, err)
for _, f := range files {
assert.NotEqual(t, "gen.out", filepath.Base(f), "gen.out should be excluded by the nested .gitignore reached via deep glob")
}
}
func TestGlobsWithGitignoreDoubleDotFile(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
// A ".."-prefixed name must not be skipped by the out-of-tree guard.
require.NoError(t, os.WriteFile(filepath.Join(dir, "..keep.log"), []byte("x"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "keep.txt"), []byte("y"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(dir, globs, true)
require.NoError(t, err)
for _, f := range files {
assert.NotEqual(t, "..keep.log", filepath.Base(f), "..keep.log should be excluded by *.log")
}
}
func TestGlobsWithGitignoreLongLine(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("x"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "keep.txt"), []byte("y"), 0o644))
// A line over bufio.Scanner's 64KB limit triggers a scan error; patterns
// parsed before it must survive.
longLine := strings.Repeat("a", 70*1024)
content := "*.log\n" + longLine + "\n"
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(content), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(dir, globs, true)
require.NoError(t, err)
for _, f := range files {
assert.NotEqual(t, "ignored.log", filepath.Base(f), "*.log parsed before the long line should still apply")
}
}
func TestGlobsWithGitignoreNoRepo(t *testing.T) {
t.Parallel()
// Cannot use t.TempDir() here because it creates a dir inside the
// go-task repo which has a .git parent, defeating the "no repo" test.
dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") //nolint:usetesting
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(dir) })
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(dir, globs, true)
require.NoError(t, err)
assert.Len(t, files, 1)
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)
func Globs(dir string, globs []*ast.Glob) ([]string, error) {
func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) {
resultMap := make(map[string]bool)
for _, g := range globs {
matches, err := glob(dir, g.Glob)
@@ -21,6 +21,11 @@ func Globs(dir string, globs []*ast.Glob) ([]string, error) {
resultMap[match] = !g.Negate
}
}
if useGitignore {
resultMap = filterGitignored(resultMap, dir)
}
return collectKeys(resultMap), nil
}

View File

@@ -89,7 +89,7 @@ func (*ChecksumChecker) Kind() string {
}
func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) {
sources, err := Globs(t.Dir, t.Sources)
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
if err != nil {
return "", err
}
@@ -119,7 +119,7 @@ func (checker *ChecksumChecker) checksumFilePath(t *ast.Task) string {
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 "-"
func normalizeFilename(f string) string {

View File

@@ -16,6 +16,10 @@ func TestNormalizeFilename(t *testing.T) {
{"foo/bar/baz", "foo-bar-baz"},
{"foo@bar/baz", "foo-bar-baz"},
{"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 {
assert.Equal(t, test.Out, normalizeFilename(test.In))

View File

@@ -28,7 +28,7 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
return false, nil
}
sources, err := Globs(t.Dir, t.Sources)
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
if err != nil {
return false, nil
}
@@ -54,7 +54,7 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
}
}
generates, err := Globs(t.Dir, t.Generates)
generates, err := Globs(t.Dir, t.Generates, t.ShouldUseGitignore())
if err != nil {
return false, nil
}
@@ -112,7 +112,7 @@ func (checker *TimestampChecker) Kind() string {
// Value implements the Checker Interface
func (checker *TimestampChecker) Value(t *ast.Task) (any, error) {
sources, err := Globs(t.Dir, t.Sources)
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
if err != nil {
return time.Now(), err
}

View File

@@ -87,6 +87,7 @@ var (
Cert string
CertKey string
Interactive bool
TempDir string
)
func init() {
@@ -143,10 +144,11 @@ func init() {
pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.")
pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.StringVar(&TempDir, "temp-dir", getConfig(config, "TEMP_DIR", func() *string { return config.TempDir }, ""), "Sets the directory used to store Task temporary files, such as checksums. Relative paths are relative to the root Taskfile.")
pflag.StringVarP(&Output.Name, "output", "o", getConfig(config, "OUTPUT", func() *string { return nil }, ""), "Sets output style: [interleaved|group|prefixed].")
pflag.StringVar(&Output.Group.Begin, "output-group-begin", getConfig(config, "OUTPUT_GROUP_BEGIN", func() *string { return nil }, ""), "Message template to print before a task's grouped output.")
pflag.StringVar(&Output.Group.End, "output-group-end", getConfig(config, "OUTPUT_GROUP_END", func() *string { return nil }, ""), "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", getConfig(config, "OUTPUT_GROUP_ERROR_ONLY", func() *bool { return nil }, false), "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", getConfig(config, "COLOR", func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, "CONCURRENCY", func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
@@ -308,6 +310,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithTaskSorter(sorter),
task.WithVersionCheck(true),
task.WithFailfast(Failfast),
task.WithTempDirPath(TempDir),
)
}

201
internal/gitignore/LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 Sourced Technologies, S.L.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,34 @@
// Vendored from go-git: github.com/go-git/go-git/v5 v5.19.1,
// plumbing/format/gitignore/matcher.go. Licensed under the Apache License 2.0;
// see the LICENSE file in this directory.
package gitignore
// Matcher defines a global multi-pattern matcher for gitignore patterns
type Matcher interface {
// Match matches patterns in the order of priorities. As soon as an inclusion or
// exclusion is found, not further matching is performed.
Match(path []string, isDir bool) bool
}
// NewMatcher constructs a new global matcher. Patterns must be given in the order of
// increasing priority. That is most generic settings files first, then the content of
// the repo .gitignore, then content of .gitignore down the path or the repo and then
// the content command line arguments.
func NewMatcher(ps []Pattern) Matcher {
return &matcher{ps}
}
type matcher struct {
patterns []Pattern
}
func (m *matcher) Match(path []string, isDir bool) bool {
n := len(m.patterns)
for i := n - 1; i >= 0; i-- {
if match := m.patterns[i].Match(path, isDir); match > NoMatch {
return match == Exclude
}
}
return false
}

View File

@@ -0,0 +1,23 @@
// Test cases ported from go-git: github.com/go-git/go-git/v5 v5.19.1,
// plumbing/format/gitignore/matcher_test.go. Licensed under the Apache
// License 2.0; see LICENSE.
package gitignore
import "testing"
func TestMatcher_Match(t *testing.T) {
t.Parallel()
m := NewMatcher([]Pattern{
ParsePattern("**/middle/v[uo]l?ano", nil),
ParsePattern("!volcano", nil),
})
if got := m.Match([]string{"head", "middle", "vulkano"}, false); got != true {
t.Errorf("Match(vulkano) = %t, want true", got)
}
if got := m.Match([]string{"head", "middle", "volcano"}, false); got != false {
t.Errorf("Match(volcano) = %t, want false (negated)", got)
}
}

View File

@@ -0,0 +1,165 @@
// Package gitignore implements gitignore pattern matching.
//
// This package is vendored from go-git:
//
// github.com/go-git/go-git/v5 v5.19.1, plumbing/format/gitignore
//
// Only the pattern parsing and matching logic (pattern.go and matcher.go) is
// copied; the file-walking helpers (dir.go) are omitted as they pull in
// go-billy and other go-git internals. The original code is licensed under the
// Apache License 2.0; see the LICENSE file in this directory.
package gitignore
import (
"path/filepath"
"strings"
)
// MatchResult defines outcomes of a match, no match, exclusion or inclusion.
type MatchResult int
const (
// NoMatch defines the no match outcome of a match check
NoMatch MatchResult = iota
// Exclude defines an exclusion of a file as a result of a match check
Exclude
// Include defines an explicit inclusion of a file as a result of a match check
Include
)
const (
inclusionPrefix = "!"
zeroToManyDirs = "**"
patternDirSep = "/"
)
// Pattern defines a single gitignore pattern.
type Pattern interface {
// Match matches the given path to the pattern.
Match(path []string, isDir bool) MatchResult
}
type pattern struct {
domain []string
pattern []string
inclusion bool
dirOnly bool
isGlob bool
}
// ParsePattern parses a gitignore pattern string into the Pattern structure.
func ParsePattern(p string, domain []string) Pattern {
// storing domain, copy it to ensure it isn't changed externally
domain = append([]string(nil), domain...)
res := pattern{domain: domain}
if strings.HasPrefix(p, inclusionPrefix) {
res.inclusion = true
p = p[1:]
}
if !strings.HasSuffix(p, "\\ ") {
p = strings.TrimRight(p, " ")
}
if strings.HasSuffix(p, patternDirSep) {
res.dirOnly = true
p = p[:len(p)-1]
}
if strings.Contains(p, patternDirSep) {
res.isGlob = true
}
res.pattern = strings.Split(p, patternDirSep)
return &res
}
func (p *pattern) Match(path []string, isDir bool) MatchResult {
if len(path) <= len(p.domain) {
return NoMatch
}
for i, e := range p.domain {
if path[i] != e {
return NoMatch
}
}
path = path[len(p.domain):]
if p.isGlob && !p.globMatch(path, isDir) {
return NoMatch
} else if !p.isGlob && !p.simpleNameMatch(path, isDir) {
return NoMatch
}
if p.inclusion {
return Include
} else {
return Exclude
}
}
func (p *pattern) simpleNameMatch(path []string, isDir bool) bool {
for i, name := range path {
if match, err := filepath.Match(p.pattern[0], name); err != nil {
return false
} else if !match {
continue
}
if p.dirOnly && !isDir && i == len(path)-1 {
return false
}
return true
}
return false
}
func (p *pattern) globMatch(path []string, isDir bool) bool {
matched := false
canTraverse := false
for i, pattern := range p.pattern {
if pattern == "" {
canTraverse = false
continue
}
if pattern == zeroToManyDirs {
if i == len(p.pattern)-1 {
break
}
canTraverse = true
continue
}
if strings.Contains(pattern, zeroToManyDirs) {
return false
}
if len(path) == 0 {
return false
}
if canTraverse {
canTraverse = false
for len(path) > 0 {
e := path[0]
path = path[1:]
if match, err := filepath.Match(pattern, e); err != nil {
return false
} else if match {
matched = true
break
} else if len(path) == 0 {
// if nothing left then fail
matched = false
}
}
} else {
if match, err := filepath.Match(pattern, path[0]); err != nil || !match {
return false
}
matched = true
path = path[1:]
}
}
if matched && p.dirOnly && !isDir && len(path) == 0 {
matched = false
}
return matched
}

View File

@@ -0,0 +1,80 @@
// Test cases ported from go-git: github.com/go-git/go-git/v5 v5.19.1,
// plumbing/format/gitignore/pattern_test.go (originally written against
// gopkg.in/check.v1; rewritten here as table-driven stdlib tests to avoid an
// extra test dependency). Licensed under the Apache License 2.0; see LICENSE.
package gitignore
import "testing"
func TestParsePattern_Match(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pattern string
domain []string
path []string
isDir bool
want MatchResult
}{
{"inclusion", "!vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Include},
{"domainLonger_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle"}, false, NoMatch},
{"domainSameLength_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle", "tail"}, false, NoMatch},
{"domainMismatch_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle", "_tail_", "value"}, false, NoMatch},
{"withDomain", "middle/", []string{"value", "volcano"}, []string{"value", "volcano", "middle", "tail"}, false, Exclude},
{"onlyMatchInDomain_mismatch", "volcano/", []string{"value", "volcano"}, []string{"value", "volcano", "tail"}, true, NoMatch},
{"atStart", "value", nil, []string{"value", "tail"}, false, Exclude},
{"inTheMiddle", "value", nil, []string{"head", "value", "tail"}, false, Exclude},
{"atEnd", "value", nil, []string{"head", "value"}, false, Exclude},
{"atStart_dirWanted", "value/", nil, []string{"value", "tail"}, false, Exclude},
{"inTheMiddle_dirWanted", "value/", nil, []string{"head", "value", "tail"}, false, Exclude},
{"atEnd_dirWanted", "value/", nil, []string{"head", "value"}, true, Exclude},
{"atEnd_dirWanted_notADir_mismatch", "value/", nil, []string{"head", "value"}, false, NoMatch},
{"mismatch", "value", nil, []string{"head", "val", "tail"}, false, NoMatch},
{"valueLonger_mismatch", "val", nil, []string{"head", "value", "tail"}, false, NoMatch},
{"withAsterisk", "v*o", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
{"withQuestionMark", "vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
{"magicChars", "v[ou]l[kc]ano", nil, []string{"value", "volcano"}, false, Exclude},
{"wrongPattern_mismatch", "v[ou]l[", nil, []string{"value", "vol["}, false, NoMatch},
{"glob_fromRootWithSlash", "/value/vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
{"glob_withDomain", "middle/tail/", []string{"value", "volcano"}, []string{"value", "volcano", "middle", "tail"}, true, Exclude},
{"glob_onlyMatchInDomain_mismatch", "volcano/tail", []string{"value", "volcano"}, []string{"value", "volcano", "tail"}, false, NoMatch},
{"glob_fromRootWithoutSlash", "value/vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
{"glob_fromRoot_mismatch", "value/vulkano", nil, []string{"value", "volcano"}, false, NoMatch},
{"glob_fromRoot_tooShort_mismatch", "value/vul?ano", nil, []string{"value"}, false, NoMatch},
{"glob_fromRoot_notAtRoot_mismatch", "/value/volcano", nil, []string{"value", "value", "volcano"}, false, NoMatch},
{"glob_leadingAsterisks_atStart", "**/*lue/vol?ano", nil, []string{"value", "volcano", "tail"}, false, Exclude},
{"glob_leadingAsterisks_notAtStart", "**/*lue/vol?ano", nil, []string{"head", "value", "volcano", "tail"}, false, Exclude},
{"glob_leadingAsterisks_mismatch", "**/*lue/vol?ano", nil, []string{"head", "value", "Volcano", "tail"}, false, NoMatch},
{"glob_leadingAsterisks_isDir", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano", "tail"}, false, Exclude},
{"glob_leadingAsterisks_isDirAtEnd", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano"}, true, Exclude},
{"glob_leadingAsterisks_isDir_mismatch", "**/*lue/vol?ano/", nil, []string{"head", "value", "Colcano"}, true, NoMatch},
{"glob_leadingAsterisks_isDirNoDirAtEnd_mismatch", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano"}, false, NoMatch},
{"glob_tailingAsterisks", "/*lue/vol?ano/**", nil, []string{"value", "volcano", "tail", "moretail"}, false, Exclude},
{"glob_tailingAsterisks_exactMatch", "/*lue/vol?ano/**", nil, []string{"value", "volcano"}, false, Exclude},
{"glob_middleAsterisks_emptyMatch", "/*lue/**/vol?ano", nil, []string{"value", "volcano"}, false, Exclude},
{"glob_middleAsterisks_oneMatch", "/*lue/**/vol?ano", nil, []string{"value", "middle", "volcano"}, false, Exclude},
{"glob_middleAsterisks_multiMatch", "/*lue/**/vol?ano", nil, []string{"value", "middle1", "middle2", "volcano"}, false, Exclude},
{"glob_middleAsterisks_isDir_trailing", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano"}, true, Exclude},
{"glob_middleAsterisks_isDir_trailing_mismatch", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano"}, false, NoMatch},
{"glob_middleAsterisks_isDir", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano", "tail"}, false, Exclude},
{"glob_wrongDoubleAsterisk_mismatch", "/*lue/**foo/vol?ano", nil, []string{"value", "foo", "volcano", "tail"}, false, NoMatch},
{"glob_magicChars", "**/head/v[ou]l[kc]ano", nil, []string{"value", "head", "volcano"}, false, Exclude},
{"glob_wrongPattern_noTraversal_mismatch", "**/head/v[ou]l[", nil, []string{"value", "head", "vol["}, false, NoMatch},
{"glob_wrongPattern_onTraversal_mismatch", "/value/**/v[ou]l[", nil, []string{"value", "head", "vol["}, false, NoMatch},
{"glob_issue_923", "**/android/**/GeneratedPluginRegistrant.java", nil, []string{"packages", "flutter_tools", "lib", "src", "android", "gradle.dart"}, false, NoMatch},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
p := ParsePattern(tt.pattern, tt.domain)
if got := p.Match(tt.path, tt.isDir); got != tt.want {
t.Errorf("ParsePattern(%q, %v).Match(%v, %t) = %v, want %v",
tt.pattern, tt.domain, tt.path, tt.isDir, got, tt.want)
}
})
}
}

View File

@@ -117,7 +117,12 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
isCommand := c.Cmd != ""
l.Outf(logger.Default, " - ")
if isCommand {
l.Outf(logger.Yellow, "%s\n", c.Cmd)
// Use the masked command so secret values are not leaked in summaries
logCmd := c.LogCmd
if logCmd == "" {
logCmd = c.Cmd
}
l.Outf(logger.Yellow, "%s\n", logCmd)
} else {
l.Outf(logger.Green, "Task: %s\n", c.Task)
}
@@ -196,6 +201,11 @@ func printTaskEnv(l *logger.Logger, t *ast.Task) {
// formatVarValue formats a variable value based on its type.
// Handles static values, shell commands (sh:), references (ref:), and maps.
func formatVarValue(v ast.Var) string {
// Never expose secret variables in the summary, whatever their type
if v.Secret {
return "*****"
}
// Shell command - check this first before Value
// because dynamic vars may have both Sh and an empty Value
if v.Sh != nil {
@@ -285,7 +295,9 @@ func isEnvVar(key string, envVars map[string]bool) bool {
key == "TASKFILE_DIR" ||
key == "USER_WORKING_DIR" ||
key == "ALIAS" ||
key == "MATCH" {
key == "MATCH" ||
key == "PATH_LIST_SEPARATOR" ||
key == "FILE_PATH_SEPARATOR" {
return true
}
return envVars[key]

View File

@@ -3,6 +3,8 @@ package templater
import (
"maps"
"math/rand/v2"
"os"
"path"
"path/filepath"
"runtime"
"strings"
@@ -21,8 +23,8 @@ var templateFuncs template.FuncMap
func init() {
taskFuncs := template.FuncMap{
"OS": os,
"ARCH": arch,
"OS": goos,
"ARCH": goarch,
"numCPU": runtime.NumCPU,
"catLines": catLines,
"splitLines": splitLines,
@@ -33,7 +35,10 @@ func init() {
"splitArgs": splitArgs,
"IsSH": IsSH, // Deprecated
"joinPath": filepath.Join,
"joinEnv": joinEnv,
"joinUrl": joinUrl,
"relPath": filepath.Rel,
"absPath": filepath.Abs,
"merge": merge,
"spew": spew.Sdump,
"fromYaml": fromYaml,
@@ -56,11 +61,11 @@ func init() {
maps.Copy(templateFuncs, taskFuncs)
}
func os() string {
func goos() string {
return runtime.GOOS
}
func arch() string {
func goarch() string {
return runtime.GOARCH
}
@@ -94,6 +99,14 @@ func IsSH() bool {
return true
}
func joinEnv(elem ...string) string {
return strings.Join(elem, string(os.PathListSeparator))
}
func joinUrl(elem ...string) string {
return path.Join(elem...)
}
func merge(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {

View File

@@ -0,0 +1,61 @@
package templater
import (
"github.com/go-task/task/v3/taskfile/ast"
)
// MaskSecrets replaces template placeholders with their values, masking secrets.
// This function uses the Go templater to resolve all variables ({{.VAR}}) while
// masking secret ones as "*****".
func MaskSecrets(cmdTemplate string, vars *ast.Vars) string {
return MaskSecretsWithExtra(cmdTemplate, vars, nil)
}
// MaskSecretsWithExtra is like MaskSecrets but also resolves extra variables (e.g., loop vars).
func MaskSecretsWithExtra(cmdTemplate string, vars *ast.Vars, extra map[string]any) string {
if vars == nil {
vars = ast.NewVars()
}
// Fast path: if there are no secrets to mask, resolve the template directly
// without the extra DeepCopy + masking pass.
if !hasSecrets(vars) {
cache := &Cache{Vars: vars}
result := ReplaceWithExtra(cmdTemplate, cache, extra)
if cache.Err() != nil {
return cmdTemplate
}
return result
}
// Create a copy with secret values masked, leaving the originals untouched.
maskedVars := vars.DeepCopy()
for name, v := range maskedVars.All() {
if v.Secret {
maskedVars.Set(name, ast.Var{
Value: "*****",
Secret: true,
})
}
}
cache := &Cache{Vars: maskedVars}
result := ReplaceWithExtra(cmdTemplate, cache, extra)
// If there was an error, return the original template
if cache.Err() != nil {
return cmdTemplate
}
return result
}
// hasSecrets reports whether any variable is marked as secret.
func hasSecrets(vars *ast.Vars) bool {
for _, v := range vars.All() {
if v.Secret {
return true
}
}
return false
}

View File

@@ -68,6 +68,13 @@ func ReplaceWithExtra[T any](v T, cache *Cache, extra map[string]any) T {
return v
}
// Optimization: skip if string is not a template
if s, ok := any(v).(string); ok {
if !strings.Contains(s, "{{") {
return v
}
}
// Initialize the cache map if it's not already initialized
if cache.cacheMap == nil {
cache.cacheMap = cache.Vars.ToCacheMap()
@@ -82,6 +89,10 @@ func ReplaceWithExtra[T any](v T, cache *Cache, extra map[string]any) T {
// Traverse the value and parse any template variables
copy, err := deepcopy.TraverseStringsFunc(v, func(v string) (string, error) {
// Optimization: skip if string is not a template
if !strings.Contains(v, "{{") {
return v, nil
}
tpl, err := template.New("").Funcs(templateFuncs).Parse(v)
if err != nil {
return v, err
@@ -121,14 +132,15 @@ func ReplaceVar(v ast.Var, cache *Cache) ast.Var {
func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {
if v.Ref != "" {
return ast.Var{Value: ResolveRef(v.Ref, cache)}
return ast.Var{Value: ResolveRef(v.Ref, cache), Secret: v.Secret}
}
return ast.Var{
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Secret: v.Secret,
}
}

View File

@@ -1 +1 @@
3.50.0
3.51.1

12
mise.toml Normal file
View 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"

View File

@@ -3,6 +3,8 @@ package task
import (
"slices"
"github.com/elliotchance/orderedmap/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/input"
"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
visited := make(map[string]bool)
varsMap := make(map[string]*ast.VarsWithValidation)
varsMap := orderedmap.NewOrderedMap[string, *ast.VarsWithValidation]()
var 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) {
if _, exists := varsMap[v.Name]; !exists {
varsMap[v.Name] = v
if !varsMap.Has(v.Name) {
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
}
prompter := e.newPrompter()
e.promptedVars = ast.NewVars()
for _, v := range varsMap {
for v := range varsMap.Values() {
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
if err != nil {
if errors.Is(err, input.ErrCancelled) {

View File

@@ -1,6 +1,7 @@
package task
import (
"cmp"
"context"
"fmt"
"os"
@@ -13,7 +14,6 @@ import (
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
@@ -133,13 +133,9 @@ func (e *Executor) setupTempDir() error {
return nil
}
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
// e.TempDirPath carries the resolved CLI precedence (flag > TASK_TEMP_DIR > taskrc).
tempDir := cmp.Or(e.TempDirPath, ".task")
if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return err

View File

@@ -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)
}
// Resolve template with secrets masked for logging
cmd.LogCmd = templater.MaskSecretsWithExtra(cmd.Cmd, vars, extra)
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
@@ -388,12 +390,12 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
return err
case cmd.Cmd != "":
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.LogCmd)
return nil
}
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 {

View File

@@ -653,6 +653,164 @@ func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest /
require.NoError(t, err, "generated.txt should be recreated after third run")
}
func writeFile(t *testing.T, dir, name, content string) {
t.Helper()
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, name), []byte(content), 0o644))
}
// gitignoreStep writes a set of files then runs the task once, capturing its
// output as a golden fixture named run.
type gitignoreStep struct {
write map[string]string
run string
}
// gitignoreSeq drives a checksum task through a sequence of runs against a
// fixture dir. create seeds runtime files (removed on cleanup); restore resets
// tracked files to their committed content on cleanup; artifacts are
// task-produced files to delete on cleanup.
type gitignoreSeq struct {
dir string
task string
create map[string]string
restore map[string]string
artifacts []string
steps []gitignoreStep
}
func (s gitignoreSeq) run(t *testing.T) {
t.Helper()
cleanup := func() {
_ = os.RemoveAll(filepathext.SmartJoin(s.dir, ".task"))
for name := range s.create {
_ = os.Remove(filepathext.SmartJoin(s.dir, name))
}
for _, name := range s.artifacts {
_ = os.Remove(filepathext.SmartJoin(s.dir, name))
}
for name, content := range s.restore {
writeFile(t, s.dir, name, content)
}
}
cleanup()
t.Cleanup(cleanup)
for name, content := range s.create {
writeFile(t, s.dir, name, content)
}
for _, step := range s.steps {
for name, content := range step.write {
writeFile(t, s.dir, name, content)
}
NewExecutorTest(t,
WithName(step.run),
WithExecutorOptions(task.WithDir(s.dir)),
WithTask(s.task),
)
}
}
func TestGitignoreChecksum(t *testing.T) { //nolint:paralleltest // shares testdata/gitignore and mutates fixture files
gitignoreSeq{
dir: "testdata/gitignore",
task: "build",
create: map[string]string{"ignored.txt": "ignored\n"},
restore: map[string]string{"source.txt": "source content\n"},
artifacts: []string{"generated.txt"},
steps: []gitignoreStep{
{run: "first run"},
{run: "up to date"},
{run: "ignored file modified", write: map[string]string{"ignored.txt": "ignored modified\n"}},
{run: "source file modified", write: map[string]string{"source.txt": "source modified\n"}},
},
}.run(t)
}
// TestGitignoreNegation checks that a `!pattern` in a nested .gitignore
// re-includes a file excluded by a parent .gitignore.
func TestGitignoreNegation(t *testing.T) { //nolint:paralleltest // mutates fixture files
gitignoreSeq{
dir: "testdata/gitignore_negation",
task: "build",
create: map[string]string{"sub/debug.log": "debug\n", "sub/other.log": "other\n"},
steps: []gitignoreStep{
{run: "first run"},
{run: "up to date"},
{run: "ignored file modified", write: map[string]string{"sub/other.log": "other modified\n"}},
{run: "reincluded file modified", write: map[string]string{"sub/debug.log": "debug modified\n"}},
},
}.run(t)
}
// TestGitignoreNested checks that a .gitignore in a subdirectory below the task
// dir is honored when its files are reached by a deep glob.
func TestGitignoreNested(t *testing.T) { //nolint:paralleltest // mutates fixture files
gitignoreSeq{
dir: "testdata/gitignore_nested",
task: "build",
create: map[string]string{"sub/secret.dat": "secret\n"},
restore: map[string]string{"sub/keep.txt": "keep\n"},
steps: []gitignoreStep{
{run: "first run"},
{run: "up to date"},
{run: "ignored file modified", write: map[string]string{"sub/secret.dat": "secret modified\n"}},
{run: "source file modified", write: map[string]string{"sub/keep.txt": "keep modified\n"}},
},
}.run(t)
}
// TestGitignoreIncluded checks that a top-level use_gitignore in an included
// Taskfile is propagated onto its tasks during merge.
func TestGitignoreIncluded(t *testing.T) { //nolint:paralleltest // mutates fixture files
gitignoreSeq{
dir: "testdata/gitignore_included",
task: "included:build",
create: map[string]string{"ignored.txt": "ignored\n"},
steps: []gitignoreStep{
{run: "first run"},
{run: "up to date"},
{run: "ignored file modified", write: map[string]string{"ignored.txt": "ignored modified\n"}},
},
}.run(t)
}
// TestGitignoreIncludedOverride checks that an explicit use_gitignore: false in
// an included Taskfile is preserved even when the root Taskfile sets it to true.
func TestGitignoreIncludedOverride(t *testing.T) { //nolint:paralleltest // mutates fixture files
gitignoreSeq{
dir: "testdata/gitignore_included_override",
task: "included:build",
create: map[string]string{"ignored.txt": "ignored\n"},
steps: []gitignoreStep{
{run: "first run"},
{run: "up to date"},
{run: "ignored file modified", write: map[string]string{"ignored.txt": "ignored modified\n"}},
},
}.run(t)
}
func TestGitignoreTaskListFallback(t *testing.T) { //nolint:paralleltest // shares testdata/gitignore with TestGitignoreChecksum
const dir = "testdata/gitignore"
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())
listed, err := e.CompiledTaskForTaskList(&task.Call{Task: "build"})
require.NoError(t, err)
assert.True(t, listed.ShouldUseGitignore(),
"task list should reflect the global use_gitignore fallback")
// "build-no-use_gitignore" explicitly disables it.
listedOff, err := e.CompiledTaskForTaskList(&task.Call{Task: "build-no-use_gitignore"})
require.NoError(t, err)
assert.False(t, listedOff.ShouldUseGitignore(),
"explicit use_gitignore: false must be preserved in the list path")
}
func TestStatusVariables(t *testing.T) {
t.Parallel()
@@ -2601,6 +2759,27 @@ func TestSplitArgs(t *testing.T) {
assert.Equal(t, "3\n", buff.String())
}
func TestAbsPath(t *testing.T) {
t.Parallel()
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir("testdata/abs_path"),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithSilent(true),
)
require.NoError(t, e.Setup())
err := e.Run(t.Context(), &task.Call{Task: "default"})
require.NoError(t, err)
cwd, err := os.Getwd()
require.NoError(t, err)
expected := filepath.Join(cwd, "bar") + "\n"
assert.Equal(t, expected, buff.String())
}
func TestSingleCmdDep(t *testing.T) {
t.Parallel()

View File

@@ -9,7 +9,8 @@ import (
// Cmd is a task command
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
For *For
If string
@@ -28,6 +29,7 @@ func (c *Cmd) DeepCopy() *Cmd {
}
return &Cmd{
Cmd: c.Cmd,
LogCmd: c.LogCmd,
Task: c.Task,
For: c.For.DeepCopy(),
If: c.If,

View File

@@ -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 {
switch node.Kind {
case yaml.MappingNode:

View File

@@ -38,6 +38,7 @@ type Task struct {
Method string
Prefix string `hash:"ignore"`
IgnoreError bool
UseGitignore *bool
Run string
Platforms []*Platform
If string
@@ -75,6 +76,12 @@ func (t *Task) IsSilent() bool {
return t.Silent != nil && *t.Silent
}
// ShouldUseGitignore returns true if gitignore filtering is enabled for the task.
// Returns false if UseGitignore is nil or set to false.
func (t *Task) ShouldUseGitignore() bool {
return t.UseGitignore != nil && *t.UseGitignore
}
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
func (t *Task) WildcardMatch(name string) (bool, []string) {
names := append([]string{t.Task}, t.Aliases...)
@@ -149,7 +156,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Internal bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
IgnoreError bool `yaml:"ignore_error"`
UseGitignore *bool `yaml:"use_gitignore,omitempty"`
Run string
Platforms []*Platform
If string
@@ -190,6 +198,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Method = task.Method
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.UseGitignore = deepcopy.Scalar(task.UseGitignore)
t.Run = task.Run
t.Platforms = task.Platforms
t.If = task.If
@@ -233,6 +242,7 @@ func (t *Task) DeepCopy() *Task {
Method: t.Method,
Prefix: t.Prefix,
IgnoreError: t.IgnoreError,
UseGitignore: deepcopy.Scalar(t.UseGitignore),
Run: t.Run,
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),

View File

@@ -20,20 +20,21 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c
// Taskfile is the abstract syntax tree for a Taskfile
type Taskfile struct {
Location string
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
Location string
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
UseGitignore *bool
}
// Merge merges the second Taskfile into the first
@@ -67,6 +68,14 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
}
}
}
if t2.UseGitignore != nil {
for _, t := range t2.Tasks.All(nil) {
if t.UseGitignore == nil {
v := *t2.UseGitignore
t.UseGitignore = &v
}
}
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
@@ -76,19 +85,20 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
var taskfile struct {
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
Version *semver.Version
Output Output
Method string
Includes *Includes
Set []string
Shopt []string
Vars *Vars
Env *Vars
Tasks *Tasks
Silent bool
Dotenv []string
Run string
Interval time.Duration
UseGitignore *bool `yaml:"use_gitignore"`
}
if err := node.Decode(&taskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -106,6 +116,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run
tf.Interval = taskfile.Interval
tf.UseGitignore = taskfile.UseGitignore
if tf.Includes == nil {
tf.Includes = NewIncludes()
}

View File

@@ -8,37 +8,54 @@ import (
// Var represents either a static or dynamic variable.
type Var struct {
Value any
Live any
Sh *string
Ref string
Dir string
Value any
Live any
Sh *string
Ref string
Dir string
Secret bool
}
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
key := "<none>"
if len(node.Content) > 0 {
key = node.Content[0].Value
var m struct {
Sh *string
Ref string
Map any
Value any
Secret bool
}
switch key {
case "sh", "ref", "map":
var m struct {
Sh *string
Ref string
Map any
if err := node.Decode(&m); err != nil {
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 err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
}
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.Ref = m.Ref
v.Secret = m.Secret
// Handle both "map" and "value" keys
if m.Map != nil {
v.Value = m.Map
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)
} else if m.Value != nil {
v.Value = m.Value
}
return nil
default:
var value any
if err := node.Decode(&value); err != nil {

View File

@@ -73,7 +73,7 @@ func NewNode(
return node, err
}
func isRemoteEntrypoint(entrypoint string) bool {
func IsRemoteEntrypoint(entrypoint string) bool {
scheme, _ := getScheme(entrypoint)
switch scheme {
case "git", "http", "https":
@@ -93,8 +93,8 @@ func getScheme(uri string) (string, error) {
return "git", nil
}
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i], nil
if before, _, ok := strings.Cut(uri, "://"); ok {
return before, nil
}
return "", nil

View File

@@ -60,7 +60,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
if IsRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -188,7 +188,7 @@ func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
if IsRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -42,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
if IsRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"os"
"slices"
"sync"
"time"
@@ -285,12 +286,7 @@ func (r *Reader) isTrusted(uri string) bool {
host := parsedURL.Host
// Check against each trusted pattern (exact match including port if provided)
for _, pattern := range r.trustedHosts {
if host == pattern {
return true
}
}
return false
return slices.Contains(r.trustedHosts, host)
}
func (r *Reader) include(ctx context.Context, node Node) error {

View File

@@ -19,6 +19,7 @@ type TaskRC struct {
Interactive *bool `yaml:"interactive"`
Remote Remote `yaml:"remote"`
Failfast bool `yaml:"failfast"`
TempDir *string `yaml:"temp-dir"`
Experiments map[string]int `yaml:"experiments"`
}
@@ -70,4 +71,5 @@ func (t *TaskRC) Merge(other *TaskRC) {
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
t.Interactive = cmp.Or(other.Interactive, t.Interactive)
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
t.TempDir = cmp.Or(other.TempDir, t.TempDir)
}

View File

@@ -112,6 +112,40 @@ func TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run
}, cfg)
}
func TestGetConfig_TempDir(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
writeFile(t, localDir, ".taskrc.yml", `
temp-dir: .task-cache
`)
cfg, err := GetConfig(localDir)
require.NoError(t, err)
require.NotNil(t, cfg)
require.NotNil(t, cfg.TempDir)
assert.Equal(t, ".task-cache", *cfg.TempDir)
}
func TestGetConfig_TempDirMergePrecedence(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)
writeFile(t, xdgConfigDir, "taskrc.yml", `
temp-dir: xdg-cache
`)
writeFile(t, homeDir, ".taskrc.yml", `
temp-dir: home-cache
`)
writeFile(t, localDir, ".taskrc.yml", `
temp-dir: local-cache
`)
cfg, err := GetConfig(localDir)
require.NoError(t, err)
require.NotNil(t, cfg)
require.NotNil(t, cfg.TempDir)
assert.Equal(t, "local-cache", *cfg.TempDir)
}
func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)

6
testdata/abs_path/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
default:
cmds:
- cmd: echo '{{absPath "foo/../bar"}}'

1
testdata/gitignore/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
ignored.txt

25
testdata/gitignore/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
version: '3'
use_gitignore: true
tasks:
build:
cmds:
- cp ./source.txt ./generated.txt
sources:
- ./*.txt
- exclude: ./generated.txt
generates:
- ./generated.txt
method: checksum
build-no-use_gitignore:
use_gitignore: false
cmds:
- cp ./source.txt ./generated.txt
sources:
- ./*.txt
- exclude: ./generated.txt
generates:
- ./generated.txt
method: checksum

1
testdata/gitignore/source.txt vendored Normal file
View File

@@ -0,0 +1 @@
source content

View File

@@ -0,0 +1 @@
task: [build] cp ./source.txt ./generated.txt

View File

@@ -0,0 +1 @@
task: Task "build" is up to date

View File

@@ -0,0 +1 @@
task: [build] cp ./source.txt ./generated.txt

View File

@@ -0,0 +1 @@
task: Task "build" is up to date

View File

@@ -0,0 +1 @@
ignored.txt

View File

@@ -0,0 +1,9 @@
version: '3'
includes:
included: ./included/Taskfile.yml
tasks:
default:
cmds:
- echo "root"

View File

@@ -0,0 +1,11 @@
version: '3'
use_gitignore: true
tasks:
build:
cmds:
- echo "build executed"
sources:
- ./*.txt
method: checksum

View File

@@ -0,0 +1 @@
source

View File

@@ -0,0 +1,2 @@
task: [included:build] echo "build executed"
build executed

View File

@@ -0,0 +1 @@
task: Task "included:build" is up to date

View File

@@ -0,0 +1 @@
task: Task "included:build" is up to date

View File

@@ -0,0 +1 @@
ignored.txt

View File

@@ -0,0 +1,11 @@
version: '3'
use_gitignore: true
includes:
included: ./included/Taskfile.yml
tasks:
default:
cmds:
- echo "root"

View File

@@ -0,0 +1,11 @@
version: '3'
use_gitignore: false
tasks:
build:
cmds:
- echo "build executed"
sources:
- ./*.txt
method: checksum

View File

@@ -0,0 +1 @@
source

View File

@@ -0,0 +1,2 @@
task: [included:build] echo "build executed"
build executed

View File

@@ -0,0 +1,2 @@
task: [included:build] echo "build executed"
build executed

View File

@@ -0,0 +1 @@
task: Task "included:build" is up to date

View File

@@ -0,0 +1 @@
*.log

View File

@@ -0,0 +1,11 @@
version: '3'
use_gitignore: true
tasks:
build:
cmds:
- echo "build executed"
sources:
- ./sub/*.log
method: checksum

View File

@@ -0,0 +1 @@
!debug.log

View File

@@ -0,0 +1,2 @@
task: [build] echo "build executed"
build executed

View File

@@ -0,0 +1 @@
task: Task "build" is up to date

View File

@@ -0,0 +1,2 @@
task: [build] echo "build executed"
build executed

View File

@@ -0,0 +1 @@
task: Task "build" is up to date

11
testdata/gitignore_nested/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
version: '3'
use_gitignore: true
tasks:
build:
cmds:
- echo "build executed"
sources:
- ./sub/*
method: checksum

View File

@@ -0,0 +1 @@
secret.dat

View File

@@ -0,0 +1 @@
keep

View File

@@ -0,0 +1,2 @@
task: [build] echo "build executed"
build executed

View File

@@ -0,0 +1 @@
task: Task "build" is up to date

View File

@@ -0,0 +1,2 @@
task: [build] echo "build executed"
build executed

View File

@@ -0,0 +1 @@
task: Task "build" is up to date

83
testdata/secrets/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
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}}"

View 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

View File

@@ -0,0 +1,5 @@
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

View 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

View 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

View File

@@ -0,0 +1,2 @@
task: [test-multiple-secrets] echo "API=***** PWD=*****"
API=secret-api-key-123 PWD=my-super-secret-password

View File

@@ -0,0 +1,2 @@
task: [test-secret-key-order] echo "Value=***** Sh=*****"
Value=order-independent-secret Sh=sh-order-independent-secret

View 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

View File

@@ -0,0 +1,15 @@
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"

View File

@@ -19,6 +19,16 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)
// shouldTaskUseGitignore resolves whether gitignore filtering applies to a
// task: the task-level value takes precedence, falling back to the Taskfile's
// global use_gitignore when the task does not set it.
func (e *Executor) shouldTaskUseGitignore(t *ast.Task) bool {
if t.UseGitignore != nil {
return *t.UseGitignore
}
return e.Taskfile.UseGitignore != nil && *e.Taskfile.UseGitignore
}
// CompiledTask returns a copy of a task, but replacing variables in almost all
// properties using the Go template package.
func (e *Executor) CompiledTask(call *Call) (*ast.Task, error) {
@@ -43,6 +53,8 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
cache := &templater.Cache{Vars: vars}
gitignore := e.shouldTaskUseGitignore(origTask)
return &ast.Task{
Task: origTask.Task,
Label: templater.Replace(origTask.Label, cache),
@@ -59,6 +71,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
Env: nil,
Dotenv: origTask.Dotenv,
Silent: deepcopy.Scalar(origTask.Silent),
UseGitignore: &gitignore,
Interactive: origTask.Interactive,
Internal: origTask.Internal,
Method: origTask.Method,
@@ -110,6 +123,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
}
}
gitignore := e.shouldTaskUseGitignore(origTask)
new := ast.Task{
Task: origTask.Task,
Label: templater.Replace(origTask.Label, cache),
@@ -126,6 +141,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
Env: nil,
Dotenv: templater.Replace(origTask.Dotenv, cache),
Silent: deepcopy.Scalar(origTask.Silent),
UseGitignore: &gitignore,
Interactive: origTask.Interactive,
Internal: origTask.Internal,
Method: templater.Replace(origTask.Method, cache),
@@ -219,7 +235,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
continue
}
if cmd.For != nil {
list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)
list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache)
if err != nil {
return nil, err
}
@@ -239,6 +255,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
extra["KEY"] = keys[i]
}
newCmd := cmd.DeepCopy()
// Resolve template with secrets masked + loop vars for logging
newCmd.LogCmd = templater.MaskSecretsWithExtra(cmd.Cmd, cache.Vars, extra)
newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
@@ -254,6 +272,8 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
continue
}
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.Task = templater.Replace(cmd.Task, cache)
newCmd.If = templater.Replace(cmd.If, cache)
@@ -268,7 +288,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
continue
}
if dep.For != nil {
list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)
list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache)
if err != nil {
return nil, err
}
@@ -339,6 +359,7 @@ func itemsFromFor(
dir string,
sources []*ast.Glob,
generates []*ast.Glob,
gitignore bool,
vars *ast.Vars,
location *ast.Location,
cache *templater.Cache,
@@ -347,13 +368,14 @@ func itemsFromFor(
var values []any // The list of values to loop over
// Get the list from a matrix
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{
URI: location.Taskfile,
Err: err,
}
}
return asAnySlice(product(f.Matrix)), nil, nil
return asAnySlice(product(resolvedMatrix)), nil, nil
}
// Get the list from the explicit for list
if len(f.List) > 0 {
@@ -361,7 +383,7 @@ func itemsFromFor(
}
// Get the list from the task sources
if f.From == "sources" {
glist, err := fingerprint.Globs(dir, sources)
glist, err := fingerprint.Globs(dir, sources, gitignore)
if err != nil {
return nil, nil, err
}
@@ -375,7 +397,7 @@ func itemsFromFor(
}
// Get the list from the task generates
if f.From == "generates" {
glist, err := fingerprint.Globs(dir, generates)
glist, err := fingerprint.Globs(dir, generates, gitignore)
if err != nil {
return nil, nil, err
}
@@ -425,22 +447,43 @@ func itemsFromFor(
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 {
return nil
return matrix, nil
}
hasRef := false
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 != "" {
v := templater.ResolveRef(row.Ref, cache)
if cache.Err() != nil {
return nil, cache.Err()
}
switch value := v.(type) {
case []any:
row.Value = value
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 {

Some files were not shown because too many files have changed in this diff Show More