mirror of
https://github.com/go-task/task.git
synced 2026-05-18 13:15:41 +02:00
Compare commits
191 Commits
v3.45.4
...
docs/homeb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
554c9811f2 | ||
|
|
c62f9c7147 | ||
|
|
c4ecff753d | ||
|
|
2ed77716be | ||
|
|
a2f8e144ca | ||
|
|
2cdd7d3e43 | ||
|
|
56b316a124 | ||
|
|
39ce6a21ac | ||
|
|
fc5f6fa3aa | ||
|
|
44a2f2e5f5 | ||
|
|
d356c649aa | ||
|
|
ca24d32f37 | ||
|
|
f63a63fa6c | ||
|
|
c0ff7105e7 | ||
|
|
8b063d6b92 | ||
|
|
dc8ac5e79f | ||
|
|
df7810ab63 | ||
|
|
82783417ea | ||
|
|
d5bed6b716 | ||
|
|
c8f722c0d5 | ||
|
|
b7743eda88 | ||
|
|
c0796e9701 | ||
|
|
cf54be3266 | ||
|
|
e129ae2fac | ||
|
|
ed69256512 | ||
|
|
40ad9719d4 | ||
|
|
48f75f0913 | ||
|
|
f000ea2b22 | ||
|
|
e8be687a40 | ||
|
|
788605a3a9 | ||
|
|
697cf442a2 | ||
|
|
e957edf783 | ||
|
|
09e7247d05 | ||
|
|
502f24a2ad | ||
|
|
f09f31c6d5 | ||
|
|
5a78808caa | ||
|
|
026c899d90 | ||
|
|
f6720760b4 | ||
|
|
065236f076 | ||
|
|
1bd5aa6bd5 | ||
|
|
c3fd3c4b5e | ||
|
|
299232ee7d | ||
|
|
12a26fa15e | ||
|
|
4ab5dec8ae | ||
|
|
af311229fe | ||
|
|
1443e2d989 | ||
|
|
5bf4e4a29b | ||
|
|
f9052c9fdf | ||
|
|
0a82e2e053 | ||
|
|
6dedcafd7d | ||
|
|
c84cfa41f7 | ||
|
|
9bc1efbc47 | ||
|
|
da7eb0c855 | ||
|
|
edb491a4d0 | ||
|
|
2ad3d26f4a | ||
|
|
cdfcd08213 | ||
|
|
382c37bc2a | ||
|
|
618cd8956f | ||
|
|
b53e5da41a | ||
|
|
b9c1ab8eae | ||
|
|
e47f55783e | ||
|
|
d5f071c096 | ||
|
|
fb784f4e3d | ||
|
|
91f9299c98 | ||
|
|
145412a75c | ||
|
|
ba38344ca6 | ||
|
|
4e963f8714 | ||
|
|
3d4d189bcd | ||
|
|
179bde1f37 | ||
|
|
e4de687aee | ||
|
|
06538860a8 | ||
|
|
8a37bf5c1f | ||
|
|
ca99266aea | ||
|
|
8dfafe507f | ||
|
|
678fdec7d2 | ||
|
|
3626b271a7 | ||
|
|
fc378cfb92 | ||
|
|
9488a2a744 | ||
|
|
6ece2445ae | ||
|
|
9b95e758f4 | ||
|
|
28fee2c356 | ||
|
|
763e77467b | ||
|
|
f2385e625d | ||
|
|
e929cccd73 | ||
|
|
cb183349b7 | ||
|
|
2ebbb99f58 | ||
|
|
6660afc8d2 | ||
|
|
b710259bfa | ||
|
|
4ec6c453bd | ||
|
|
28408ef3f4 | ||
|
|
a2d34ffc4c | ||
|
|
1a190a118f | ||
|
|
18efa3982f | ||
|
|
655e83454e | ||
|
|
3ad4604c36 | ||
|
|
5a27d04655 | ||
|
|
ea933bcc55 | ||
|
|
e0d6b71971 | ||
|
|
d7ee855e49 | ||
|
|
511f35a456 | ||
|
|
5889ff6b65 | ||
|
|
85a98b5f90 | ||
|
|
89b6140166 | ||
|
|
8cd51af3b0 | ||
|
|
a40ddd4949 | ||
|
|
b1814277c2 | ||
|
|
500ab8b941 | ||
|
|
745633dc0e | ||
|
|
9b99866224 | ||
|
|
54e4905432 | ||
|
|
c95805e0e0 | ||
|
|
4560589652 | ||
|
|
084d6444b4 | ||
|
|
3fb7919577 | ||
|
|
69b345efc9 | ||
|
|
4af5278d73 | ||
|
|
12fbdd3ec7 | ||
|
|
72a349b0e9 | ||
|
|
896d65b21f | ||
|
|
2161f33b5c | ||
|
|
b93638b97a | ||
|
|
47b78ca879 | ||
|
|
f0b15d397b | ||
|
|
eb285fa3d2 | ||
|
|
02b13a687a | ||
|
|
a085d62727 | ||
|
|
4ab1958df1 | ||
|
|
54ca217b92 | ||
|
|
a6c0c1daba | ||
|
|
9cc1c7b40b | ||
|
|
7901cce831 | ||
|
|
c7b4f26900 | ||
|
|
3ed403b839 | ||
|
|
386dcbc1a0 | ||
|
|
799bc85498 | ||
|
|
0d9e8dd71b | ||
|
|
a927ffb31e | ||
|
|
42ad618205 | ||
|
|
2b713f564f | ||
|
|
cb8e94aa33 | ||
|
|
6bc339d714 | ||
|
|
5712c463f5 | ||
|
|
78cc6e5fd3 | ||
|
|
38e07ea812 | ||
|
|
72e25a25fd | ||
|
|
a496ee5fcb | ||
|
|
ef4292c42f | ||
|
|
dc315efc7f | ||
|
|
a3a3e7fb0b | ||
|
|
ee99849b1d | ||
|
|
bf9dc3f662 | ||
|
|
94f82cbc5a | ||
|
|
b14318ed3f | ||
|
|
17757c0c15 | ||
|
|
19f72b7eb0 | ||
|
|
0052ad2309 | ||
|
|
af1e755196 | ||
|
|
43074c20f2 | ||
|
|
39c86992bd | ||
|
|
c71241bcbd | ||
|
|
7c2bb78540 | ||
|
|
32e675895a | ||
|
|
786813d95d | ||
|
|
f7287c503a | ||
|
|
413574e3ee | ||
|
|
4b39becf65 | ||
|
|
15b7e3c69a | ||
|
|
7c93ea8b44 | ||
|
|
6a7cfa58f9 | ||
|
|
74b93f6eef | ||
|
|
88101613c8 | ||
|
|
599591ad3c | ||
|
|
348158a5f6 | ||
|
|
c3e410e95a | ||
|
|
42bcd5406a | ||
|
|
ba23aca631 | ||
|
|
5ef245a4bd | ||
|
|
036a60f517 | ||
|
|
9c969541a5 | ||
|
|
a52b483dd0 | ||
|
|
4e84c6bb76 | ||
|
|
0f9baf62a1 | ||
|
|
979ad523ef | ||
|
|
975c07688e | ||
|
|
67a02255b5 | ||
|
|
028ae1a660 | ||
|
|
68b1d2783d | ||
|
|
12793c350d | ||
|
|
8716ab81be | ||
|
|
c2a4e4470b | ||
|
|
f5a8ec8a0c |
11
.github/renovate.json
vendored
11
.github/renovate.json
vendored
@@ -8,6 +8,17 @@
|
||||
],
|
||||
"mode": "full",
|
||||
"addLabels":["area: dependencies"],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": ["^\\.github/workflows/.*\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"uses:\\s*golangci/golangci-lint-action@\\S+\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
|
||||
],
|
||||
"datasourceTemplate": "github-releases",
|
||||
"depNameTemplate": "golangci/golangci-lint"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
issue-awaiting-response:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-closed.yml
vendored
2
.github/workflows/issue-closed.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
issue-closed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
14
.github/workflows/issue-experiment.yml
vendored
14
.github/workflows/issue-experiment.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} proposed', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} draft', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} candidate', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} stable', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} released', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} abandoned', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
if: github.event.label.name == format('status{0} superseded', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-needs-triage.yml
vendored
2
.github/workflows/issue-needs-triage.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
issue-needs-triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -16,25 +16,25 @@ jobs:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.1.0
|
||||
version: v2.10.1
|
||||
|
||||
lint-jsonschema:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: 3.14
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: install check-jsonschema
|
||||
run: python -m pip install 'check-jsonschema==0.27.3'
|
||||
|
||||
69
.github/workflows/pr-build.yml
vendored
Normal file
69
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
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@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.26.x'
|
||||
cache: true
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: '~> v2'
|
||||
args: release --snapshot --clean --config .goreleaser-pr.yml
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: task_linux_amd64
|
||||
path: dist/task_linux_amd64.tar.gz
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: task_linux_arm64
|
||||
path: dist/task_linux_arm64.tar.gz
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: task_darwin_amd64
|
||||
path: dist/task_darwin_amd64.tar.gz
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: task_darwin_arm64
|
||||
path: dist/task_darwin_arm64.tar.gz
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: task_windows_amd64
|
||||
path: dist/task_windows_amd64.zip
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: checksums
|
||||
path: dist/task_checksums.txt
|
||||
- uses: peter-evans/find-comment@v4
|
||||
id: find-comment
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT || github.token }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: '📦 Build artifacts ready!'
|
||||
- uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT || 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
|
||||
6
.github/workflows/release-nightly.yml
vendored
6
.github/workflows/release-nightly.yml
vendored
@@ -9,14 +9,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
|
||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -5,23 +5,32 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: npm-login
|
||||
run: |
|
||||
npm config set '//registry.npmjs.org/:_authToken'=${{ secrets.NPM_TOKEN }}
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
- name: Set up Go ${{matrix.go-version}}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,3 +35,4 @@ tags
|
||||
/testdata/vars/v1
|
||||
/tmp
|
||||
node_modules
|
||||
website/.netlify/
|
||||
|
||||
31
.goreleaser-pr.yml
Normal file
31
.goreleaser-pr.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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'
|
||||
@@ -22,6 +22,8 @@ builds:
|
||||
goarch: '386'
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
- goos: windows
|
||||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
env:
|
||||
@@ -60,7 +62,7 @@ nfpms:
|
||||
- vendor: Task
|
||||
homepage: https://taskfile.dev
|
||||
maintainer: The Task authors <task@taskfile.dev>
|
||||
description: Simple task runner written in Go
|
||||
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||
section: golang
|
||||
license: MIT
|
||||
conflicts:
|
||||
@@ -69,7 +71,7 @@ nfpms:
|
||||
- deb
|
||||
- rpm
|
||||
- apk
|
||||
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
|
||||
file_name_template: '{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}'
|
||||
contents:
|
||||
- src: completion/bash/task.bash
|
||||
dst: /etc/bash_completion.d/task
|
||||
@@ -78,21 +80,20 @@ nfpms:
|
||||
- src: completion/zsh/_task
|
||||
dst: /usr/local/share/zsh/site-functions/_task
|
||||
|
||||
brews:
|
||||
homebrew_casks:
|
||||
- name: go-task
|
||||
description: Task runner / simpler Make alternative written in Go
|
||||
license: MIT
|
||||
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||
homepage: https://taskfile.dev
|
||||
directory: Formula
|
||||
binaries:
|
||||
- task
|
||||
completions:
|
||||
bash: completion/bash/task.bash
|
||||
zsh: completion/zsh/_task
|
||||
fish: completion/fish/task.fish
|
||||
directory: Casks
|
||||
repository:
|
||||
owner: go-task
|
||||
name: homebrew-tap
|
||||
test: system "#{bin}/task", "--help"
|
||||
install: |-
|
||||
bin.install "task"
|
||||
bash_completion.install "completion/bash/task.bash" => "task"
|
||||
zsh_completion.install "completion/zsh/_task" => "_task"
|
||||
fish_completion.install "completion/fish/task.fish"
|
||||
commit_author:
|
||||
name: task-bot
|
||||
email: 106601941+task-bot@users.noreply.github.com
|
||||
@@ -100,8 +101,8 @@ brews:
|
||||
winget:
|
||||
- name: Task
|
||||
publisher: Task
|
||||
short_description: A task runner / simpler Make alternative written in Go
|
||||
description: Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.
|
||||
short_description: The modern task runner.
|
||||
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||
license: MIT
|
||||
homepage: https://taskfile.dev/
|
||||
publisher_url: https://taskfile.dev/
|
||||
@@ -127,7 +128,7 @@ winget:
|
||||
repository:
|
||||
owner: go-task
|
||||
name: winget-pkgs
|
||||
branch: 'chore/task-{{.Version}}'
|
||||
branch: 'task-{{.Version}}'
|
||||
pull_request:
|
||||
enabled: true
|
||||
draft: false
|
||||
@@ -136,13 +137,15 @@ winget:
|
||||
owner: microsoft
|
||||
name: winget-pkgs
|
||||
branch: master
|
||||
body: |
|
||||
/cc @andreynering @pd93 @vmaerten
|
||||
|
||||
|
||||
npms:
|
||||
- name: "@go-task/cli"
|
||||
repository: "git+https://github.com/go-task/task.git"
|
||||
bugs: https://github.com/go-task/task/issues
|
||||
description: A task runner / simpler Make alternative written in Go
|
||||
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||
homepage: https://taskfile.dev
|
||||
license: MIT
|
||||
author: "The Task authors"
|
||||
@@ -153,7 +156,6 @@ npms:
|
||||
- "build-tool"
|
||||
- "task-runner"
|
||||
|
||||
|
||||
cloudsmiths:
|
||||
- organization: "task"
|
||||
repository: "{{if not .IsNightly}}task{{end}}"
|
||||
|
||||
13
.vscode/settings-sample.json
vendored
13
.vscode/settings-sample.json
vendored
@@ -1,15 +1,12 @@
|
||||
{
|
||||
"yaml.schemas": {
|
||||
"./website/static/schema.json": [
|
||||
"Taskfile.yml",
|
||||
"tmp/**/*.yml"
|
||||
"./website/src/public/schema.json": [
|
||||
"Taskfile.yml",
|
||||
"Taskfile.yaml",
|
||||
"taskfile.yml",
|
||||
"taskfile.yaml"
|
||||
]
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/versioned_docs": true,
|
||||
"**/versioned_sidesbars": true,
|
||||
"**/i18n": true
|
||||
},
|
||||
"gopls": {
|
||||
"formatting.local": "github.com/go-task"
|
||||
},
|
||||
|
||||
156
CHANGELOG.md
156
CHANGELOG.md
@@ -1,5 +1,161 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Fixed included Taskfiles with `watch: true` not triggering watch mode when
|
||||
called from the root Taskfile (#2686, #1763 by @trulede).
|
||||
- Fixed Remote Git Taskfiles failing on Windows due to backslashes in URL paths
|
||||
(#2656 by @Trim21).
|
||||
- Fixed remote git Taskfiles timing out when resolving includes after accepting
|
||||
the trust prompt (#2669, #2668 by @vmaerten).
|
||||
- Fixed unclear error message when Taskfile search stops at a directory
|
||||
ownership boundary (#2682, #1683 by @trulede).
|
||||
- Fixed global variables from imported Taskfiles not resolving `ref:` values
|
||||
correctly (#2632 by @trulede).
|
||||
- Every `.taskrc.yml` option can now be overridden with a `TASK_`-prefixed
|
||||
environment variable, making CI and container configuration easier (#2607,
|
||||
#1066 by @vmaerten).
|
||||
|
||||
## v3.48.0 - 2026-01-26
|
||||
|
||||
- Fixed `if:` conditions when using to check dynamic variables. Also, skip
|
||||
variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).
|
||||
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
|
||||
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
|
||||
@trulede).
|
||||
- Included Taskfiles with `silent: true` now properly propagate silence to their
|
||||
tasks, while still allowing individual tasks to override with `silent: false`
|
||||
(#2640, #1319 by @trulede).
|
||||
- Added TLS certificate options for Remote Taskfiles: use `--cacert` for
|
||||
self-signed certificates and `--cert`/`--cert-key` for mTLS authentication
|
||||
(#2537, #2242 by @vmaerten).
|
||||
|
||||
## v3.47.0 - 2026-01-24
|
||||
|
||||
- Fixed remote git Taskfiles: cloning now works without explicit ref, and
|
||||
directory includes are properly resolved (#2602 by @vmaerten).
|
||||
- For `output: prefixed`, print `prefix:` if set instead of task name (#1566,
|
||||
#2633 by @trulede).
|
||||
- Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by
|
||||
@trulede).
|
||||
- Task aliases can now contain wildcards and will match accordingly (e.g., `s-*`
|
||||
as alias for `start-*`) (#1900, #2234 by @vmaerten).
|
||||
- Added conditional execution with the `if` field: skip tasks, commands, or task
|
||||
calls based on shell exit codes or template expressions like
|
||||
`{{ eq .ENV "prod" }}` (#2564, #608 by @vmaerten).
|
||||
- Task can now interactively prompt for missing required variables when running
|
||||
in a TTY, with support for enum selection menus. Enable with `--interactive`
|
||||
flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten).
|
||||
|
||||
## v3.46.4 - 2025-12-24
|
||||
|
||||
- Fixed regressions in completion script for Fish (#2591, #2604, #2592 by
|
||||
@WinkelCode).
|
||||
|
||||
## v3.46.3 - 2025-12-19
|
||||
|
||||
- Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten).
|
||||
|
||||
## v3.46.2 - 2025-12-18
|
||||
|
||||
- Fixed a regression on previous release that affected variables passed via
|
||||
command line (#2588, #2589 by @vmaerten).
|
||||
|
||||
## v3.46.1 - 2025-12-18
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- A small behavior change was made to dependencies. Task will now wait for all
|
||||
dependencies to finish running before continuing, even if any of them fail. To
|
||||
opt for the previous behavior, set `failfast: true` either on your
|
||||
`.taskrc.yml` or per task, or use the `--failfast` flag, which will also work
|
||||
for `--parallel` (#1246, #2525 by @andreynering).
|
||||
- The `--summary` flag now displays `vars:` (both global and task-level),
|
||||
`env:`, and `requires:` sections. Dynamic variables show their shell command
|
||||
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
|
||||
@vmaerten).
|
||||
- Improved performance of fuzzy task name matching by implementing lazy
|
||||
initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option
|
||||
to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).
|
||||
- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`
|
||||
and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).
|
||||
- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to
|
||||
skip confirmation prompts for specified hosts when using Remote Taskfiles
|
||||
(#2491, #2473 by @maciejlech).
|
||||
- When running in GitHub Actions, Task now automatically emits error annotations
|
||||
on failure, improving visibility in workflow summaries (#2568 by @vmaerten).
|
||||
- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`
|
||||
variable (#2577, #2479 by @semihbkgr).
|
||||
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
|
||||
flags and dynamic experimental feature detection (#2532 by @vmaerten).
|
||||
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
|
||||
#1944 by @vmaerten).
|
||||
- Shell completion now works when Task is installed or aliased under a different
|
||||
binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).
|
||||
- Some small fixes and improvements were made to `task --init` and to the
|
||||
default Taskfile it generates (#2433 by @andreynering).
|
||||
- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to
|
||||
customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).
|
||||
- Zsh completion now supports zstyle verbose option to show or hide task
|
||||
descriptions (#2571 by @vmaerten).
|
||||
- Task now automatically enables colored output in CI environments (GitHub
|
||||
Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by
|
||||
@vmaerten).
|
||||
- Added color taskrc option to explicitly enable or disable colored output
|
||||
globally (#2569 by @vmaerten).
|
||||
- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication
|
||||
now works out of the box and `applyOf` is properly supported (#2512 by
|
||||
@vmaerten).
|
||||
|
||||
### 🐛 Fixes
|
||||
|
||||
- Fix RPM upload to Cloudsmith by including the version in the filename to
|
||||
ensure unique filenames (#2507 by @vmaerten).
|
||||
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
|
||||
(#2508, #2511 by @trulede).
|
||||
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
|
||||
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
|
||||
@boiledfroginthewell).
|
||||
- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or
|
||||
`run: when_changed` settings (#2566, #1388 by @trulede).
|
||||
- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in
|
||||
root-level vars section (#2403, #2397 by @trulede, @vmaerten).
|
||||
- Fixed a bug where `ignore_error` was ignored when using `task:` to call
|
||||
another task (#2552, #363 by @trulede).
|
||||
- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`
|
||||
flag (#1574, #2574 by @vmaerten).
|
||||
- Fixed Fish completion failing to parse task descriptions containing colons
|
||||
(e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).
|
||||
- Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when
|
||||
using `for` loops in Taskfiles (#2576 by @vmaerten).
|
||||
|
||||
## v3.45.5 - 2025-11-11
|
||||
|
||||
- Fixed bug that made a generic message, instead of an useful one, appear when a
|
||||
Taskfile could not be found (#2431 by @andreynering).
|
||||
- Fixed a bug that caused an error when including a Remote Git Taskfile (#2438
|
||||
by @twelvelabs).
|
||||
- Fixed issue where `.taskrc.yml` was not returned if reading it failed, and
|
||||
corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten).
|
||||
- Improved performance of `--list` and `--list-all` by introducing a faster
|
||||
compilation method that skips source globbing and checksum updates (#1322,
|
||||
#2053 by @vmaerten).
|
||||
- Fixed a concurrency bug with `output: group`. This ensures that begin/end
|
||||
parts won't be mixed up from different tasks (#1208, #2349, #2350 by
|
||||
@trulede).
|
||||
- Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede).
|
||||
- Improve error message when a Taskfile is not found (#2441, #2494 by
|
||||
@vmaerten).
|
||||
- Fixed generic error message `exit status 1` when a dependency task failed
|
||||
(#2286 by @GrahamDennis).
|
||||
- Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork
|
||||
maintained by the official YAML org (#2171, #2434 by @andreynering).
|
||||
- On Windows, the built-in version of the `rm` core utils contains a fix related
|
||||
to the `-f` flag (#2426,
|
||||
[u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464),
|
||||
[mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199), #2506 by
|
||||
@andreynering).
|
||||
|
||||
## v3.45.4 - 2025-09-17
|
||||
|
||||
- Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423
|
||||
|
||||
18
README.md
18
README.md
@@ -3,14 +3,14 @@
|
||||
<img src="website/src/public/img/logo.svg" width="200px" height="200px" />
|
||||
</a>
|
||||
|
||||
<h1>Task</h1>
|
||||
<h1>Task: The Modern Task Runner</h1>
|
||||
|
||||
<p>
|
||||
Task is a task runner / build tool that aims to be simpler and easier to use than, for example, <a href="https://www.gnu.org/software/make/">GNU Make<a>.
|
||||
A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
|
||||
<a href="https://taskfile.dev/docs/installation">Installation</a> | <a href="https://taskfile.dev/docs/getting-started">Getting Started</a> | <a href="https://taskfile.dev/docs/guide">Docs</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
|
||||
</p>
|
||||
|
||||
<h1>Gold Sponsors</h1>
|
||||
@@ -19,7 +19,17 @@
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a target="_blank" href="https://devowl.io">
|
||||
<img src="https://devowl.io/wp-content/uploads/meta/favicon.webp" height="100px" title="devowl.io" />
|
||||
<img src="website/src/public/img/devowl.io.svg" height="100px" width="200px" title="devowl.io" />
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a target="_blank" href="https://goodx.international/">
|
||||
<img src="website/src/public/img/goodx.svg" height="80px" width="200px" title="GoodX" />
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a target="_blank" href="https://magic.dev/">
|
||||
<img src="website/src/public/img/magic.png" height="100px" width="200px" title="Magic" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -29,6 +29,7 @@ tasks:
|
||||
aliases: [i]
|
||||
sources:
|
||||
- './**/*.go'
|
||||
- go.mod
|
||||
cmds:
|
||||
- go install -v ./cmd/task
|
||||
|
||||
@@ -226,7 +227,7 @@ tasks:
|
||||
- "git add --all"
|
||||
- "git commit -m v{{.VERSION}}"
|
||||
- "git push"
|
||||
- "git tag v{{.VERSION}}"
|
||||
- "git tag -a v{{.VERSION}} -m v{{.VERSION}}"
|
||||
- "git push origin tag v{{.VERSION}}"
|
||||
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
|
||||
silent: true
|
||||
|
||||
@@ -121,11 +121,15 @@ func changelog(version *semver.Version) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wrap the changelog content with v-pre directive for VitePress to prevent
|
||||
// Vue from interpreting template syntax like {{.TASK_VERSION}}
|
||||
changelogWithVPre := strings.Replace(changelog, "# Changelog\n\n", "# Changelog\n\n::: v-pre\n\n", 1) + "\n:::"
|
||||
|
||||
// Add the frontmatter to the changelog
|
||||
changelog = fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelog)
|
||||
changelogWithFrontmatter := fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelogWithVPre)
|
||||
|
||||
// Write the changelog to the target file
|
||||
return os.WriteFile(changelogTarget, []byte(changelog), 0o644)
|
||||
return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644)
|
||||
}
|
||||
|
||||
func setVersionFile(fileName string, version *semver.Version) error {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
@@ -28,19 +29,34 @@ func main() {
|
||||
Color: flags.Color,
|
||||
}
|
||||
if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode {
|
||||
emitCIErrorAnnotation(err)
|
||||
l.Errf(logger.Red, "%v\n", err)
|
||||
os.Exit(err.TaskExitCode())
|
||||
}
|
||||
if err, ok := err.(errors.TaskError); ok {
|
||||
emitCIErrorAnnotation(err)
|
||||
l.Errf(logger.Red, "%v\n", err)
|
||||
os.Exit(err.Code())
|
||||
}
|
||||
emitCIErrorAnnotation(err)
|
||||
l.Errf(logger.Red, "%v\n", err)
|
||||
os.Exit(errors.CodeUnknown)
|
||||
}
|
||||
os.Exit(errors.CodeOk)
|
||||
}
|
||||
|
||||
// emitCIErrorAnnotation emits an error annotation for supported CI providers.
|
||||
func emitCIErrorAnnotation(err error) {
|
||||
if isGA, _ := strconv.ParseBool(os.Getenv("GITHUB_ACTIONS")); !isGA {
|
||||
return
|
||||
}
|
||||
if e, ok := err.(*errors.TaskRunError); ok {
|
||||
fmt.Fprintf(os.Stdout, "::error title=Task '%s' failed::%v\n", e.TaskName, e.Err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "::error title=Task failed::%v\n", err)
|
||||
}
|
||||
|
||||
func run() error {
|
||||
log := &logger.Logger{
|
||||
Stdout: os.Stdout,
|
||||
@@ -156,18 +172,23 @@ func run() error {
|
||||
calls = append(calls, &task.Call{Task: "default"})
|
||||
}
|
||||
|
||||
// Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults
|
||||
e.Taskfile.Vars.Merge(globals, nil)
|
||||
|
||||
// Then ReverseMerge special variables so they're available for templating
|
||||
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
|
||||
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
|
||||
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
|
||||
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
|
||||
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
|
||||
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
|
||||
e.Taskfile.Vars.Merge(globals, nil)
|
||||
|
||||
specialVars := ast.NewVars()
|
||||
specialVars.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
|
||||
specialVars.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
|
||||
specialVars.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
|
||||
specialVars.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
|
||||
specialVars.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
|
||||
specialVars.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
|
||||
specialVars.Set("CLI_ASSUME_YES", ast.Var{Value: flags.AssumeYes})
|
||||
e.Taskfile.Vars.ReverseMerge(specialVars, nil)
|
||||
if !flags.Watch {
|
||||
e.InterceptInterruptSignals()
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||
defer cancel()
|
||||
if err := run(ctx); err != nil {
|
||||
fmt.Println(ctx.Err())
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
req, err := http.NewRequest("GET", "https://taskfile.dev/schema.json", nil)
|
||||
if err != nil {
|
||||
fmt.Println(1)
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
fmt.Println(2)
|
||||
return err
|
||||
}
|
||||
fmt.Println(3)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -61,13 +61,14 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
||||
newVar := templater.ReplaceVar(v, cache)
|
||||
// If the variable should not be evaluated, but is nil, set it to an empty string
|
||||
// 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: ""})
|
||||
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
|
||||
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})
|
||||
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
|
||||
return nil
|
||||
}
|
||||
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
|
||||
@@ -113,6 +114,9 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Resolve any outstanding 'Ref' values in global vars (esp. globals from imported Taskfiles).
|
||||
c.TaskfileVars = templater.ReplaceVars(c.TaskfileVars, &templater.Cache{Vars: result})
|
||||
|
||||
if t != nil {
|
||||
for k, v := range t.IncludeVars.All() {
|
||||
if err := rangeFunc(k, v); err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# vim: set tabstop=2 shiftwidth=2 expandtab:
|
||||
|
||||
_GO_TASK_COMPLETION_LIST_OPTION='--list-all'
|
||||
TASK_CMD="${TASK_EXE:-task}"
|
||||
|
||||
function _task()
|
||||
{
|
||||
@@ -21,10 +22,14 @@ function _task()
|
||||
|
||||
# Handle special arguments of options.
|
||||
case "$prev" in
|
||||
-d|--dir)
|
||||
-d|--dir|--remote-cache-dir)
|
||||
_filedir -d
|
||||
return $?
|
||||
;;
|
||||
--cacert|--cert|--cert-key)
|
||||
_filedir
|
||||
return $?
|
||||
;;
|
||||
-t|--taskfile)
|
||||
_filedir yaml || return $?
|
||||
_filedir yml
|
||||
@@ -52,4 +57,4 @@ function _task()
|
||||
__ltrim_colon_completions "$cur"
|
||||
}
|
||||
|
||||
complete -F _task task
|
||||
complete -F _task "$TASK_CMD"
|
||||
|
||||
@@ -1,4 +1,31 @@
|
||||
set -l GO_TASK_PROGNAME task
|
||||
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
|
||||
|
||||
# Cache variables for experiments (global)
|
||||
set -g __task_experiments_cache ""
|
||||
set -g __task_experiments_cache_time 0
|
||||
|
||||
# Helper function to get experiments with 1-second cache
|
||||
function __task_get_experiments
|
||||
set -l now (date +%s)
|
||||
set -l ttl 1 # Cache for 1 second only
|
||||
|
||||
# Return cached value if still valid
|
||||
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
|
||||
printf '%s\n' $__task_experiments_cache
|
||||
return
|
||||
end
|
||||
|
||||
# Refresh cache
|
||||
set -g __task_experiments_cache (task --experiments 2>/dev/null)
|
||||
set -g __task_experiments_cache_time $now
|
||||
printf '%s\n' $__task_experiments_cache
|
||||
end
|
||||
|
||||
# Helper function to check if an experiment is enabled
|
||||
function __task_is_experiment_enabled
|
||||
set -l experiment $argv[1]
|
||||
__task_get_experiments | string match -qr "^\* $experiment:.*on"
|
||||
end
|
||||
|
||||
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
|
||||
# Check if the global task is requested
|
||||
@@ -27,28 +54,67 @@ function __task_get_tasks --description "Prints all available tasks with their d
|
||||
end
|
||||
|
||||
# Grab names and descriptions (if any) of the tasks
|
||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
|
||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):[[:space:]]\{2,\}\(.*\)[[:space:]]\{2,\}(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):[[:space:]]\{2,\}\(.*\)/\1\t\2/'| string split0)
|
||||
if test $output
|
||||
echo $output
|
||||
end
|
||||
end
|
||||
|
||||
complete -c $GO_TASK_PROGNAME -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was
|
||||
specified.' -xa "(__task_get_tasks)"
|
||||
complete -c $GO_TASK_PROGNAME \
|
||||
-d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \
|
||||
-xa "(__task_get_tasks)" \
|
||||
-n "not __fish_seen_subcommand_from --"
|
||||
|
||||
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
|
||||
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'sets directory of execution'
|
||||
complete -c $GO_TASK_PROGNAME -l dry -d 'compiles and prints tasks in the order that they would be run, without executing them'
|
||||
complete -c $GO_TASK_PROGNAME -s f -l force -d 'forces execution even when the task is up-to-date'
|
||||
complete -c $GO_TASK_PROGNAME -s h -l help -d 'shows Task usage'
|
||||
complete -c $GO_TASK_PROGNAME -s i -l init -d 'creates a new Taskfile.yml in the current folder'
|
||||
complete -c $GO_TASK_PROGNAME -s l -l list -d 'lists tasks with description of current Taskfile'
|
||||
complete -c $GO_TASK_PROGNAME -s o -l output -d 'sets output style: [interleaved|group|prefixed]' -xa "interleaved group prefixed"
|
||||
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'executes tasks provided on command line in parallel'
|
||||
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disables echoing'
|
||||
complete -c $GO_TASK_PROGNAME -l status -d 'exits with non-zero exit code if any of the given tasks is not up-to-date'
|
||||
complete -c $GO_TASK_PROGNAME -l summary -d 'show summary about a task'
|
||||
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose which Taskfile to run. Defaults to "Taskfile.yml"'
|
||||
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'enables verbose mode'
|
||||
complete -c $GO_TASK_PROGNAME -l version -d 'show Task version'
|
||||
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'enables watch of the given task'
|
||||
# Standard flags
|
||||
complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks'
|
||||
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
|
||||
complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks'
|
||||
complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell"
|
||||
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution'
|
||||
complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names'
|
||||
complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing'
|
||||
complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command'
|
||||
complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments'
|
||||
complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails'
|
||||
complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date'
|
||||
complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory'
|
||||
complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help'
|
||||
complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile'
|
||||
complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads'
|
||||
complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes'
|
||||
complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON'
|
||||
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
|
||||
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
|
||||
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
|
||||
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
|
||||
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
|
||||
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
|
||||
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
|
||||
complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks'
|
||||
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel'
|
||||
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing'
|
||||
complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none"
|
||||
complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date'
|
||||
complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary'
|
||||
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run'
|
||||
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output'
|
||||
complete -c $GO_TASK_PROGNAME -l version -d 'show version'
|
||||
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes'
|
||||
complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts'
|
||||
|
||||
# Experimental flags (dynamically checked at completion time via -n condition)
|
||||
# GentleForce experiment
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'
|
||||
|
||||
# RemoteTaskfiles experiment - Options
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles'
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
|
||||
|
||||
# RemoteTaskfiles experiment - Operations
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile'
|
||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l clear-cache -d 'clear remote Taskfile cache'
|
||||
|
||||
@@ -5,22 +5,86 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
|
||||
|
||||
if ($commandName.StartsWith('-')) {
|
||||
$completions = @(
|
||||
[CompletionResult]::new('--list-all ', '--list-all ', [CompletionResultType]::ParameterName, 'list all tasks'),
|
||||
[CompletionResult]::new('--color ', '--color', [CompletionResultType]::ParameterName, '--color'),
|
||||
[CompletionResult]::new('--concurrency=', '--concurrency=', [CompletionResultType]::ParameterName, 'concurrency'),
|
||||
[CompletionResult]::new('--interval=', '--interval=', [CompletionResultType]::ParameterName, 'interval'),
|
||||
[CompletionResult]::new('--output=interleaved ', '--output=interleaved', [CompletionResultType]::ParameterName, '--output='),
|
||||
[CompletionResult]::new('--output=group ', '--output=group', [CompletionResultType]::ParameterName, '--output='),
|
||||
[CompletionResult]::new('--output=prefixed ', '--output=prefixed', [CompletionResultType]::ParameterName, '--output='),
|
||||
[CompletionResult]::new('--dry ', '--dry', [CompletionResultType]::ParameterName, '--dry'),
|
||||
[CompletionResult]::new('--force ', '--force', [CompletionResultType]::ParameterName, '--force'),
|
||||
[CompletionResult]::new('--parallel ', '--parallel', [CompletionResultType]::ParameterName, '--parallel'),
|
||||
[CompletionResult]::new('--silent ', '--silent', [CompletionResultType]::ParameterName, '--silent'),
|
||||
[CompletionResult]::new('--status ', '--status', [CompletionResultType]::ParameterName, '--status'),
|
||||
[CompletionResult]::new('--verbose ', '--verbose', [CompletionResultType]::ParameterName, '--verbose'),
|
||||
[CompletionResult]::new('--watch ', '--watch', [CompletionResultType]::ParameterName, '--watch')
|
||||
# Standard flags (alphabetical order)
|
||||
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),
|
||||
[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),
|
||||
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),
|
||||
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),
|
||||
[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
|
||||
[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
|
||||
[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),
|
||||
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),
|
||||
[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),
|
||||
[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),
|
||||
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),
|
||||
[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),
|
||||
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'),
|
||||
[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),
|
||||
[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),
|
||||
[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),
|
||||
[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),
|
||||
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),
|
||||
[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),
|
||||
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),
|
||||
[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),
|
||||
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),
|
||||
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),
|
||||
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),
|
||||
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),
|
||||
[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),
|
||||
[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),
|
||||
[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),
|
||||
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),
|
||||
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),
|
||||
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),
|
||||
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
|
||||
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
|
||||
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
|
||||
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
|
||||
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
|
||||
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
|
||||
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
|
||||
[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),
|
||||
[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),
|
||||
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),
|
||||
[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),
|
||||
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),
|
||||
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),
|
||||
[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),
|
||||
[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),
|
||||
[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),
|
||||
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),
|
||||
[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),
|
||||
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),
|
||||
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),
|
||||
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),
|
||||
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),
|
||||
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),
|
||||
[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),
|
||||
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes')
|
||||
)
|
||||
|
||||
# Experimental flags (dynamically added based on enabled experiments)
|
||||
$experiments = & task --experiments 2>$null | Out-String
|
||||
|
||||
if ($experiments -match '\* GENTLE_FORCE:.*on') {
|
||||
$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')
|
||||
}
|
||||
|
||||
if ($experiments -match '\* REMOTE_TASKFILES:.*on') {
|
||||
# Options
|
||||
$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')
|
||||
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
|
||||
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
|
||||
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
|
||||
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
|
||||
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
|
||||
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
|
||||
# Operations
|
||||
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
|
||||
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')
|
||||
}
|
||||
|
||||
return $completions.Where{ $_.CompletionText.StartsWith($commandName) }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
#compdef task
|
||||
compdef _task task
|
||||
typeset -A opt_args
|
||||
TASK_CMD="${TASK_EXE:-task}"
|
||||
compdef _task "$TASK_CMD"
|
||||
|
||||
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
|
||||
|
||||
# Check if an experiment is enabled
|
||||
function __task_is_experiment_enabled() {
|
||||
local experiment=$1
|
||||
task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on"
|
||||
}
|
||||
|
||||
# Listing commands from Taskfile.yml
|
||||
function __task_list() {
|
||||
local -a scripts cmd
|
||||
local -i enabled=0
|
||||
local taskfile item task desc
|
||||
|
||||
cmd=(task)
|
||||
cmd=($TASK_CMD)
|
||||
taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
|
||||
taskfile=${taskfile//\~/$HOME}
|
||||
|
||||
for arg in "${words[@]:0:$CURRENT}"; do
|
||||
if [[ "$arg" = "--" ]]; then
|
||||
# Use default completion for words after `--` as they are CLI_ARGS.
|
||||
_default
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "$taskfile" && -f "$taskfile" ]]; then
|
||||
cmd+=(--taskfile "$taskfile")
|
||||
fi
|
||||
|
||||
# Check if global flag is set
|
||||
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
|
||||
cmd+=(--global)
|
||||
fi
|
||||
|
||||
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
|
||||
enabled=1
|
||||
@@ -27,38 +45,107 @@ function __task_list() {
|
||||
(( enabled )) || return 0
|
||||
|
||||
scripts=()
|
||||
|
||||
# Read zstyle verbose option (default = true via -T)
|
||||
local show_desc
|
||||
zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false
|
||||
|
||||
for item in "${(@)${(f)output}[2,-1]#\* }"; do
|
||||
task="${item%%:[[:space:]]*}"
|
||||
desc="${item##[^[:space:]]##[[:space:]]##}"
|
||||
scripts+=( "${task//:/\\:}:$desc" )
|
||||
|
||||
if [[ "$show_desc" == "true" ]]; then
|
||||
local desc="${item##[^[:space:]]##[[:space:]]##}"
|
||||
scripts+=( "${task//:/\\:}:$desc" )
|
||||
else
|
||||
scripts+=( "$task" )
|
||||
fi
|
||||
done
|
||||
_describe 'Task to run' scripts
|
||||
|
||||
if [[ "$show_desc" == "true" ]]; then
|
||||
_describe 'Task to run' scripts
|
||||
else
|
||||
compadd -Q -a scripts
|
||||
fi
|
||||
}
|
||||
|
||||
_task() {
|
||||
_arguments \
|
||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
|
||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
|
||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
|
||||
'(-c --color)'{-c,--color}'[colored output]' \
|
||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
|
||||
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
|
||||
'(-s --silent)'{-s,--silent}'[disable echoing]' \
|
||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
|
||||
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
|
||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
|
||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
|
||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
|
||||
+ '(operation)' \
|
||||
{-l,--list}'[list describable tasks]' \
|
||||
{-a,--list-all}'[list all tasks]' \
|
||||
{-i,--init}'[create new Taskfile.yml]' \
|
||||
'(-*)'{-h,--help}'[show help]' \
|
||||
'(-*)--version[show version and exit]' \
|
||||
'*: :__task_list'
|
||||
local -a standard_args operation_args
|
||||
|
||||
standard_args=(
|
||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '
|
||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'
|
||||
'(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'
|
||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]'
|
||||
'(-c --color)'{-c,--color}'[colored output]'
|
||||
'(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'
|
||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'
|
||||
'(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'
|
||||
'(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'
|
||||
'(--dry)--dry[dry-run mode, compile and print tasks only]'
|
||||
'(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'
|
||||
'(--experiments)--experiments[list available experiments]'
|
||||
'(-g --global)'{-g,--global}'[run global Taskfile from home directory]'
|
||||
'(--insecure)--insecure[allow insecure Taskfile downloads]'
|
||||
'(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '
|
||||
'(-j --json)'{-j,--json}'[format task list as JSON]'
|
||||
'(--nested)--nested[nest namespaces when listing as JSON]'
|
||||
'(--no-status)--no-status[ignore status when listing as JSON]'
|
||||
'(--interactive)--interactive[prompt for missing required variables]'
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
|
||||
'(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'
|
||||
'(-s --silent)'{-s,--silent}'[disable echoing]'
|
||||
'(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'
|
||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]'
|
||||
'(--summary)--summary[show summary\: field from tasks instead of running them]'
|
||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'
|
||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]'
|
||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'
|
||||
'(-y --yes)'{-y,--yes}'[assume yes to all prompts]'
|
||||
)
|
||||
|
||||
# Experimental flags (dynamically added based on enabled experiments)
|
||||
# Options (modify behavior)
|
||||
if __task_is_experiment_enabled "GENTLE_FORCE"; then
|
||||
standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')
|
||||
fi
|
||||
|
||||
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
|
||||
standard_args+=(
|
||||
'(--offline --download)--offline[use only local or cached Taskfiles]'
|
||||
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
|
||||
'(--expiry)--expiry[cache expiry duration]:duration: '
|
||||
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
|
||||
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
|
||||
'(--cert)--cert[client certificate for mTLS]:file:_files'
|
||||
'(--cert-key)--cert-key[client certificate private key]:file:_files'
|
||||
)
|
||||
fi
|
||||
|
||||
operation_args=(
|
||||
# Task names completion (can be specified multiple times)
|
||||
'(operation)*: :__task_list'
|
||||
# Operational args completion (mutually exclusive)
|
||||
+ '(operation)'
|
||||
'(*)'{-l,--list}'[list describable tasks]'
|
||||
'(*)'{-a,--list-all}'[list all tasks]'
|
||||
'(*)'{-i,--init}'[create new Taskfile.yml]'
|
||||
'(- *)'{-h,--help}'[show help]'
|
||||
'(- *)--version[show version and exit]'
|
||||
)
|
||||
|
||||
# Experimental operations (dynamically added based on enabled experiments)
|
||||
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
|
||||
standard_args+=(
|
||||
'(--offline --clear-cache)--download[download remote Taskfile]'
|
||||
)
|
||||
operation_args+=(
|
||||
'(* --download)--clear-cache[clear remote Taskfile cache]'
|
||||
)
|
||||
fi
|
||||
|
||||
_arguments -S $standard_args $operation_args
|
||||
}
|
||||
|
||||
# don't run the completion function when being source-ed or eval-ed
|
||||
|
||||
@@ -5,15 +5,12 @@ import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
|
||||
|
||||
type (
|
||||
TaskfileDecodeError struct {
|
||||
Message string
|
||||
@@ -53,10 +50,10 @@ func (err *TaskfileDecodeError) Error() string {
|
||||
if len(te.Errors) > 1 {
|
||||
fmt.Fprintln(buf, color.RedString("errs:"))
|
||||
for _, message := range te.Errors {
|
||||
fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
|
||||
fmt.Fprintln(buf, color.RedString("- %s", message))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0]))
|
||||
}
|
||||
} else {
|
||||
// Otherwise print the error message normally
|
||||
@@ -128,11 +125,3 @@ func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *T
|
||||
err.Snippet = snippet
|
||||
return err
|
||||
}
|
||||
|
||||
func extractTypeErrorMessage(message string) string {
|
||||
matches := typeErrorRegex.FindStringSubmatch(message)
|
||||
if len(matches) == 2 {
|
||||
return matches[1]
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ func (err *TaskRunError) TaskExitCode() int {
|
||||
return err.Code()
|
||||
}
|
||||
|
||||
func (err *TaskRunError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
// TaskInternalError when the user attempts to invoke a task that is internal.
|
||||
type TaskInternalError struct {
|
||||
TaskName string
|
||||
@@ -191,9 +195,9 @@ type TaskNotAllowedVarsError struct {
|
||||
func (err *TaskNotAllowedVarsError) Error() string {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
|
||||
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName)) //nolint:staticcheck
|
||||
for _, s := range err.NotAllowedVars {
|
||||
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
|
||||
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum)) //nolint:staticcheck
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
|
||||
@@ -11,14 +11,21 @@ import (
|
||||
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when
|
||||
// searching the filesystem.
|
||||
type TaskfileNotFoundError struct {
|
||||
URI string
|
||||
Walk bool
|
||||
URI string
|
||||
Walk bool
|
||||
AskInit bool
|
||||
OwnerChange bool
|
||||
}
|
||||
|
||||
func (err TaskfileNotFoundError) Error() string {
|
||||
var walkText string
|
||||
if err.Walk {
|
||||
walkText = " (or any of the parent directories)"
|
||||
if err.OwnerChange {
|
||||
walkText = " (or any of the parent directories until ownership changed)."
|
||||
} else if err.Walk {
|
||||
walkText = " (or any of the parent directories)."
|
||||
}
|
||||
if err.AskInit {
|
||||
walkText += " Run `task --init` to create a new Taskfile."
|
||||
}
|
||||
return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
|
||||
}
|
||||
|
||||
123
executor.go
123
executor.go
@@ -7,7 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
"github.com/sajari/fuzzy"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
@@ -34,19 +34,27 @@ type (
|
||||
Insecure bool
|
||||
Download bool
|
||||
Offline bool
|
||||
TrustedHosts []string
|
||||
Timeout time.Duration
|
||||
CacheExpiryDuration time.Duration
|
||||
RemoteCacheDir string
|
||||
CACert string
|
||||
Cert string
|
||||
CertKey string
|
||||
Watch bool
|
||||
Verbose bool
|
||||
Silent bool
|
||||
DisableFuzzy bool
|
||||
AssumeYes bool
|
||||
AssumeTerm bool // Used for testing
|
||||
Interactive bool
|
||||
Dry bool
|
||||
Summary bool
|
||||
Parallel bool
|
||||
Color bool
|
||||
Concurrency int
|
||||
Interval time.Duration
|
||||
Failfast bool
|
||||
|
||||
// I/O
|
||||
Stdin io.Reader
|
||||
@@ -63,14 +71,16 @@ type (
|
||||
UserWorkingDir string
|
||||
EnableVersionCheck bool
|
||||
|
||||
fuzzyModel *fuzzy.Model
|
||||
fuzzyModel *fuzzy.Model
|
||||
fuzzyModelOnce sync.Once
|
||||
|
||||
promptedVars *ast.Vars // vars collected via interactive prompts
|
||||
concurrencySemaphore chan struct{}
|
||||
taskCallCount map[string]*int32
|
||||
mkdirMutexMap map[string]*sync.Mutex
|
||||
executionHashes map[string]context.Context
|
||||
executionHashesMutex sync.Mutex
|
||||
watchedDirs *xsync.MapOf[string, bool]
|
||||
watchedDirs *xsync.Map[string, bool]
|
||||
}
|
||||
TempDir struct {
|
||||
Remote string
|
||||
@@ -225,6 +235,20 @@ func (o *offlineOption) ApplyToExecutor(e *Executor) {
|
||||
e.Offline = o.offline
|
||||
}
|
||||
|
||||
// WithTrustedHosts configures the [Executor] with a list of trusted hosts for remote
|
||||
// Taskfiles. Hosts in this list will not prompt for user confirmation.
|
||||
func WithTrustedHosts(trustedHosts []string) ExecutorOption {
|
||||
return &trustedHostsOption{trustedHosts}
|
||||
}
|
||||
|
||||
type trustedHostsOption struct {
|
||||
trustedHosts []string
|
||||
}
|
||||
|
||||
func (o *trustedHostsOption) ApplyToExecutor(e *Executor) {
|
||||
e.TrustedHosts = o.trustedHosts
|
||||
}
|
||||
|
||||
// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
|
||||
// default, the timeout is set to 10 seconds.
|
||||
func WithTimeout(timeout time.Duration) ExecutorOption {
|
||||
@@ -240,7 +264,7 @@ func (o *timeoutOption) ApplyToExecutor(e *Executor) {
|
||||
}
|
||||
|
||||
// WithCacheExpiryDuration sets the duration after which the cache is considered
|
||||
// expired. By default, the cache is considered expired after 24 hours.
|
||||
// expired. By default, the cache is 0 (disabled).
|
||||
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
|
||||
return &cacheExpiryDurationOption{duration: duration}
|
||||
}
|
||||
@@ -253,6 +277,58 @@ func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
|
||||
r.CacheExpiryDuration = o.duration
|
||||
}
|
||||
|
||||
// WithRemoteCacheDir sets the directory where remote taskfiles are cached.
|
||||
func WithRemoteCacheDir(dir string) ExecutorOption {
|
||||
return &remoteCacheDirOption{dir: dir}
|
||||
}
|
||||
|
||||
type remoteCacheDirOption struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func (o *remoteCacheDirOption) ApplyToExecutor(e *Executor) {
|
||||
e.RemoteCacheDir = o.dir
|
||||
}
|
||||
|
||||
// WithCACert sets the path to a custom CA certificate for TLS connections.
|
||||
func WithCACert(caCert string) ExecutorOption {
|
||||
return &caCertOption{caCert: caCert}
|
||||
}
|
||||
|
||||
type caCertOption struct {
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (o *caCertOption) ApplyToExecutor(e *Executor) {
|
||||
e.CACert = o.caCert
|
||||
}
|
||||
|
||||
// WithCert sets the path to a client certificate for TLS connections.
|
||||
func WithCert(cert string) ExecutorOption {
|
||||
return &certOption{cert: cert}
|
||||
}
|
||||
|
||||
type certOption struct {
|
||||
cert string
|
||||
}
|
||||
|
||||
func (o *certOption) ApplyToExecutor(e *Executor) {
|
||||
e.Cert = o.cert
|
||||
}
|
||||
|
||||
// WithCertKey sets the path to a client certificate key for TLS connections.
|
||||
func WithCertKey(certKey string) ExecutorOption {
|
||||
return &certKeyOption{certKey: certKey}
|
||||
}
|
||||
|
||||
type certKeyOption struct {
|
||||
certKey string
|
||||
}
|
||||
|
||||
func (o *certKeyOption) ApplyToExecutor(e *Executor) {
|
||||
e.CertKey = o.certKey
|
||||
}
|
||||
|
||||
// WithWatch tells the [Executor] to keep running in the background and watch
|
||||
// for changes to the fingerprint of the tasks that are run. When changes are
|
||||
// detected, a new task run is triggered.
|
||||
@@ -296,6 +372,19 @@ func (o *silentOption) ApplyToExecutor(e *Executor) {
|
||||
e.Silent = o.silent
|
||||
}
|
||||
|
||||
// WithDisableFuzzy tells the [Executor] to disable fuzzy matching for task names.
|
||||
func WithDisableFuzzy(disableFuzzy bool) ExecutorOption {
|
||||
return &disableFuzzyOption{disableFuzzy}
|
||||
}
|
||||
|
||||
type disableFuzzyOption struct {
|
||||
disableFuzzy bool
|
||||
}
|
||||
|
||||
func (o *disableFuzzyOption) ApplyToExecutor(e *Executor) {
|
||||
e.DisableFuzzy = o.disableFuzzy
|
||||
}
|
||||
|
||||
// WithAssumeYes tells the [Executor] to assume "yes" for all prompts.
|
||||
func WithAssumeYes(assumeYes bool) ExecutorOption {
|
||||
return &assumeYesOption{assumeYes}
|
||||
@@ -322,6 +411,19 @@ func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
|
||||
e.AssumeTerm = o.assumeTerm
|
||||
}
|
||||
|
||||
// WithInteractive tells the [Executor] to prompt for missing required variables.
|
||||
func WithInteractive(interactive bool) ExecutorOption {
|
||||
return &interactiveOption{interactive}
|
||||
}
|
||||
|
||||
type interactiveOption struct {
|
||||
interactive bool
|
||||
}
|
||||
|
||||
func (o *interactiveOption) ApplyToExecutor(e *Executor) {
|
||||
e.Interactive = o.interactive
|
||||
}
|
||||
|
||||
// WithDry tells the [Executor] to output the commands that would be run without
|
||||
// actually running them.
|
||||
func WithDry(dry bool) ExecutorOption {
|
||||
@@ -502,3 +604,16 @@ type versionCheckOption struct {
|
||||
func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
|
||||
e.EnableVersionCheck = o.enableVersionCheck
|
||||
}
|
||||
|
||||
// WithFailfast tells the [Executor] whether or not to check the version of
|
||||
func WithFailfast(failfast bool) ExecutorOption {
|
||||
return &failfastOption{failfast}
|
||||
}
|
||||
|
||||
type failfastOption struct {
|
||||
failfast bool
|
||||
}
|
||||
|
||||
func (o *failfastOption) ApplyToExecutor(e *Executor) {
|
||||
e.Failfast = o.failfast
|
||||
}
|
||||
|
||||
213
executor_test.go
213
executor_test.go
@@ -143,12 +143,12 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
||||
t.Helper()
|
||||
f := func(t *testing.T) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
var buffer SyncBuffer
|
||||
|
||||
opts := append(
|
||||
tt.executorOpts,
|
||||
task.WithStdout(&buf),
|
||||
task.WithStderr(&buf),
|
||||
task.WithStdout(&buffer),
|
||||
task.WithStderr(&buffer),
|
||||
)
|
||||
|
||||
// If the test has input, create a reader for it and add it to the
|
||||
@@ -171,7 +171,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
||||
if err := e.Setup(); tt.wantSetupError {
|
||||
require.Error(t, err)
|
||||
tt.writeFixtureErrSetup(t, g, err)
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
tt.writeFixtureBuffer(t, g, buffer.buf)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
@@ -192,7 +192,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
||||
if err := e.Run(ctx, call); tt.wantRunError {
|
||||
require.Error(t, err)
|
||||
tt.writeFixtureErrRun(t, g, err)
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
tt.writeFixtureBuffer(t, g, buffer.buf)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
@@ -205,7 +205,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
tt.writeFixtureBuffer(t, g, buf)
|
||||
tt.writeFixtureBuffer(t, g, buffer.buf)
|
||||
}
|
||||
|
||||
// Run the test (with a name if it has one)
|
||||
@@ -263,6 +263,23 @@ func TestVars(t *testing.T) {
|
||||
task.WithSilent(true),
|
||||
),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("cli-var-priority-default"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/vars"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("cli-var-priority"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("cli-var-priority-override"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/vars"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithTask("cli-var-priority"),
|
||||
WithVar("CLI_VAR", "from_cli"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestRequires(t *testing.T) {
|
||||
@@ -347,6 +364,7 @@ func TestSpecialVars(t *testing.T) {
|
||||
// Root
|
||||
"print-task",
|
||||
"print-root-dir",
|
||||
"print-root-taskfile",
|
||||
"print-taskfile",
|
||||
"print-taskfile-dir",
|
||||
"print-task-dir",
|
||||
@@ -621,6 +639,30 @@ func TestAlias(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
func TestSummaryWithVarsAndRequires(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test basic case from prompt.md - vars and requires
|
||||
NewExecutorTest(t,
|
||||
WithName("vars-and-requires"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/summary-vars-requires"),
|
||||
task.WithSummary(true),
|
||||
),
|
||||
WithTask("mytask"),
|
||||
)
|
||||
|
||||
// Test with shell variables
|
||||
NewExecutorTest(t,
|
||||
WithName("shell-vars"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/summary-vars-requires"),
|
||||
task.WithSummary(true),
|
||||
),
|
||||
WithTask("with-sh-var"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestLabel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -665,6 +707,36 @@ func TestLabel(t *testing.T) {
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("label in error"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/label_error"),
|
||||
),
|
||||
WithTask("foo"),
|
||||
WithRunError(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("up to date"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/prefix_uptodate"),
|
||||
task.WithOutputStyle(ast.Output{Name: "prefixed"}),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("up to dat with no output style"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/prefix_uptodate"),
|
||||
),
|
||||
WithTask("foo"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestPromptInSummary(t *testing.T) {
|
||||
@@ -855,6 +927,10 @@ func TestReference(t *testing.T) {
|
||||
name: "reference using templating resolver and dynamic var",
|
||||
call: "ref-resolver-sh",
|
||||
},
|
||||
{
|
||||
name: "reference using templating resolver and global var",
|
||||
call: "ref-global",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -987,3 +1063,128 @@ func TestIncludeChecksum(t *testing.T) {
|
||||
WithFixtureTemplating(),
|
||||
)
|
||||
}
|
||||
|
||||
func TestIncludeSilent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("include-taskfile-silent"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/includes_silent"),
|
||||
),
|
||||
WithTask("default"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestFailfast(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("default"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/failfast/default"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
WithRunError(),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Option", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("default"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/failfast/default"),
|
||||
task.WithSilent(true),
|
||||
task.WithFailfast(true),
|
||||
),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
WithRunError(),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("Task", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("task"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/failfast/task"),
|
||||
task.WithSilent(true),
|
||||
),
|
||||
WithPostProcessFn(PPSortedLines),
|
||||
WithRunError(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIf(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
task string
|
||||
vars map[string]any
|
||||
verbose bool
|
||||
}{
|
||||
// Basic command-level if
|
||||
{name: "cmd-if-true", task: "cmd-if-true"},
|
||||
{name: "cmd-if-false", task: "cmd-if-false"},
|
||||
|
||||
// Task-level if
|
||||
{name: "task-if-true", task: "task-if-true"},
|
||||
{name: "task-if-false", task: "task-if-false", verbose: true},
|
||||
|
||||
// Task call with if
|
||||
{name: "task-call-if-true", task: "task-call-if-true"},
|
||||
{name: "task-call-if-false", task: "task-call-if-false", verbose: true},
|
||||
|
||||
// Go template conditions
|
||||
{name: "template-eq-true", task: "template-eq-true"},
|
||||
{name: "template-eq-false", task: "template-eq-false", verbose: true},
|
||||
{name: "template-ne", task: "template-ne"},
|
||||
{name: "template-bool-true", task: "template-bool-true"},
|
||||
{name: "template-bool-false", task: "template-bool-false"},
|
||||
{name: "template-direct-true", task: "template-direct-true"},
|
||||
{name: "template-direct-false", task: "template-direct-false"},
|
||||
{name: "template-and", task: "template-and"},
|
||||
{name: "template-or", task: "template-or"},
|
||||
|
||||
// CLI variable override
|
||||
{name: "template-cli-var", task: "template-cli-var", vars: map[string]any{"MY_VAR": "yes"}},
|
||||
|
||||
// Task-level if with template
|
||||
{name: "task-level-template", task: "task-level-template"},
|
||||
{name: "task-level-template-false", task: "task-level-template-false", verbose: true},
|
||||
|
||||
// For loop with if
|
||||
{name: "if-in-for-loop", task: "if-in-for-loop", verbose: true},
|
||||
|
||||
// Task-level if with dynamic variable
|
||||
{name: "task-if-dynamic-true", task: "task-if-dynamic-true"},
|
||||
{name: "task-if-dynamic-false", task: "task-if-dynamic-false", verbose: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
opts := []ExecutorTestOption{
|
||||
WithName(test.name),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/if"),
|
||||
task.WithSilent(true),
|
||||
task.WithVerbose(test.verbose),
|
||||
),
|
||||
WithTask(test.task),
|
||||
}
|
||||
if test.vars != nil {
|
||||
for k, v := range test.vars {
|
||||
opts = append(opts, WithVar(k, v))
|
||||
}
|
||||
}
|
||||
NewExecutorTest(t, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,12 @@ var xList []Experiment
|
||||
|
||||
func Parse(dir string) {
|
||||
config, _ := taskrc.GetConfig(dir)
|
||||
|
||||
ParseWithConfig(dir, config)
|
||||
}
|
||||
|
||||
func ParseWithConfig(dir string, config *ast.TaskRC) {
|
||||
// Read any .env files
|
||||
readDotEnv(dir)
|
||||
|
||||
// Initialize the experiments
|
||||
GentleForce = New("GENTLE_FORCE", config, 1)
|
||||
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)
|
||||
|
||||
144
go.mod
144
go.mod
@@ -1,66 +1,140 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.24.0
|
||||
go 1.24.6
|
||||
|
||||
toolchain go1.26.0
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0-rc.1
|
||||
charm.land/bubbletea/v2 v2.0.0-rc.2
|
||||
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
|
||||
github.com/Ladicle/tabwriter v1.0.0
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/alecthomas/chroma/v2 v2.20.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/chainguard-dev/git-urls v1.0.2
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
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.18.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-git/go-billy/v5 v5.6.2
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
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.4
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0
|
||||
github.com/sajari/fuzzy v1.0.0
|
||||
github.com/sebdah/goldie/v2 v2.7.1
|
||||
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.0.2
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/term v0.35.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20250915182820-b717ad599e17
|
||||
mvdan.cc/sh/v3 v3.12.0
|
||||
github.com/zeebo/xxh3 v1.1.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.40.0
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
|
||||
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
cloud.google.com/go/storage v1.58.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // 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.3.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.1 // 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.5.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.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.19 // 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
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 // indirect
|
||||
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
351
go.sum
351
go.sum
@@ -1,85 +1,194 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM=
|
||||
charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
|
||||
charm.land/bubbletea/v2 v2.0.0-rc.2 h1:TdTbUOFzbufDJmSz/3gomL6q+fR6HwfY+P13hXQzD7k=
|
||||
charm.land/bubbletea/v2 v2.0.0-rc.2/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
|
||||
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 h1:059k1h5vvZ4ASinki9nmBguxu9Rq0UDDSa6q8LOUphk=
|
||||
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
|
||||
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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
|
||||
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
||||
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
|
||||
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
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.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/alecthomas/chroma/v2 v2.23.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0EofhbyKhhk=
|
||||
github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
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/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=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
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/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 h1:7Rs87fbKJoIIxsQS8YKJYGYa0tlsDwwb0twQjV1KB+g=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38/go.mod h1:6lfcr3MNP+kZR25sF1nQwJFuQnNYBlFy3PGX5rvslXc=
|
||||
github.com/charmbracelet/x/ansi v0.11.1 h1:iXAC8SyMQDJgtcz9Jnw+HU8WMEctHzoTAETIeA3JXMk=
|
||||
github.com/charmbracelet/x/ansi v0.11.1/go.mod h1:M49wjzpIujwPceJ+t5w3qh2i87+HRtHohgb5iTyepL0=
|
||||
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=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
|
||||
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
|
||||
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
|
||||
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
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/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=
|
||||
@@ -89,109 +198,139 @@ 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/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=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
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.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
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/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=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
|
||||
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
|
||||
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 h1:ax+jBy7xFhh+Ka0IGLmH5mft+YDuqvzEjSgWuAP0nsM=
|
||||
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7/go.mod h1:/0Qr7qJeDwWxoKku2xKQ4Szc+SwBE3g9VE8jNiamsmc=
|
||||
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA=
|
||||
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
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.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
|
||||
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
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-20250807215248-5a1a658912aa h1:sRmA9AmA5+9CbK6a7N52q9W9jAeoBy1EJ7cncm+SLxw=
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20250807215248-5a1a658912aa/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20250915182820-b717ad599e17 h1:2FU24GcRtL5Idt1KOtmzxU3RiXwirUQQUTV0voIHI2g=
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20250915182820-b717ad599e17/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
|
||||
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
|
||||
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
|
||||
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.12.1-0.20260124232039-e74afc18e65b h1:PUPnLxbDzRO9kg/03l7TZk7+ywTv7FxmOhDHOtOdOtk=
|
||||
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b/go.mod h1:mencVHx2sy9XZG5wJbCA9nRUOE3zvMtoRXOmXMxH7sc=
|
||||
|
||||
25
init.go
25
init.go
@@ -6,9 +6,10 @@ import (
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
const defaultTaskFilename = "Taskfile.yml"
|
||||
const defaultFilename = "Taskfile.yml"
|
||||
|
||||
//go:embed taskfile/templates/default.yml
|
||||
var DefaultTaskfile string
|
||||
@@ -20,22 +21,30 @@ var DefaultTaskfile string
|
||||
//
|
||||
// The final file path is always returned and may be different from the input path.
|
||||
func InitTaskfile(path string) (string, error) {
|
||||
fi, err := os.Stat(path)
|
||||
if err == nil && !fi.IsDir() {
|
||||
info, err := os.Stat(path)
|
||||
if err == nil && !info.IsDir() {
|
||||
return path, errors.TaskfileAlreadyExistsError{}
|
||||
}
|
||||
|
||||
if fi != nil && fi.IsDir() {
|
||||
path = filepathext.SmartJoin(path, defaultTaskFilename)
|
||||
// path was a directory, so check if Taskfile.yml exists in it
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
if info != nil && info.IsDir() {
|
||||
// path was a directory, check if there is a Taskfile already
|
||||
if hasDefaultTaskfile(path) {
|
||||
return path, errors.TaskfileAlreadyExistsError{}
|
||||
}
|
||||
path = filepathext.SmartJoin(path, defaultFilename)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func hasDefaultTaskfile(dir string) bool {
|
||||
for _, name := range taskfile.DefaultTaskfiles {
|
||||
if _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,15 @@ type Copier[T any] interface {
|
||||
DeepCopy() T
|
||||
}
|
||||
|
||||
func Scalar[T any](orig *T) *T {
|
||||
if orig == nil {
|
||||
return nil
|
||||
} else {
|
||||
v := *orig
|
||||
return &v
|
||||
}
|
||||
}
|
||||
|
||||
func Slice[T any](orig []T) []T {
|
||||
if orig == nil {
|
||||
return nil
|
||||
|
||||
62
internal/env/env.go
vendored
62
internal/env/env.go
vendored
@@ -3,7 +3,9 @@ package env
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
@@ -61,3 +63,63 @@ func isTypeAllowed(v any) bool {
|
||||
func GetTaskEnv(key string) string {
|
||||
return os.Getenv(taskVarPrefix + key)
|
||||
}
|
||||
|
||||
// GetTaskEnvBool returns the boolean value of a TASK_ prefixed env var.
|
||||
// Returns the value and true if set and valid, or false and false if not set or invalid.
|
||||
func GetTaskEnvBool(key string) (bool, bool) {
|
||||
v := GetTaskEnv(key)
|
||||
if v == "" {
|
||||
return false, false
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
return b, err == nil
|
||||
}
|
||||
|
||||
// GetTaskEnvInt returns the integer value of a TASK_ prefixed env var.
|
||||
// Returns the value and true if set and valid, or 0 and false if not set or invalid.
|
||||
func GetTaskEnvInt(key string) (int, bool) {
|
||||
v := GetTaskEnv(key)
|
||||
if v == "" {
|
||||
return 0, false
|
||||
}
|
||||
i, err := strconv.Atoi(v)
|
||||
return i, err == nil
|
||||
}
|
||||
|
||||
// GetTaskEnvDuration returns the duration value of a TASK_ prefixed env var.
|
||||
// Returns the value and true if set and valid, or 0 and false if not set or invalid.
|
||||
func GetTaskEnvDuration(key string) (time.Duration, bool) {
|
||||
v := GetTaskEnv(key)
|
||||
if v == "" {
|
||||
return 0, false
|
||||
}
|
||||
d, err := time.ParseDuration(v)
|
||||
return d, err == nil
|
||||
}
|
||||
|
||||
// GetTaskEnvString returns the string value of a TASK_ prefixed env var.
|
||||
// Returns the value and true if set (non-empty), or empty string and false if not set.
|
||||
func GetTaskEnvString(key string) (string, bool) {
|
||||
v := GetTaskEnv(key)
|
||||
return v, v != ""
|
||||
}
|
||||
|
||||
// GetTaskEnvStringSlice returns a comma-separated list from a TASK_ prefixed env var.
|
||||
// Returns the slice and true if set (non-empty), or nil and false if not set.
|
||||
func GetTaskEnvStringSlice(key string) ([]string, bool) {
|
||||
v := GetTaskEnv(key)
|
||||
if v == "" {
|
||||
return nil, false
|
||||
}
|
||||
parts := strings.Split(v, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if trimmed := strings.TrimSpace(p); trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
@@ -127,12 +127,8 @@ func ExpandFields(s string) ([]string, error) {
|
||||
s = escape(s)
|
||||
p := syntax.NewParser()
|
||||
var words []*syntax.Word
|
||||
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
|
||||
for w := range p.WordsSeq(strings.NewReader(s)) {
|
||||
words = append(words, w)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := &expand.Config{
|
||||
Env: expand.FuncEnviron(os.Getenv),
|
||||
@@ -147,7 +143,7 @@ func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHan
|
||||
if useGoCoreUtils {
|
||||
handlers = append(handlers, coreutils.ExecHandler)
|
||||
}
|
||||
return
|
||||
return handlers
|
||||
}
|
||||
|
||||
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
|
||||
|
||||
@@ -5,13 +5,16 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
"github.com/go-task/task/v3/taskrc"
|
||||
@@ -58,6 +61,7 @@ var (
|
||||
Watch bool
|
||||
Verbose bool
|
||||
Silent bool
|
||||
DisableFuzzy bool
|
||||
AssumeYes bool
|
||||
Dry bool
|
||||
Summary bool
|
||||
@@ -69,13 +73,20 @@ var (
|
||||
Output ast.Output
|
||||
Color bool
|
||||
Interval time.Duration
|
||||
Failfast bool
|
||||
Global bool
|
||||
Experiments bool
|
||||
Download bool
|
||||
Offline bool
|
||||
TrustedHosts []string
|
||||
ClearCache bool
|
||||
Timeout time.Duration
|
||||
CacheExpiryDuration time.Duration
|
||||
RemoteCacheDir string
|
||||
CACert string
|
||||
Cert string
|
||||
CertKey string
|
||||
Interactive bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -119,13 +130,15 @@ func init() {
|
||||
pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.")
|
||||
pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON")
|
||||
pflag.BoolVar(&Nested, "nested", false, "Nest namespaces when listing tasks as JSON")
|
||||
pflag.BoolVar(&Insecure, "insecure", getConfig(config, func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.")
|
||||
pflag.BoolVar(&Insecure, "insecure", getConfig(config, "REMOTE_INSECURE", func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.")
|
||||
pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.")
|
||||
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.")
|
||||
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
|
||||
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
|
||||
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, "VERBOSE", func() *bool { return config.Verbose }, false), "Enables verbose mode.")
|
||||
pflag.BoolVarP(&Silent, "silent", "s", getConfig(config, "SILENT", func() *bool { return config.Silent }, false), "Disables echoing.")
|
||||
pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, "DISABLE_FUZZY", func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
|
||||
pflag.BoolVarP(&AssumeYes, "yes", "y", getConfig(config, "ASSUME_YES", func() *bool { return nil }, false), "Assume \"yes\" as answer to all prompts.")
|
||||
pflag.BoolVar(&Interactive, "interactive", getConfig(config, "INTERACTIVE", func() *bool { return config.Interactive }, false), "Prompt for missing required variables.")
|
||||
pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
|
||||
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
|
||||
pflag.BoolVarP(&Dry, "dry", "n", getConfig(config, "DRY", func() *bool { return nil }, false), "Compiles and prints tasks in the order that they would be run, without executing them.")
|
||||
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
|
||||
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.")
|
||||
@@ -134,9 +147,10 @@ func init() {
|
||||
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.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
|
||||
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
|
||||
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.")
|
||||
pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, "FAILFAST", func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.")
|
||||
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
|
||||
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
|
||||
|
||||
@@ -151,12 +165,40 @@ func init() {
|
||||
// Remote Taskfiles experiment will adds the "download" and "offline" flags
|
||||
if experiments.RemoteTaskfiles.Enabled() {
|
||||
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
|
||||
pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
|
||||
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
|
||||
pflag.BoolVar(&Offline, "offline", getConfig(config, "REMOTE_OFFLINE", func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
|
||||
pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, "REMOTE_TRUSTED_HOSTS", func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).")
|
||||
pflag.DurationVar(&Timeout, "timeout", getConfig(config, "REMOTE_TIMEOUT", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
|
||||
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
|
||||
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
|
||||
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
|
||||
pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, "REMOTE_CACHE_DIR", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
|
||||
pflag.StringVar(&CACert, "cacert", getConfig(config, "REMOTE_CACERT", func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
|
||||
pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
|
||||
pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
|
||||
}
|
||||
pflag.Parse()
|
||||
|
||||
// Auto-detect color based on environment when not explicitly configured
|
||||
// Priority: CLI flag > TASK_COLOR env > taskrc config > NO_COLOR > FORCE_COLOR/CI > default
|
||||
colorExplicitlySet := pflag.Lookup("color").Changed || env.GetTaskEnv("COLOR") != "" || (config != nil && config.Color != nil)
|
||||
if !colorExplicitlySet {
|
||||
if os.Getenv("NO_COLOR") != "" {
|
||||
Color = false
|
||||
color.NoColor = true
|
||||
} else if os.Getenv("FORCE_COLOR") != "" || isCI() {
|
||||
Color = true
|
||||
color.NoColor = false // Force colors even without TTY
|
||||
}
|
||||
// Otherwise, let fatih/color auto-detect TTY
|
||||
} else {
|
||||
// Explicit config: sync with fatih/color
|
||||
color.NoColor = !Color
|
||||
}
|
||||
}
|
||||
|
||||
// isCI returns true if running in a CI environment
|
||||
func isCI() bool {
|
||||
ci, _ := strconv.ParseBool(os.Getenv("CI"))
|
||||
return ci
|
||||
}
|
||||
|
||||
func Validate() error {
|
||||
@@ -200,6 +242,11 @@ func Validate() error {
|
||||
return errors.New("task: --nested only applies to --json with --list or --list-all")
|
||||
}
|
||||
|
||||
// Validate certificate flags
|
||||
if (Cert != "" && CertKey == "") || (Cert == "" && CertKey != "") {
|
||||
return errors.New("task: --cert and --cert-key must be provided together")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -238,12 +285,19 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
|
||||
task.WithInsecure(Insecure),
|
||||
task.WithDownload(Download),
|
||||
task.WithOffline(Offline),
|
||||
task.WithTrustedHosts(TrustedHosts),
|
||||
task.WithTimeout(Timeout),
|
||||
task.WithCacheExpiryDuration(CacheExpiryDuration),
|
||||
task.WithRemoteCacheDir(RemoteCacheDir),
|
||||
task.WithCACert(CACert),
|
||||
task.WithCert(Cert),
|
||||
task.WithCertKey(CertKey),
|
||||
task.WithWatch(Watch),
|
||||
task.WithVerbose(Verbose),
|
||||
task.WithSilent(Silent),
|
||||
task.WithDisableFuzzy(DisableFuzzy),
|
||||
task.WithAssumeYes(AssumeYes),
|
||||
task.WithInteractive(Interactive),
|
||||
task.WithDry(Dry || Status),
|
||||
task.WithSummary(Summary),
|
||||
task.WithParallel(Parallel),
|
||||
@@ -253,18 +307,49 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
|
||||
task.WithOutputStyle(Output),
|
||||
task.WithTaskSorter(sorter),
|
||||
task.WithVersionCheck(true),
|
||||
task.WithFailfast(Failfast),
|
||||
)
|
||||
}
|
||||
|
||||
// getConfig extracts a config value directly from a pointer field with a fallback default
|
||||
func getConfig[T any](config *taskrcast.TaskRC, fieldFunc func() *T, fallback T) T {
|
||||
if config == nil {
|
||||
return fallback
|
||||
// getConfig extracts a config value with priority: env var > taskrc config > fallback
|
||||
func getConfig[T any](config *taskrcast.TaskRC, envKey string, fieldFunc func() *T, fallback T) T {
|
||||
if envKey != "" {
|
||||
if val, ok := getEnvAs[T](envKey); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
field := fieldFunc()
|
||||
if field != nil {
|
||||
return *field
|
||||
if config != nil {
|
||||
if field := fieldFunc(); field != nil {
|
||||
return *field
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// getEnvAs parses a TASK_ prefixed env var as type T
|
||||
func getEnvAs[T any](envKey string) (T, bool) {
|
||||
var zero T
|
||||
switch any(zero).(type) {
|
||||
case bool:
|
||||
if val, ok := env.GetTaskEnvBool(envKey); ok {
|
||||
return any(val).(T), true
|
||||
}
|
||||
case int:
|
||||
if val, ok := env.GetTaskEnvInt(envKey); ok {
|
||||
return any(val).(T), true
|
||||
}
|
||||
case time.Duration:
|
||||
if val, ok := env.GetTaskEnvDuration(envKey); ok {
|
||||
return any(val).(T), true
|
||||
}
|
||||
case string:
|
||||
if val, ok := env.GetTaskEnvString(envKey); ok {
|
||||
return any(val).(T), true
|
||||
}
|
||||
case []string:
|
||||
if val, ok := env.GetTaskEnvStringSlice(envKey); ok {
|
||||
return any(val).(T), true
|
||||
}
|
||||
}
|
||||
return zero, false
|
||||
}
|
||||
|
||||
@@ -108,10 +108,10 @@ func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, er
|
||||
}
|
||||
}
|
||||
paths, err := SearchNPathRecursively(dir, possibleFilenames, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(entrypoints, paths...), nil
|
||||
// The call to SearchNPathRecursively is ambiguous and may return
|
||||
// os.ErrPermission if its search ends, however it may have still
|
||||
// returned valid paths. Caller may choose to ignore that error.
|
||||
return append(entrypoints, paths...), err
|
||||
}
|
||||
|
||||
// SearchPath will check if a file at the given path exists or not. If it does,
|
||||
@@ -188,9 +188,12 @@ func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Error if we reached the root directory and still haven't found a file
|
||||
// OR if the user id of the directory changes
|
||||
if path == parentPath || (parentOwner != owner) {
|
||||
// If the user id of the directory changes indicate a permission error, otherwise
|
||||
// the calling code will infer an error condition based on the accumulated
|
||||
// contents of paths.
|
||||
if parentOwner != owner {
|
||||
return paths, os.ErrPermission
|
||||
} else if path == parentPath {
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@ func Name(t *ast.Task) (string, error) {
|
||||
|
||||
func Hash(t *ast.Task) (string, error) {
|
||||
h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil)
|
||||
return fmt.Sprintf("%s:%d", t.Task, h), err
|
||||
return fmt.Sprintf("%s:%s:%d", t.Location.Taskfile, t.LocalName(), h), err
|
||||
}
|
||||
|
||||
211
internal/input/input.go
Normal file
211
internal/input/input.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
var ErrCancelled = errors.New("prompt cancelled")
|
||||
|
||||
var (
|
||||
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
|
||||
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
|
||||
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true) // green bold
|
||||
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray
|
||||
)
|
||||
|
||||
// Prompter handles interactive variable prompting
|
||||
type Prompter struct {
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
// Text prompts the user for a text value
|
||||
func (p *Prompter) Text(varName string) (string, error) {
|
||||
m := newTextModel(varName)
|
||||
|
||||
prog := tea.NewProgram(m,
|
||||
tea.WithInput(p.Stdin),
|
||||
tea.WithOutput(p.Stderr),
|
||||
)
|
||||
|
||||
result, err := prog.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
model := result.(textModel)
|
||||
if model.cancelled {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
|
||||
return model.value, nil
|
||||
}
|
||||
|
||||
// Select prompts the user to select from a list of options
|
||||
func (p *Prompter) Select(varName string, options []string) (string, error) {
|
||||
if len(options) == 0 {
|
||||
return "", errors.New("no options provided")
|
||||
}
|
||||
|
||||
m := newSelectModel(varName, options)
|
||||
|
||||
prog := tea.NewProgram(m,
|
||||
tea.WithInput(p.Stdin),
|
||||
tea.WithOutput(p.Stderr),
|
||||
)
|
||||
|
||||
result, err := prog.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
model := result.(selectModel)
|
||||
if model.cancelled {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
|
||||
return model.options[model.cursor], nil
|
||||
}
|
||||
|
||||
// Prompt prompts for a variable value, using Select if enum is provided, Text otherwise
|
||||
func (p *Prompter) Prompt(varName string, enum []string) (string, error) {
|
||||
if len(enum) > 0 {
|
||||
return p.Select(varName, enum)
|
||||
}
|
||||
return p.Text(varName)
|
||||
}
|
||||
|
||||
// textModel is the Bubble Tea model for text input
|
||||
type textModel struct {
|
||||
varName string
|
||||
textInput textinput.Model
|
||||
value string
|
||||
cancelled bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newTextModel(varName string) textModel {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.CharLimit = 256
|
||||
ti.SetWidth(40)
|
||||
ti.Focus()
|
||||
|
||||
return textModel{
|
||||
varName: varName,
|
||||
textInput: ti,
|
||||
}
|
||||
}
|
||||
|
||||
func (m textModel) Init() tea.Cmd {
|
||||
return tea.Batch(m.textInput.Focus(), textinput.Blink)
|
||||
}
|
||||
|
||||
func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.Keystroke() {
|
||||
case "ctrl+c", "escape":
|
||||
m.cancelled = true
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
m.value = m.textInput.Value()
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m textModel) View() tea.View {
|
||||
if m.done {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", m.varName))
|
||||
return tea.NewView(prompt + m.textInput.View() + "\n")
|
||||
}
|
||||
|
||||
// selectModel is the Bubble Tea model for selection
|
||||
type selectModel struct {
|
||||
varName string
|
||||
options []string
|
||||
cursor int
|
||||
cancelled bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newSelectModel(varName string, options []string) selectModel {
|
||||
return selectModel{
|
||||
varName: varName,
|
||||
options: options,
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m selectModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.Keystroke() {
|
||||
case "ctrl+c", "escape":
|
||||
m.cancelled = true
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
case "up", "shift+tab", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
case "down", "tab", "j":
|
||||
if m.cursor < len(m.options)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
case "enter":
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m selectModel) View() tea.View {
|
||||
if m.done {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", m.varName)))
|
||||
b.WriteString("\n")
|
||||
|
||||
for i, opt := range m.options {
|
||||
if i == m.cursor {
|
||||
b.WriteString(cursorStyle.Render("❯ "))
|
||||
b.WriteString(selectedStyle.Render(opt))
|
||||
} else {
|
||||
b.WriteString(" " + opt)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(dimStyle.Render(" (↑/↓ to move, enter to select, esc to cancel)"))
|
||||
|
||||
return tea.NewView(b.String())
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package logger
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -43,6 +42,12 @@ type (
|
||||
PrintFunc func(io.Writer, string, ...any)
|
||||
)
|
||||
|
||||
func None() PrintFunc {
|
||||
c := color.New()
|
||||
c.DisableColor()
|
||||
return c.FprintfFunc()
|
||||
}
|
||||
|
||||
func Default() PrintFunc {
|
||||
return color.New(attrsReset...).FprintfFunc()
|
||||
}
|
||||
@@ -96,10 +101,6 @@ func BrightRed() PrintFunc {
|
||||
}
|
||||
|
||||
func envColor(name string, defaultColor color.Attribute) []color.Attribute {
|
||||
if os.Getenv("FORCE_COLOR") != "" {
|
||||
color.NoColor = false
|
||||
}
|
||||
|
||||
// Fetch the environment variable
|
||||
override := env.GetTaskEnv(name)
|
||||
|
||||
@@ -149,7 +150,7 @@ func (l *Logger) FOutf(w io.Writer, color Color, s string, args ...any) {
|
||||
s, args = "%s", []any{s}
|
||||
}
|
||||
if !l.Color {
|
||||
color = Default
|
||||
color = None
|
||||
}
|
||||
print := color()
|
||||
print(w, s, args...)
|
||||
@@ -168,7 +169,7 @@ func (l *Logger) Errf(color Color, s string, args ...any) {
|
||||
s, args = "%s", []any{s}
|
||||
}
|
||||
if !l.Color {
|
||||
color = Default
|
||||
color = None
|
||||
}
|
||||
print := color()
|
||||
print(l.Stderr, s, args...)
|
||||
|
||||
@@ -24,7 +24,6 @@ func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, cache *templater.Cache)
|
||||
if g.ErrorOnly && err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gw.close()
|
||||
}
|
||||
}
|
||||
@@ -40,14 +39,22 @@ func (gw *groupWriter) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (gw *groupWriter) close() error {
|
||||
if gw.buff.Len() == 0 {
|
||||
// don't print begin/end messages if there's no buffered entries
|
||||
switch {
|
||||
case gw.buff.Len() == 0:
|
||||
return nil
|
||||
}
|
||||
if _, err := io.WriteString(gw.writer, gw.begin); err != nil {
|
||||
case gw.begin == "" && gw.end == "":
|
||||
_, err := io.Copy(gw.writer, &gw.buff)
|
||||
return err
|
||||
default:
|
||||
_, err := io.Copy(gw.writer, gw.combinedBuff())
|
||||
return err
|
||||
}
|
||||
gw.buff.WriteString(gw.end)
|
||||
_, err := io.Copy(gw.writer, &gw.buff)
|
||||
return err
|
||||
}
|
||||
|
||||
func (gw *groupWriter) combinedBuff() io.Reader {
|
||||
var b bytes.Buffer
|
||||
_, _ = b.WriteString(gw.begin)
|
||||
_, _ = io.Copy(&b, &gw.buff)
|
||||
_, _ = b.WriteString(gw.end)
|
||||
return &b
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package summary
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
@@ -29,6 +31,9 @@ func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
|
||||
func PrintTask(l *logger.Logger, t *ast.Task) {
|
||||
printTaskName(l, t)
|
||||
printTaskDescribingText(t, l)
|
||||
printTaskVars(l, t)
|
||||
printTaskEnv(l, t)
|
||||
printTaskRequires(l, t)
|
||||
printTaskDependencies(l, t)
|
||||
printTaskAliases(l, t)
|
||||
printTaskCommands(l, t)
|
||||
@@ -118,3 +123,168 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printTaskVars(l *logger.Logger, t *ast.Task) {
|
||||
if t.Vars == nil || t.Vars.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
osEnvVars := getEnvVarNames()
|
||||
|
||||
taskfileEnvVars := make(map[string]bool)
|
||||
if t.Env != nil {
|
||||
for key := range t.Env.All() {
|
||||
taskfileEnvVars[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
hasNonEnvVars := false
|
||||
for key := range t.Vars.All() {
|
||||
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
|
||||
hasNonEnvVars = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasNonEnvVars {
|
||||
return
|
||||
}
|
||||
|
||||
l.Outf(logger.Default, "\n")
|
||||
l.Outf(logger.Default, "vars:\n")
|
||||
|
||||
for key, value := range t.Vars.All() {
|
||||
// Only display variables that are not from OS environment or Taskfile env
|
||||
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
|
||||
formattedValue := formatVarValue(value)
|
||||
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printTaskEnv(l *logger.Logger, t *ast.Task) {
|
||||
if t.Env == nil || t.Env.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
envVars := getEnvVarNames()
|
||||
|
||||
hasNonEnvVars := false
|
||||
for key := range t.Env.All() {
|
||||
if !isEnvVar(key, envVars) {
|
||||
hasNonEnvVars = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasNonEnvVars {
|
||||
return
|
||||
}
|
||||
|
||||
l.Outf(logger.Default, "\n")
|
||||
l.Outf(logger.Default, "env:\n")
|
||||
|
||||
for key, value := range t.Env.All() {
|
||||
// Only display variables that are not from OS environment
|
||||
if !isEnvVar(key, envVars) {
|
||||
formattedValue := formatVarValue(value)
|
||||
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Shell command - check this first before Value
|
||||
// because dynamic vars may have both Sh and an empty Value
|
||||
if v.Sh != nil {
|
||||
return fmt.Sprintf("sh: %s", *v.Sh)
|
||||
}
|
||||
|
||||
// Reference
|
||||
if v.Ref != "" {
|
||||
return fmt.Sprintf("ref: %s", v.Ref)
|
||||
}
|
||||
|
||||
// Static value
|
||||
if v.Value != nil {
|
||||
// Check if it's a map or complex type
|
||||
if m, ok := v.Value.(map[string]any); ok {
|
||||
return formatMap(m, 4)
|
||||
}
|
||||
// Simple string value
|
||||
return fmt.Sprintf(`"%v"`, v.Value)
|
||||
}
|
||||
|
||||
return `""`
|
||||
}
|
||||
|
||||
// formatMap formats a map value with proper indentation for YAML.
|
||||
func formatMap(m map[string]any, indent int) string {
|
||||
if len(m) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString("\n")
|
||||
spaces := strings.Repeat(" ", indent)
|
||||
|
||||
for k, v := range m {
|
||||
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v)) //nolint:staticcheck
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func printTaskRequires(l *logger.Logger, t *ast.Task) {
|
||||
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l.Outf(logger.Default, "\n")
|
||||
l.Outf(logger.Default, "requires:\n")
|
||||
l.Outf(logger.Default, " vars:\n")
|
||||
|
||||
for _, v := range t.Requires.Vars {
|
||||
// If the variable has enum constraints, format accordingly
|
||||
if len(v.Enum) > 0 {
|
||||
l.Outf(logger.Yellow, " - %s:\n", v.Name)
|
||||
l.Outf(logger.Yellow, " enum:\n")
|
||||
for _, enumValue := range v.Enum {
|
||||
l.Outf(logger.Yellow, " - %s\n", enumValue)
|
||||
}
|
||||
} else {
|
||||
// Simple required variable
|
||||
l.Outf(logger.Yellow, " - %s\n", v.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getEnvVarNames() map[string]bool {
|
||||
envMap := make(map[string]bool)
|
||||
for _, e := range os.Environ() {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) > 0 {
|
||||
envMap[parts[0]] = true
|
||||
}
|
||||
}
|
||||
return envMap
|
||||
}
|
||||
|
||||
// isEnvVar checks if a variable is from OS environment or auto-generated by Task.
|
||||
func isEnvVar(key string, envVars map[string]bool) bool {
|
||||
// Filter out auto-generated Task variables
|
||||
if strings.HasPrefix(key, "TASK_") ||
|
||||
strings.HasPrefix(key, "CLI_") ||
|
||||
strings.HasPrefix(key, "ROOT_") ||
|
||||
key == "TASK" ||
|
||||
key == "TASKFILE" ||
|
||||
key == "TASKFILE_DIR" ||
|
||||
key == "USER_WORKING_DIR" ||
|
||||
key == "ALIAS" ||
|
||||
key == "MATCH" {
|
||||
return true
|
||||
}
|
||||
return envVars[key]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.45.4
|
||||
3.48.0
|
||||
|
||||
174
requires.go
174
requires.go
@@ -4,35 +4,180 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/input"
|
||||
"github.com/go-task/task/v3/internal/term"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
|
||||
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
func (e *Executor) canPrompt() bool {
|
||||
return e.Interactive && (e.AssumeTerm || term.IsTerminal())
|
||||
}
|
||||
|
||||
func (e *Executor) newPrompter() *input.Prompter {
|
||||
return &input.Prompter{
|
||||
Stdin: e.Stdin,
|
||||
Stdout: e.Stdout,
|
||||
Stderr: e.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// promptDepsVars traverses the dependency tree, collects all missing required
|
||||
// variables, and prompts for them upfront. This is used for deps which execute
|
||||
// in parallel, so all prompts must happen before execution to avoid interleaving.
|
||||
// Prompted values are stored in e.promptedVars for injection into task calls.
|
||||
func (e *Executor) promptDepsVars(calls []*Call) error {
|
||||
if !e.canPrompt() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var missingVars []errors.MissingVar
|
||||
for _, requiredVar := range t.Requires.Vars {
|
||||
_, ok := t.Vars.Get(requiredVar.Name)
|
||||
if !ok {
|
||||
missingVars = append(missingVars, errors.MissingVar{
|
||||
Name: requiredVar.Name,
|
||||
AllowedValues: requiredVar.Enum,
|
||||
})
|
||||
// Collect all missing vars from the dependency tree
|
||||
visited := make(map[string]bool)
|
||||
varsMap := make(map[string]*ast.VarsWithValidation)
|
||||
|
||||
var collect func(call *Call) error
|
||||
collect = func(call *Call) error {
|
||||
compiledTask, err := e.FastCompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range getMissingRequiredVars(compiledTask) {
|
||||
if _, exists := varsMap[v.Name]; !exists {
|
||||
varsMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Check visited AFTER collecting vars to handle duplicate task calls with different vars
|
||||
if visited[call.Task] {
|
||||
return nil
|
||||
}
|
||||
visited[call.Task] = true
|
||||
|
||||
for _, dep := range compiledTask.Deps {
|
||||
depCall := &Call{
|
||||
Task: dep.Task,
|
||||
Vars: dep.Vars,
|
||||
Silent: dep.Silent,
|
||||
}
|
||||
if err := collect(depCall); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, call := range calls {
|
||||
if err := collect(call); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingVars) > 0 {
|
||||
return &errors.TaskMissingRequiredVarsError{
|
||||
TaskName: t.Name(),
|
||||
MissingVars: missingVars,
|
||||
if len(varsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
prompter := e.newPrompter()
|
||||
e.promptedVars = ast.NewVars()
|
||||
|
||||
for _, v := range varsMap {
|
||||
value, err := prompter.Prompt(v.Name, v.Enum)
|
||||
if err != nil {
|
||||
if errors.Is(err, input.ErrCancelled) {
|
||||
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.promptedVars.Set(v.Name, ast.Var{Value: value})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// promptTaskVars prompts for any missing required vars from a single task.
|
||||
// Used for sequential task calls (cmds) where we can prompt just-in-time.
|
||||
// Returns true if any vars were prompted (caller should recompile the task).
|
||||
func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
|
||||
if !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Find missing vars, excluding already prompted ones
|
||||
var missing []*ast.VarsWithValidation
|
||||
for _, v := range getMissingRequiredVars(t) {
|
||||
if e.promptedVars != nil {
|
||||
if _, ok := e.promptedVars.Get(v.Name); ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
missing = append(missing, v)
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
prompter := e.newPrompter()
|
||||
|
||||
for _, v := range missing {
|
||||
value, err := prompter.Prompt(v.Name, v.Enum)
|
||||
if err != nil {
|
||||
if errors.Is(err, input.ErrCancelled) {
|
||||
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Add to call.Vars for recompilation
|
||||
if call.Vars == nil {
|
||||
call.Vars = ast.NewVars()
|
||||
}
|
||||
call.Vars.Set(v.Name, ast.Var{Value: value})
|
||||
|
||||
// Cache for reuse by other tasks
|
||||
if e.promptedVars == nil {
|
||||
e.promptedVars = ast.NewVars()
|
||||
}
|
||||
e.promptedVars.Set(v.Name, ast.Var{Value: value})
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getMissingRequiredVars returns required vars that are not set in the task's vars.
|
||||
func getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation {
|
||||
if t.Requires == nil {
|
||||
return nil
|
||||
}
|
||||
var missing []*ast.VarsWithValidation
|
||||
for _, v := range t.Requires.Vars {
|
||||
if _, ok := t.Vars.Get(v.Name); !ok {
|
||||
missing = append(missing, v)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
|
||||
missing := getMissingRequiredVars(t)
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
missingVars := make([]errors.MissingVar, len(missing))
|
||||
for i, v := range missing {
|
||||
missingVars[i] = errors.MissingVar{
|
||||
Name: v.Name,
|
||||
AllowedValues: v.Enum,
|
||||
}
|
||||
}
|
||||
|
||||
return &errors.TaskMissingRequiredVarsError{
|
||||
TaskName: t.Name(),
|
||||
MissingVars: missingVars,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
||||
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
return nil
|
||||
@@ -50,7 +195,6 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
||||
Name: requiredVar.Name,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(notAllowedValuesVars) > 0 {
|
||||
|
||||
29
setup.go
29
setup.go
@@ -35,7 +35,6 @@ func (e *Executor) Setup() error {
|
||||
if err := e.readTaskfile(node); err != nil {
|
||||
return err
|
||||
}
|
||||
e.setupFuzzyModel()
|
||||
e.setupStdFiles()
|
||||
if err := e.setupOutput(); err != nil {
|
||||
return err
|
||||
@@ -55,11 +54,21 @@ func (e *Executor) Setup() error {
|
||||
}
|
||||
|
||||
func (e *Executor) getRootNode() (taskfile.Node, error) {
|
||||
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
|
||||
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,
|
||||
taskfile.WithCACert(e.CACert),
|
||||
taskfile.WithCert(e.Cert),
|
||||
taskfile.WithCertKey(e.CertKey),
|
||||
)
|
||||
var taskNotFoundError errors.TaskfileNotFoundError
|
||||
if errors.As(err, &taskNotFoundError) {
|
||||
taskNotFoundError.AskInit = true
|
||||
return nil, taskNotFoundError
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Dir = node.Dir()
|
||||
e.Entrypoint = node.Location()
|
||||
return node, err
|
||||
}
|
||||
|
||||
@@ -76,8 +85,12 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
taskfile.WithInsecure(e.Insecure),
|
||||
taskfile.WithDownload(e.Download),
|
||||
taskfile.WithOffline(e.Offline),
|
||||
taskfile.WithTrustedHosts(e.TrustedHosts),
|
||||
taskfile.WithTempDir(e.TempDir.Remote),
|
||||
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
|
||||
taskfile.WithReaderCACert(e.CACert),
|
||||
taskfile.WithReaderCert(e.Cert),
|
||||
taskfile.WithReaderCertKey(e.CertKey),
|
||||
taskfile.WithDebugFunc(debugFunc),
|
||||
taskfile.WithPromptFunc(promptFunc),
|
||||
)
|
||||
@@ -145,16 +158,16 @@ func (e *Executor) setupTempDir() error {
|
||||
}
|
||||
}
|
||||
|
||||
remoteDir := env.GetTaskEnv("REMOTE_DIR")
|
||||
if remoteDir != "" {
|
||||
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
|
||||
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
|
||||
// RemoteCacheDir from taskrc/env can override the remote cache directory
|
||||
if e.RemoteCacheDir != "" {
|
||||
if filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, "~") {
|
||||
remoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.TempDir.Remote = remoteTempDir
|
||||
e.TempDir.Remote = remoteCacheDir
|
||||
} else {
|
||||
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
|
||||
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
196
task.go
196
task.go
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -73,14 +74,21 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prompt for all required vars from deps upfront (parallel execution)
|
||||
if err := e.promptDepsVars(calls); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g := &errgroup.Group{}
|
||||
if e.Failfast {
|
||||
g, ctx = errgroup.WithContext(ctx)
|
||||
}
|
||||
for _, c := range regularCalls {
|
||||
c := c
|
||||
if e.Parallel {
|
||||
g.Go(func() error { return e.RunTask(ctx, c) })
|
||||
} else {
|
||||
@@ -113,11 +121,24 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
|
||||
regularCalls = append(regularCalls, c)
|
||||
}
|
||||
}
|
||||
return
|
||||
return regularCalls, watchCalls, err
|
||||
}
|
||||
|
||||
// RunTask runs a task by its name
|
||||
func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
// Inject prompted vars into call if available
|
||||
if e.promptedVars != nil {
|
||||
if call.Vars == nil {
|
||||
call.Vars = ast.NewVars()
|
||||
}
|
||||
for name, v := range e.promptedVars.All() {
|
||||
// Only inject if not already set in call
|
||||
if _, ok := call.Vars.Get(name); !ok {
|
||||
call.Vars.Set(name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t, err := e.FastCompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -127,8 +148,12 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsSet(t); err != nil {
|
||||
return err
|
||||
// Check required vars early (before template compilation) if we can't prompt.
|
||||
// This gives a clear "missing required variables" error instead of a template error.
|
||||
if !e.canPrompt() {
|
||||
if err := e.areTaskRequiredVarsSet(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t, err = e.CompiledTask(call)
|
||||
@@ -136,6 +161,35 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if condition after CompiledTask so dynamic variables are resolved
|
||||
if strings.TrimSpace(t.If) != "" {
|
||||
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: t.If,
|
||||
Dir: t.Dir,
|
||||
Env: env.Get(t),
|
||||
}); err != nil {
|
||||
e.Logger.VerboseOutf(logger.Yellow, "task: if condition not met - skipped: %q\n", call.Task)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for missing required vars after if check (avoid prompting if task won't run)
|
||||
prompted, err := e.promptTaskVars(t, call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prompted {
|
||||
// Recompile with the new vars
|
||||
t, err = e.FastCompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsSet(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -150,7 +204,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
release := e.acquireConcurrencyLimit()
|
||||
defer release()
|
||||
|
||||
return e.startExecution(ctx, t, func(ctx context.Context) error {
|
||||
if err = e.startExecution(ctx, t, func(ctx context.Context) error {
|
||||
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
|
||||
if err := e.runDeps(ctx, t); err != nil {
|
||||
return err
|
||||
@@ -183,8 +237,12 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
}
|
||||
|
||||
if upToDate && preCondMet {
|
||||
if e.Verbose || (!call.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
|
||||
e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", t.Name())
|
||||
if e.Verbose || (!call.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
|
||||
name := t.Name()
|
||||
if e.OutputStyle.Name == "prefixed" {
|
||||
name = t.Prefix
|
||||
}
|
||||
e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -210,7 +268,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
|
||||
for i := range t.Cmds {
|
||||
if t.Cmds[i].Defer {
|
||||
defer e.runDeferred(t, call, i, &deferredExitCode)
|
||||
defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -228,16 +286,16 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
deferredExitCode = uint8(exitCode)
|
||||
}
|
||||
|
||||
if call.Indirect {
|
||||
return err
|
||||
}
|
||||
|
||||
return &errors.TaskRunError{TaskName: t.Task, Err: err}
|
||||
return err
|
||||
}
|
||||
}
|
||||
e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return &errors.TaskRunError{TaskName: t.Name(), Err: err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) mkdir(t *ast.Task) error {
|
||||
@@ -258,13 +316,15 @@ func (e *Executor) mkdir(t *ast.Task) error {
|
||||
}
|
||||
|
||||
func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g := &errgroup.Group{}
|
||||
if e.Failfast || t.Failfast {
|
||||
g, ctx = errgroup.WithContext(ctx)
|
||||
}
|
||||
|
||||
reacquire := e.releaseConcurrencyLimit()
|
||||
defer reacquire()
|
||||
|
||||
for _, d := range t.Deps {
|
||||
d := d
|
||||
g.Go(func() error {
|
||||
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
|
||||
if err != nil {
|
||||
@@ -277,17 +337,11 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
|
||||
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, deferredExitCode *uint8) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
origTask, err := e.GetTask(call)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := t.Cmds[i]
|
||||
vars, _ := e.Compiler.GetVariables(origTask, call)
|
||||
cache := &templater.Cache{Vars: vars}
|
||||
extra := map[string]any{}
|
||||
|
||||
@@ -297,6 +351,7 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode
|
||||
|
||||
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
|
||||
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
|
||||
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
|
||||
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
|
||||
|
||||
if err := e.runCommand(ctx, t, call, i); err != nil {
|
||||
@@ -307,23 +362,37 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode
|
||||
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {
|
||||
cmd := t.Cmds[i]
|
||||
|
||||
// Check if condition for any command type
|
||||
if strings.TrimSpace(cmd.If) != "" {
|
||||
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: cmd.If,
|
||||
Dir: t.Dir,
|
||||
Env: env.Get(t),
|
||||
}); err != nil {
|
||||
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] if condition not met - skipped\n", t.Name())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case cmd.Task != "":
|
||||
reacquire := e.releaseConcurrencyLimit()
|
||||
defer reacquire()
|
||||
|
||||
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
|
||||
if err != nil {
|
||||
return err
|
||||
var exitCode interp.ExitStatus
|
||||
if errors.As(err, &exitCode) && cmd.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
|
||||
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
|
||||
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
|
||||
}
|
||||
|
||||
@@ -372,7 +441,7 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
|
||||
return err
|
||||
}
|
||||
|
||||
if h == "" {
|
||||
if h == "" || t.Watch {
|
||||
return execute(ctx)
|
||||
}
|
||||
|
||||
@@ -400,19 +469,40 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
|
||||
}
|
||||
|
||||
// FindMatchingTasks returns a list of tasks that match the given call. A task
|
||||
// matches a call if its name is equal to the call's task name or if it matches
|
||||
// matches a call if its name is equal to the call's task name, or one of aliases, or if it matches
|
||||
// a wildcard pattern. The function returns a list of MatchingTask structs, each
|
||||
// containing a task and a list of wildcards that were matched.
|
||||
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
|
||||
// If multiple tasks match due to aliases, a TaskNameConflictError is returned.
|
||||
func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
|
||||
if call == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
var matchingTasks []*MatchingTask
|
||||
// If there is a direct match, return it
|
||||
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
|
||||
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
|
||||
return matchingTasks
|
||||
return matchingTasks, nil
|
||||
}
|
||||
var aliasedTasks []string
|
||||
for task := range e.Taskfile.Tasks.Values(nil) {
|
||||
if slices.Contains(task.Aliases, call.Task) {
|
||||
aliasedTasks = append(aliasedTasks, task.Task)
|
||||
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
|
||||
}
|
||||
}
|
||||
|
||||
if len(aliasedTasks) == 1 {
|
||||
return matchingTasks, nil
|
||||
}
|
||||
|
||||
// If we found multiple tasks
|
||||
if len(aliasedTasks) > 1 {
|
||||
return nil, &errors.TaskNameConflictError{
|
||||
Call: call.Task,
|
||||
TaskNames: aliasedTasks,
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt a wildcard match
|
||||
for _, value := range e.Taskfile.Tasks.All(nil) {
|
||||
if match, wildcards := value.WildcardMatch(call.Task); match {
|
||||
@@ -422,7 +512,7 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
|
||||
})
|
||||
}
|
||||
}
|
||||
return matchingTasks
|
||||
return matchingTasks, nil
|
||||
}
|
||||
|
||||
// GetTask will return the task with the name matching the given call from the taskfile.
|
||||
@@ -430,7 +520,11 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
|
||||
// If multiple tasks contain the same alias or no matches are found an error is returned.
|
||||
func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
|
||||
// Search for a matching task
|
||||
matchingTasks := e.FindMatchingTasks(call)
|
||||
matchingTasks, err := e.FindMatchingTasks(call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(matchingTasks) > 0 {
|
||||
if call.Vars == nil {
|
||||
call.Vars = ast.NewVars()
|
||||
@@ -439,34 +533,18 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
|
||||
return matchingTasks[0].Task, nil
|
||||
}
|
||||
|
||||
// If didn't find one, search for a task with a matching alias
|
||||
var matchingTask *ast.Task
|
||||
var aliasedTasks []string
|
||||
for task := range e.Taskfile.Tasks.Values(nil) {
|
||||
if slices.Contains(task.Aliases, call.Task) {
|
||||
aliasedTasks = append(aliasedTasks, task.Task)
|
||||
matchingTask = task
|
||||
}
|
||||
}
|
||||
// If we found multiple tasks
|
||||
if len(aliasedTasks) > 1 {
|
||||
return nil, &errors.TaskNameConflictError{
|
||||
Call: call.Task,
|
||||
TaskNames: aliasedTasks,
|
||||
}
|
||||
}
|
||||
// If we found no tasks
|
||||
if len(aliasedTasks) == 0 {
|
||||
didYouMean := ""
|
||||
didYouMean := ""
|
||||
if !e.DisableFuzzy {
|
||||
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
|
||||
if e.fuzzyModel != nil {
|
||||
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
|
||||
}
|
||||
return nil, &errors.TaskNotFoundError{
|
||||
TaskName: call.Task,
|
||||
DidYouMean: didYouMean,
|
||||
}
|
||||
}
|
||||
return matchingTask, nil
|
||||
return nil, &errors.TaskNotFoundError{
|
||||
TaskName: call.Task,
|
||||
DidYouMean: didYouMean,
|
||||
}
|
||||
}
|
||||
|
||||
type FilterFunc func(task *ast.Task) bool
|
||||
@@ -498,7 +576,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
|
||||
// Compile the list of tasks
|
||||
for i := range tasks {
|
||||
g.Go(func() error {
|
||||
compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task})
|
||||
compiledTask, err := e.CompiledTaskForTaskList(&Call{Task: tasks[i].Task})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
59
task_test.go
59
task_test.go
@@ -9,6 +9,7 @@ import (
|
||||
rand "math/rand/v2"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -569,7 +570,9 @@ func TestCyclicDep(t *testing.T) {
|
||||
task.WithStderr(io.Discard),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(t.Context(), &task.Call{Task: "task-1"}))
|
||||
err := e.Run(t.Context(), &task.Call{Task: "task-1"})
|
||||
var taskCalledTooManyTimesError *errors.TaskCalledTooManyTimesError
|
||||
assert.ErrorAs(t, err, &taskCalledTooManyTimesError)
|
||||
}
|
||||
|
||||
func TestTaskVersion(t *testing.T) {
|
||||
@@ -784,6 +787,11 @@ func TestIncludesRemote(t *testing.T) {
|
||||
|
||||
var buff SyncBuffer
|
||||
|
||||
// Extract host from server URL for trust testing
|
||||
parsedURL, err := url.Parse(srv.URL)
|
||||
require.NoError(t, err)
|
||||
trustedHost := parsedURL.Host
|
||||
|
||||
executors := []struct {
|
||||
name string
|
||||
executor *task.Executor
|
||||
@@ -823,6 +831,23 @@ func TestIncludesRemote(t *testing.T) {
|
||||
task.WithOffline(true),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "with trusted hosts, no prompts",
|
||||
executor: task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithTimeout(time.Minute),
|
||||
task.WithInsecure(true),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithVerbose(true),
|
||||
|
||||
// With trusted hosts
|
||||
task.WithTrustedHosts([]string{trustedHost}),
|
||||
task.WithDownload(true),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range executors {
|
||||
@@ -1052,7 +1077,7 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
|
||||
const dir = "testdata/includes_optional_implicit_false"
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
|
||||
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
|
||||
expected := fmt.Sprintf(message, wd, dir)
|
||||
|
||||
e := task.NewExecutor(
|
||||
@@ -1072,7 +1097,7 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
|
||||
const dir = "testdata/includes_optional_explicit_false"
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
|
||||
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
|
||||
expected := fmt.Sprintf(message, wd, dir)
|
||||
|
||||
e := task.NewExecutor(
|
||||
@@ -1849,6 +1874,29 @@ func TestRunOnceSharedDeps(t *testing.T) {
|
||||
assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`)
|
||||
}
|
||||
|
||||
func TestRunWhenChanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const dir = "testdata/run_when_changed"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithForceAll(true),
|
||||
task.WithSilent(true),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "start"}))
|
||||
expectedOutputOrder := strings.TrimSpace(`
|
||||
login server=fubar user=fubar
|
||||
login server=foo user=foo
|
||||
login server=bar user=bar
|
||||
`)
|
||||
assert.Contains(t, buff.String(), expectedOutputOrder)
|
||||
}
|
||||
|
||||
func TestDeferredCmds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2564,6 +2612,11 @@ func TestWildcard(t *testing.T) {
|
||||
call: "start-foo",
|
||||
expectedOutput: "Starting foo\n",
|
||||
},
|
||||
{
|
||||
name: "alias",
|
||||
call: "s-foo",
|
||||
expectedOutput: "Starting foo\n",
|
||||
},
|
||||
{
|
||||
name: "matches exactly",
|
||||
call: "matches-exactly-*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
@@ -12,6 +12,7 @@ type Cmd struct {
|
||||
Cmd string
|
||||
Task string
|
||||
For *For
|
||||
If string
|
||||
Silent bool
|
||||
Set []string
|
||||
Shopt []string
|
||||
@@ -29,6 +30,7 @@ func (c *Cmd) DeepCopy() *Cmd {
|
||||
Cmd: c.Cmd,
|
||||
Task: c.Task,
|
||||
For: c.For.DeepCopy(),
|
||||
If: c.If,
|
||||
Silent: c.Silent,
|
||||
Set: deepcopy.Slice(c.Set),
|
||||
Shopt: deepcopy.Slice(c.Shopt),
|
||||
@@ -55,6 +57,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
Cmd string
|
||||
Task string
|
||||
For *For
|
||||
If string
|
||||
Silent bool
|
||||
Set []string
|
||||
Shopt []string
|
||||
@@ -92,7 +95,9 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
c.Task = cmdStruct.Task
|
||||
c.Vars = cmdStruct.Vars
|
||||
c.For = cmdStruct.For
|
||||
c.If = cmdStruct.If
|
||||
c.Silent = cmdStruct.Silent
|
||||
c.IgnoreError = cmdStruct.IgnoreError
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -100,6 +105,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
if cmdStruct.Cmd != "" {
|
||||
c.Cmd = cmdStruct.Cmd
|
||||
c.For = cmdStruct.For
|
||||
c.If = cmdStruct.If
|
||||
c.Silent = cmdStruct.Silent
|
||||
c.Set = cmdStruct.Set
|
||||
c.Shopt = cmdStruct.Shopt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"iter"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/goext"
|
||||
|
||||
@@ -3,7 +3,7 @@ package ast
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// Task represents a task
|
||||
type Task struct {
|
||||
Task string
|
||||
Task string `hash:"ignore"`
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
@@ -32,22 +32,24 @@ type Task struct {
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
Silent bool
|
||||
Silent *bool
|
||||
Interactive bool
|
||||
Internal bool
|
||||
Method string
|
||||
Prefix string
|
||||
Prefix string `hash:"ignore"`
|
||||
IgnoreError bool
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
Watch bool
|
||||
Location *Location
|
||||
Failfast bool
|
||||
// Populated during merging
|
||||
Namespace string
|
||||
Namespace string `hash:"ignore"`
|
||||
IncludeVars *Vars
|
||||
IncludedTaskfileVars *Vars
|
||||
|
||||
FullName string
|
||||
FullName string `hash:"ignore"`
|
||||
}
|
||||
|
||||
func (t *Task) Name() string {
|
||||
@@ -67,28 +69,37 @@ func (t *Task) LocalName() string {
|
||||
return name
|
||||
}
|
||||
|
||||
// IsSilent returns true if the task has silent mode explicitly enabled.
|
||||
// Returns false if Silent is nil (not set) or explicitly set to false.
|
||||
func (t *Task) IsSilent() bool {
|
||||
return t.Silent != nil && *t.Silent
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Convert the name into a regex string
|
||||
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(t.Task, "*", "(.*)"))
|
||||
regex := regexp.MustCompile(regexStr)
|
||||
wildcards := regex.FindStringSubmatch(name)
|
||||
wildcardCount := strings.Count(t.Task, "*")
|
||||
names := append([]string{t.Task}, t.Aliases...)
|
||||
|
||||
// If there are no wildcards, return false
|
||||
if len(wildcards) == 0 {
|
||||
return false, nil
|
||||
for _, taskName := range names {
|
||||
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)"))
|
||||
regex := regexp.MustCompile(regexStr)
|
||||
wildcards := regex.FindStringSubmatch(name)
|
||||
|
||||
if len(wildcards) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove the first match, which is the full string
|
||||
wildcards = wildcards[1:]
|
||||
wildcardCount := strings.Count(taskName, "*")
|
||||
|
||||
if len(wildcards) != wildcardCount {
|
||||
continue
|
||||
}
|
||||
|
||||
return true, wildcards
|
||||
}
|
||||
|
||||
// Remove the first match, which is the full string
|
||||
wildcards = wildcards[1:]
|
||||
|
||||
// If there are more/less wildcards than matches, return false
|
||||
if len(wildcards) != wildcardCount {
|
||||
return false, wildcards
|
||||
}
|
||||
|
||||
return true, wildcards
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
@@ -133,7 +144,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
Silent bool
|
||||
Silent *bool `yaml:"silent,omitempty"`
|
||||
Interactive bool
|
||||
Internal bool
|
||||
Method string
|
||||
@@ -141,8 +152,10 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
Requires *Requires
|
||||
Watch bool
|
||||
Failfast bool
|
||||
}
|
||||
if err := node.Decode(&task); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@@ -171,7 +184,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
t.Vars = task.Vars
|
||||
t.Env = task.Env
|
||||
t.Dotenv = task.Dotenv
|
||||
t.Silent = task.Silent
|
||||
t.Silent = deepcopy.Scalar(task.Silent)
|
||||
t.Interactive = task.Interactive
|
||||
t.Internal = task.Internal
|
||||
t.Method = task.Method
|
||||
@@ -179,8 +192,10 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
t.IgnoreError = task.IgnoreError
|
||||
t.Run = task.Run
|
||||
t.Platforms = task.Platforms
|
||||
t.If = task.If
|
||||
t.Requires = task.Requires
|
||||
t.Watch = task.Watch
|
||||
t.Failfast = task.Failfast
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -212,7 +227,7 @@ func (t *Task) DeepCopy() *Task {
|
||||
Vars: t.Vars.DeepCopy(),
|
||||
Env: t.Env.DeepCopy(),
|
||||
Dotenv: deepcopy.Slice(t.Dotenv),
|
||||
Silent: t.Silent,
|
||||
Silent: deepcopy.Scalar(t.Silent),
|
||||
Interactive: t.Interactive,
|
||||
Internal: t.Internal,
|
||||
Method: t.Method,
|
||||
@@ -222,10 +237,13 @@ func (t *Task) DeepCopy() *Task {
|
||||
IncludeVars: t.IncludeVars.DeepCopy(),
|
||||
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
|
||||
Platforms: deepcopy.Slice(t.Platforms),
|
||||
If: t.If,
|
||||
Location: t.Location.DeepCopy(),
|
||||
Requires: t.Requires.DeepCopy(),
|
||||
Namespace: t.Namespace,
|
||||
FullName: t.FullName,
|
||||
Watch: t.Watch,
|
||||
Failfast: t.Failfast,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
@@ -59,6 +59,14 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
if t1.Tasks == nil {
|
||||
t1.Tasks = NewTasks()
|
||||
}
|
||||
if t2.Silent {
|
||||
for _, t := range t2.Tasks.All(nil) {
|
||||
if t.Silent == nil {
|
||||
v := true
|
||||
t.Silent = &v
|
||||
}
|
||||
}
|
||||
}
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
@@ -244,8 +244,8 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
|
||||
}
|
||||
|
||||
func taskNameWithNamespace(taskName string, namespace string) string {
|
||||
if strings.HasPrefix(taskName, NamespaceSeparator) {
|
||||
return strings.TrimPrefix(taskName, NamespaceSeparator)
|
||||
if after, ok := strings.CutPrefix(taskName, NamespaceSeparator); ok {
|
||||
return after
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
@@ -113,12 +113,12 @@ func (vars *Vars) ToCacheMap() (m map[string]any) {
|
||||
m[k] = v.Value
|
||||
}
|
||||
}
|
||||
return
|
||||
return m
|
||||
}
|
||||
|
||||
// Merge loops over other and merges it values with the variables in vars. If
|
||||
// the include parameter is not nil and its it is an advanced import, the
|
||||
// directory is set set to the value of the include parameter.
|
||||
// directory is set to the value of the include parameter.
|
||||
func (vars *Vars) Merge(other *Vars, include *Include) {
|
||||
if vars == nil || vars.om == nil || other == nil {
|
||||
return
|
||||
@@ -133,6 +133,35 @@ func (vars *Vars) Merge(other *Vars, include *Include) {
|
||||
}
|
||||
}
|
||||
|
||||
// ReverseMerge merges other variables with the existing variables in vars, but
|
||||
// keeps the other variables first in order. If the include parameter is not
|
||||
// nil and it is an advanced import, the directory is set to the value of the
|
||||
// include parameter.
|
||||
func (vars *Vars) ReverseMerge(other *Vars, include *Include) {
|
||||
if vars == nil || vars.om == nil || other == nil || other.om == nil {
|
||||
return
|
||||
}
|
||||
|
||||
newOM := orderedmap.NewOrderedMap[string, Var]()
|
||||
|
||||
other.mutex.RLock()
|
||||
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
|
||||
val := pair.Value
|
||||
if include != nil && include.AdvancedImport {
|
||||
val.Dir = include.Dir
|
||||
}
|
||||
newOM.Set(pair.Key, val)
|
||||
}
|
||||
other.mutex.RUnlock()
|
||||
|
||||
vars.mutex.Lock()
|
||||
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
|
||||
newOM.Set(pair.Key, pair.Value)
|
||||
}
|
||||
vars.om = newOM
|
||||
vars.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (vs *Vars) DeepCopy() *Vars {
|
||||
if vs == nil {
|
||||
return nil
|
||||
|
||||
@@ -34,13 +34,14 @@ func NewRootNode(
|
||||
dir string,
|
||||
insecure bool,
|
||||
timeout time.Duration,
|
||||
opts ...NodeOption,
|
||||
) (Node, error) {
|
||||
dir = fsext.DefaultDir(entrypoint, dir)
|
||||
// If the entrypoint is "-", we read from stdin
|
||||
if entrypoint == "-" {
|
||||
return NewStdinNode(dir)
|
||||
}
|
||||
return NewNode(entrypoint, dir, insecure)
|
||||
return NewNode(entrypoint, dir, insecure, opts...)
|
||||
}
|
||||
|
||||
func NewNode(
|
||||
@@ -72,6 +73,16 @@ func NewNode(
|
||||
return node, err
|
||||
}
|
||||
|
||||
func isRemoteEntrypoint(entrypoint string) bool {
|
||||
scheme, _ := getScheme(entrypoint)
|
||||
switch scheme {
|
||||
case "git", "http", "https":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getScheme(uri string) (string, error) {
|
||||
u, err := giturls.Parse(uri)
|
||||
if u == nil {
|
||||
|
||||
@@ -10,6 +10,9 @@ type (
|
||||
parent Node
|
||||
dir string
|
||||
checksum string
|
||||
caCert string
|
||||
cert string
|
||||
certKey string
|
||||
}
|
||||
)
|
||||
|
||||
@@ -54,3 +57,21 @@ func (node *baseNode) Checksum() string {
|
||||
func (node *baseNode) Verify(checksum string) bool {
|
||||
return node.checksum == "" || node.checksum == checksum
|
||||
}
|
||||
|
||||
func WithCACert(caCert string) NodeOption {
|
||||
return func(node *baseNode) {
|
||||
node.caCert = caCert
|
||||
}
|
||||
}
|
||||
|
||||
func WithCert(cert string) NodeOption {
|
||||
return func(node *baseNode) {
|
||||
node.cert = cert
|
||||
}
|
||||
}
|
||||
|
||||
func WithCertKey(certKey string) NodeOption {
|
||||
return func(node *baseNode) {
|
||||
node.certKey = certKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/fsext"
|
||||
@@ -19,8 +19,17 @@ type FileNode struct {
|
||||
|
||||
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
|
||||
// Find the entrypoint file
|
||||
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, defaultTaskfiles)
|
||||
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if entrypoint == "" {
|
||||
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true}
|
||||
} else {
|
||||
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
|
||||
}
|
||||
} else if errors.Is(err, os.ErrPermission) {
|
||||
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true, OwnerChange: true}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -51,10 +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 strings.Contains(entrypoint, "://") {
|
||||
return entrypoint, nil
|
||||
}
|
||||
if strings.HasPrefix(entrypoint, "git") {
|
||||
if isRemoteEntrypoint(entrypoint) {
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,20 @@ package taskfile
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
giturls "github.com/chainguard-dev/git-urls"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/hashicorp/go-getter"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/fsext"
|
||||
)
|
||||
|
||||
// An GitNode is a node that reads a Taskfile from a remote location via Git.
|
||||
@@ -28,6 +28,36 @@ type GitNode struct {
|
||||
path string
|
||||
}
|
||||
|
||||
type gitRepoCache struct {
|
||||
mu sync.Mutex // Protects the locks map
|
||||
locks map[string]*sync.Mutex // One mutex per repo cache key
|
||||
}
|
||||
|
||||
func (c *gitRepoCache) getLockForRepo(cacheKey string) *sync.Mutex {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if _, exists := c.locks[cacheKey]; !exists {
|
||||
c.locks[cacheKey] = &sync.Mutex{}
|
||||
}
|
||||
|
||||
return c.locks[cacheKey]
|
||||
}
|
||||
|
||||
var globalGitRepoCache = &gitRepoCache{
|
||||
locks: make(map[string]*sync.Mutex),
|
||||
}
|
||||
|
||||
func CleanGitCache() error {
|
||||
// Clear the in-memory locks map to prevent memory leak
|
||||
globalGitRepoCache.mu.Lock()
|
||||
globalGitRepoCache.locks = make(map[string]*sync.Mutex)
|
||||
globalGitRepoCache.mu.Unlock()
|
||||
|
||||
cacheDir := filepath.Join(os.TempDir(), "task-git-repos")
|
||||
return os.RemoveAll(cacheDir)
|
||||
}
|
||||
|
||||
func NewGitNode(
|
||||
entrypoint string,
|
||||
dir string,
|
||||
@@ -72,24 +102,83 @@ func (node *GitNode) Read() ([]byte, error) {
|
||||
return node.ReadContext(context.Background())
|
||||
}
|
||||
|
||||
func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
|
||||
fs := memfs.New()
|
||||
storer := memory.NewStorage()
|
||||
_, err := git.Clone(storer, fs, &git.CloneOptions{
|
||||
URL: node.url.String(),
|
||||
ReferenceName: plumbing.ReferenceName(node.ref),
|
||||
SingleBranch: true,
|
||||
Depth: 1,
|
||||
})
|
||||
func (node *GitNode) buildURL() string {
|
||||
// Get the base URL
|
||||
baseURL := node.url.String()
|
||||
|
||||
// Always use git:: prefix for git URLs (following Terraform's pattern)
|
||||
// This forces go-getter to use git protocol
|
||||
if node.ref != "" {
|
||||
return fmt.Sprintf("git::%s?ref=%s&depth=1", baseURL, node.ref)
|
||||
}
|
||||
// When no ref is specified, omit it entirely to let git clone the default branch
|
||||
return fmt.Sprintf("git::%s?depth=1", baseURL)
|
||||
}
|
||||
|
||||
// getOrCloneRepo returns the path to a cached git repository.
|
||||
// If the repository is not cached, it clones it first.
|
||||
// This function is thread-safe: multiple goroutines cloning the same repo+ref
|
||||
// will synchronize, and only one clone operation will occur.
|
||||
//
|
||||
// The cache directory is /tmp/task-git-repos/{cache_key}/
|
||||
func (node *GitNode) getOrCloneRepo(ctx context.Context) (string, error) {
|
||||
cacheKey := node.repoCacheKey()
|
||||
|
||||
repoMutex := globalGitRepoCache.getLockForRepo(cacheKey)
|
||||
repoMutex.Lock()
|
||||
defer repoMutex.Unlock()
|
||||
|
||||
cacheDir := filepath.Join(os.TempDir(), "task-git-repos", cacheKey)
|
||||
|
||||
// Check cache FIRST - if already cloned, no network needed, timeout irrelevant
|
||||
gitDir := filepath.Join(cacheDir, ".git")
|
||||
if _, err := os.Stat(gitDir); err == nil {
|
||||
return cacheDir, nil
|
||||
}
|
||||
|
||||
// Only check context if we need to clone (requires network)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", fmt.Errorf("context cancelled while waiting for repository lock: %w", err)
|
||||
}
|
||||
|
||||
getterURL := node.buildURL()
|
||||
|
||||
client := &getter.Client{
|
||||
Ctx: ctx,
|
||||
Src: getterURL,
|
||||
Dst: cacheDir,
|
||||
Mode: getter.ClientModeDir,
|
||||
}
|
||||
|
||||
if err := client.Get(); err != nil {
|
||||
_ = os.RemoveAll(cacheDir)
|
||||
return "", fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
|
||||
return cacheDir, nil
|
||||
}
|
||||
|
||||
func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
|
||||
// Get or clone the repository into cache
|
||||
repoDir, err := node.getOrCloneRepo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := fs.Open(node.path)
|
||||
|
||||
// Build path to Taskfile in the cached repo
|
||||
// If node.path is empty, search in repo root; otherwise search in the specified path
|
||||
// fsext.SearchPath handles both files and directories (searching for DefaultTaskfiles)
|
||||
searchPath := repoDir
|
||||
if node.path != "" {
|
||||
searchPath = filepath.Join(repoDir, node.path)
|
||||
}
|
||||
filePath, err := fsext.SearchPath(searchPath, DefaultTaskfiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Read the entire response body
|
||||
b, err := io.ReadAll(file)
|
||||
|
||||
// Read file from cached repo
|
||||
b, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,8 +187,13 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||
dir, _ := filepath.Split(node.path)
|
||||
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
|
||||
// If the file is remote, we don't need to resolve the path
|
||||
if isRemoteEntrypoint(entrypoint) {
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
dir, _ := path.Split(node.path)
|
||||
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, path.Join(dir, entrypoint))
|
||||
if node.ref != "" {
|
||||
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
|
||||
}
|
||||
@@ -133,6 +227,22 @@ func (node *GitNode) CacheKey() string {
|
||||
return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum)
|
||||
}
|
||||
|
||||
// repoCacheKey generates a unique cache key for the repository+ref combination.
|
||||
// Unlike CacheKey() which includes the file path, this identifies the repository itself.
|
||||
// Two GitNodes with the same repo+ref but different file paths will share the same cache.
|
||||
//
|
||||
// Returns a path like: github.com/user/repo.git/main
|
||||
func (node *GitNode) repoCacheKey() string {
|
||||
repoPath := strings.Trim(node.url.Path, "/")
|
||||
|
||||
ref := node.ref
|
||||
if ref == "" {
|
||||
ref = "_default_" // Placeholder for the remote's default branch
|
||||
}
|
||||
|
||||
return filepath.Join(node.url.Host, repoPath, ref)
|
||||
}
|
||||
|
||||
func splitURLOnDoubleSlash(u *url.URL) (string, string) {
|
||||
x := strings.Split(u.Path, "//")
|
||||
switch len(x) {
|
||||
|
||||
@@ -21,6 +21,17 @@ func TestGitNode_ssh(t *testing.T) {
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
|
||||
}
|
||||
|
||||
func TestGitNode_sshWithAltRepo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
entrypoint, err := node.ResolveEntrypoint("git@github.com:foo/other.git//Taskfile.yml?ref=dev")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "git@github.com:foo/other.git//Taskfile.yml?ref=dev", entrypoint)
|
||||
}
|
||||
|
||||
func TestGitNode_sshWithDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -91,3 +102,146 @@ func TestGitNode_CacheKey(t *testing.T) {
|
||||
assert.Equal(t, tt.expectedKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitNode_buildURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
entrypoint string
|
||||
expectedURL string
|
||||
}{
|
||||
{
|
||||
name: "HTTPS with ref",
|
||||
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
||||
expectedURL: "git::https://github.com/foo/bar.git?ref=main&depth=1",
|
||||
},
|
||||
{
|
||||
name: "SSH with ref",
|
||||
entrypoint: "git@github.com:foo/bar.git//Taskfile.yml?ref=main",
|
||||
expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=main&depth=1",
|
||||
},
|
||||
{
|
||||
name: "HTTPS with tag ref",
|
||||
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=v1.0.0",
|
||||
expectedURL: "git::https://github.com/foo/bar.git?ref=v1.0.0&depth=1",
|
||||
},
|
||||
{
|
||||
name: "HTTPS without ref (uses remote default branch)",
|
||||
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml",
|
||||
expectedURL: "git::https://github.com/foo/bar.git?depth=1",
|
||||
},
|
||||
{
|
||||
name: "SSH with directory path",
|
||||
entrypoint: "git@github.com:foo/bar.git//directory/Taskfile.yml?ref=dev",
|
||||
expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=dev&depth=1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node, err := NewGitNode(tt.entrypoint, "", false)
|
||||
require.NoError(t, err)
|
||||
gotURL := node.buildURL()
|
||||
assert.Equal(t, tt.expectedURL, gotURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCacheKey_SameRepoSameRef(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Same repo, same ref, different files should have SAME cache key
|
||||
node1, err := NewGitNode("https://github.com/foo/bar.git//file1.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
node2, err := NewGitNode("https://github.com/foo/bar.git//dir/file2.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := node1.repoCacheKey()
|
||||
key2 := node2.repoCacheKey()
|
||||
|
||||
assert.Equal(t, key1, key2, "Same repo+ref should generate same cache key regardless of file path")
|
||||
}
|
||||
|
||||
func TestRepoCacheKey_SameRepoDifferentRef(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Same repo, different ref should have DIFFERENT cache keys
|
||||
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=dev", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := node1.repoCacheKey()
|
||||
key2 := node2.repoCacheKey()
|
||||
|
||||
assert.NotEqual(t, key1, key2, "Different refs should generate different cache keys")
|
||||
}
|
||||
|
||||
func TestRepoCacheKey_DifferentRepos(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Different repos should have DIFFERENT cache keys
|
||||
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
node2, err := NewGitNode("https://github.com/foo/other.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := node1.repoCacheKey()
|
||||
key2 := node2.repoCacheKey()
|
||||
|
||||
assert.NotEqual(t, key1, key2, "Different repos should generate different cache keys")
|
||||
}
|
||||
|
||||
func TestRepoCacheKey_NoRefVsExplicitRef(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// No ref (uses default branch) vs explicit ref should have DIFFERENT cache keys
|
||||
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := node1.repoCacheKey()
|
||||
key2 := node2.repoCacheKey()
|
||||
|
||||
assert.NotEqual(t, key1, key2, "No ref and explicit ref should generate different cache keys")
|
||||
}
|
||||
|
||||
func TestRepoCacheKey_SSHvsHTTPS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// SSH vs HTTPS pointing to same repo should have SAME cache key
|
||||
// They clone the same repo, so we want to share the cache
|
||||
node1, err := NewGitNode("git@github.com:foo/bar.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := node1.repoCacheKey()
|
||||
key2 := node2.repoCacheKey()
|
||||
|
||||
assert.Equal(t, key1, key2, "SSH and HTTPS for same repo should share cache")
|
||||
}
|
||||
|
||||
func TestRepoCacheKey_Consistency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Calling repoCacheKey multiple times on same node should return same key
|
||||
node, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := node.repoCacheKey()
|
||||
key2 := node.repoCacheKey()
|
||||
key3 := node.repoCacheKey()
|
||||
|
||||
assert.Equal(t, key1, key2)
|
||||
assert.Equal(t, key2, key3)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package taskfile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -17,7 +20,54 @@ import (
|
||||
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
|
||||
type HTTPNode struct {
|
||||
*baseNode
|
||||
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
|
||||
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
|
||||
client *http.Client // HTTP client with optional TLS configuration
|
||||
}
|
||||
|
||||
// buildHTTPClient creates an HTTP client with optional TLS configuration.
|
||||
// If no certificate options are provided, it returns http.DefaultClient.
|
||||
func buildHTTPClient(insecure bool, caCert, cert, certKey string) (*http.Client, error) {
|
||||
// Validate that cert and certKey are provided together
|
||||
if (cert != "" && certKey == "") || (cert == "" && certKey != "") {
|
||||
return nil, fmt.Errorf("both --cert and --cert-key must be provided together")
|
||||
}
|
||||
|
||||
// If no TLS customization is needed, return the default client
|
||||
if !insecure && caCert == "" && cert == "" {
|
||||
return http.DefaultClient, nil
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
}
|
||||
|
||||
// Load custom CA certificate if provided
|
||||
if caCert != "" {
|
||||
caCertData, err := os.ReadFile(caCert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read CA certificate: %w", err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCertData) {
|
||||
return nil, fmt.Errorf("failed to parse CA certificate")
|
||||
}
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
// Load client certificate and key if provided
|
||||
if cert != "" && certKey != "" {
|
||||
clientCert, err := tls.LoadX509KeyPair(cert, certKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load client certificate: %w", err)
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{clientCert}
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewHTTPNode(
|
||||
@@ -34,9 +84,16 @@ func NewHTTPNode(
|
||||
if url.Scheme == "http" && !insecure {
|
||||
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}
|
||||
}
|
||||
|
||||
client, err := buildHTTPClient(insecure, base.caCert, base.cert, base.certKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HTTPNode{
|
||||
baseNode: base,
|
||||
url: url,
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -49,16 +106,16 @@ func (node *HTTPNode) Read() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
|
||||
url, err := RemoteExists(ctx, *node.url)
|
||||
url, err := RemoteExists(ctx, *node.url, node.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
resp, err := node.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -47,3 +58,227 @@ func TestHTTPNode_CacheKey(t *testing.T) {
|
||||
assert.Equal(t, tt.expectedKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_Default(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// When no TLS customization is needed, should return http.DefaultClient
|
||||
client, err := buildHTTPClient(false, "", "", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.DefaultClient, client)
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_Insecure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := buildHTTPClient(true, "", "", "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
assert.NotEqual(t, http.DefaultClient, client)
|
||||
|
||||
// Check that InsecureSkipVerify is set
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.TLSClientConfig)
|
||||
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_CACert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary CA cert file
|
||||
tempDir := t.TempDir()
|
||||
caCertPath := filepath.Join(tempDir, "ca.crt")
|
||||
|
||||
// Generate a valid CA certificate
|
||||
caCertPEM := generateTestCACert(t)
|
||||
err := os.WriteFile(caCertPath, caCertPEM, 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := buildHTTPClient(false, caCertPath, "", "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
assert.NotEqual(t, http.DefaultClient, client)
|
||||
|
||||
// Check that custom RootCAs is set
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.TLSClientConfig)
|
||||
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_CACertNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := buildHTTPClient(false, "/nonexistent/ca.crt", "", "")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "failed to read CA certificate")
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_CACertInvalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary file with invalid content
|
||||
tempDir := t.TempDir()
|
||||
caCertPath := filepath.Join(tempDir, "invalid.crt")
|
||||
err := os.WriteFile(caCertPath, []byte("not a valid certificate"), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := buildHTTPClient(false, caCertPath, "", "")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "failed to parse CA certificate")
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_CertWithoutKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := buildHTTPClient(false, "", "/path/to/cert.crt", "")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together")
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_KeyWithoutCert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := buildHTTPClient(false, "", "", "/path/to/key.pem")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together")
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_CertAndKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create temporary cert and key files
|
||||
tempDir := t.TempDir()
|
||||
certPath := filepath.Join(tempDir, "client.crt")
|
||||
keyPath := filepath.Join(tempDir, "client.key")
|
||||
|
||||
// Generate a self-signed certificate and key for testing
|
||||
cert, key := generateTestCertAndKey(t)
|
||||
err := os.WriteFile(certPath, cert, 0o600)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(keyPath, key, 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
client, err := buildHTTPClient(false, "", certPath, keyPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
assert.NotEqual(t, http.DefaultClient, client)
|
||||
|
||||
// Check that client certificate is set
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.TLSClientConfig)
|
||||
assert.Len(t, transport.TLSClientConfig.Certificates, 1)
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_CertNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, err := buildHTTPClient(false, "", "/nonexistent/cert.crt", "/nonexistent/key.pem")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, client)
|
||||
assert.Contains(t, err.Error(), "failed to load client certificate")
|
||||
}
|
||||
|
||||
func TestBuildHTTPClient_InsecureWithCACert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary CA cert file
|
||||
tempDir := t.TempDir()
|
||||
caCertPath := filepath.Join(tempDir, "ca.crt")
|
||||
|
||||
// Generate a valid CA certificate
|
||||
caCertPEM := generateTestCACert(t)
|
||||
err := os.WriteFile(caCertPath, caCertPEM, 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Both insecure and CA cert can be set together
|
||||
client, err := buildHTTPClient(true, caCertPath, "", "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, transport.TLSClientConfig)
|
||||
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
|
||||
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
|
||||
}
|
||||
|
||||
// generateTestCertAndKey generates a self-signed certificate and key for testing
|
||||
func generateTestCertAndKey(t *testing.T) (certPEM, keyPEM []byte) {
|
||||
t.Helper()
|
||||
|
||||
// Generate a new ECDSA private key
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Task Org"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// Create the certificate
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Encode certificate to PEM
|
||||
certPEM = pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
|
||||
// Encode private key to PEM
|
||||
keyDER, err := x509.MarshalECPrivateKey(privateKey)
|
||||
require.NoError(t, err)
|
||||
keyPEM = pem.EncodeToMemory(&pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: keyDER,
|
||||
})
|
||||
|
||||
return certPEM, keyPEM
|
||||
}
|
||||
|
||||
// generateTestCACert generates a self-signed CA certificate for testing
|
||||
func generateTestCACert(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
|
||||
// Generate a new ECDSA private key
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a CA certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Test CA"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
// Create the certificate
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Encode certificate to PEM
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certDER,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
@@ -43,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 strings.Contains(entrypoint, "://") {
|
||||
if isRemoteEntrypoint(entrypoint) {
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ package taskfile
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dominikbraun/graph"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
@@ -43,8 +44,12 @@ type (
|
||||
insecure bool
|
||||
download bool
|
||||
offline bool
|
||||
trustedHosts []string
|
||||
tempDir string
|
||||
cacheExpiryDuration time.Duration
|
||||
caCert string
|
||||
cert string
|
||||
certKey string
|
||||
debugFunc DebugFunc
|
||||
promptFunc PromptFunc
|
||||
promptMutex sync.Mutex
|
||||
@@ -59,6 +64,7 @@ func NewReader(opts ...ReaderOption) *Reader {
|
||||
insecure: false,
|
||||
download: false,
|
||||
offline: false,
|
||||
trustedHosts: nil,
|
||||
tempDir: os.TempDir(),
|
||||
cacheExpiryDuration: 0,
|
||||
debugFunc: nil,
|
||||
@@ -119,6 +125,20 @@ func (o *offlineOption) ApplyToReader(r *Reader) {
|
||||
r.offline = o.offline
|
||||
}
|
||||
|
||||
// WithTrustedHosts configures the [Reader] with a list of trusted hosts for remote
|
||||
// Taskfiles. Hosts in this list will not prompt for user confirmation.
|
||||
func WithTrustedHosts(trustedHosts []string) ReaderOption {
|
||||
return &trustedHostsOption{trustedHosts: trustedHosts}
|
||||
}
|
||||
|
||||
type trustedHostsOption struct {
|
||||
trustedHosts []string
|
||||
}
|
||||
|
||||
func (o *trustedHostsOption) ApplyToReader(r *Reader) {
|
||||
r.trustedHosts = o.trustedHosts
|
||||
}
|
||||
|
||||
// WithTempDir sets the temporary directory that will be used by the [Reader].
|
||||
// By default, the reader uses [os.TempDir].
|
||||
func WithTempDir(tempDir string) ReaderOption {
|
||||
@@ -182,14 +202,59 @@ func (o *promptFuncOption) ApplyToReader(r *Reader) {
|
||||
r.promptFunc = o.promptFunc
|
||||
}
|
||||
|
||||
// WithReaderCACert sets the path to a custom CA certificate for TLS connections.
|
||||
func WithReaderCACert(caCert string) ReaderOption {
|
||||
return &readerCACertOption{caCert: caCert}
|
||||
}
|
||||
|
||||
type readerCACertOption struct {
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (o *readerCACertOption) ApplyToReader(r *Reader) {
|
||||
r.caCert = o.caCert
|
||||
}
|
||||
|
||||
// WithReaderCert sets the path to a client certificate for TLS connections.
|
||||
func WithReaderCert(cert string) ReaderOption {
|
||||
return &readerCertOption{cert: cert}
|
||||
}
|
||||
|
||||
type readerCertOption struct {
|
||||
cert string
|
||||
}
|
||||
|
||||
func (o *readerCertOption) ApplyToReader(r *Reader) {
|
||||
r.cert = o.cert
|
||||
}
|
||||
|
||||
// WithReaderCertKey sets the path to a client certificate key for TLS connections.
|
||||
func WithReaderCertKey(certKey string) ReaderOption {
|
||||
return &readerCertKeyOption{certKey: certKey}
|
||||
}
|
||||
|
||||
type readerCertKeyOption struct {
|
||||
certKey string
|
||||
}
|
||||
|
||||
func (o *readerCertKeyOption) ApplyToReader(r *Reader) {
|
||||
r.certKey = o.certKey
|
||||
}
|
||||
|
||||
// Read will read the Taskfile defined by the [Reader]'s [Node] and recurse
|
||||
// through any [ast.Includes] it finds, reading each included Taskfile and
|
||||
// building an [ast.TaskfileGraph] as it goes. If any errors occur, they will be
|
||||
// returned immediately.
|
||||
func (r *Reader) Read(ctx context.Context, node Node) (*ast.TaskfileGraph, error) {
|
||||
// Clean up git cache after reading all taskfiles
|
||||
defer func() {
|
||||
_ = CleanGitCache()
|
||||
}()
|
||||
|
||||
if err := r.include(ctx, node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.graph, nil
|
||||
}
|
||||
|
||||
@@ -206,6 +271,28 @@ func (r *Reader) promptf(format string, a ...any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isTrusted checks if a URI's host matches any of the trusted hosts patterns.
|
||||
func (r *Reader) isTrusted(uri string) bool {
|
||||
if len(r.trustedHosts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the URI to extract the host
|
||||
parsedURL, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (r *Reader) include(ctx context.Context, node Node) error {
|
||||
// Create a new vertex for the Taskfile
|
||||
vertex := &ast.TaskfileVertex{
|
||||
@@ -269,6 +356,9 @@ func (r *Reader) include(ctx context.Context, node Node) error {
|
||||
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
|
||||
WithParent(node),
|
||||
WithChecksum(include.Checksum),
|
||||
WithCACert(r.caCert),
|
||||
WithCert(r.cert),
|
||||
WithCertKey(r.certKey),
|
||||
)
|
||||
if err != nil {
|
||||
if include.Optional {
|
||||
@@ -459,9 +549,9 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
|
||||
|
||||
// If there is no manual checksum pin, run the automatic checks
|
||||
if node.Checksum() == "" {
|
||||
// Prompt the user if required
|
||||
// Prompt the user if required (unless host is trusted)
|
||||
prompt := cache.ChecksumPrompt(checksum)
|
||||
if prompt != "" {
|
||||
if prompt != "" && !r.isTrusted(node.Location()) {
|
||||
if err := func() error {
|
||||
r.promptMutex.Lock()
|
||||
defer r.promptMutex.Unlock()
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
defaultTaskfiles = []string{
|
||||
// DefaultTaskfiles is the list of Taskfile file names supported by default.
|
||||
DefaultTaskfiles = []string{
|
||||
"Taskfile.yml",
|
||||
"taskfile.yml",
|
||||
"Taskfile.yaml",
|
||||
@@ -28,6 +29,7 @@ var (
|
||||
"text/x-yaml",
|
||||
"application/yaml",
|
||||
"application/x-yaml",
|
||||
"application/octet-stream",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,7 +38,7 @@ var (
|
||||
// at the given URL with any of the default Taskfile files names. If any of
|
||||
// these match a file, the first matching path will be returned. If no files are
|
||||
// found, an error will be returned.
|
||||
func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
|
||||
func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL, error) {
|
||||
// Create a new HEAD request for the given URL to check if the resource exists
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
|
||||
if err != nil {
|
||||
@@ -44,7 +46,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
|
||||
}
|
||||
|
||||
// Request the given URL
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
|
||||
@@ -66,7 +68,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
|
||||
|
||||
// If the request was not successful, append the default Taskfile names to
|
||||
// the URL and return the URL of the first successful request
|
||||
for _, taskfile := range defaultTaskfiles {
|
||||
for _, taskfile := range DefaultTaskfiles {
|
||||
// Fixes a bug with JoinPath where a leading slash is not added to the
|
||||
// path if it is empty
|
||||
if u.Path == "" {
|
||||
@@ -76,7 +78,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
|
||||
req.URL = alt
|
||||
|
||||
// Try the alternative URL
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# https://taskfile.dev
|
||||
# yaml-language-server: $schema=https://taskfile.dev/schema.json
|
||||
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
GREETING: Hello, world!
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: Print a greeting message
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
silent: true
|
||||
|
||||
@@ -3,24 +3,35 @@ package ast
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
type TaskRC struct {
|
||||
Version *semver.Version `yaml:"version"`
|
||||
Verbose *bool `yaml:"verbose"`
|
||||
Concurrency *int `yaml:"concurrency"`
|
||||
Remote Remote `yaml:"remote"`
|
||||
Experiments map[string]int `yaml:"experiments"`
|
||||
Version *semver.Version `yaml:"version"`
|
||||
Verbose *bool `yaml:"verbose"`
|
||||
Silent *bool `yaml:"silent"`
|
||||
Color *bool `yaml:"color"`
|
||||
DisableFuzzy *bool `yaml:"disable-fuzzy"`
|
||||
Concurrency *int `yaml:"concurrency"`
|
||||
Interactive *bool `yaml:"interactive"`
|
||||
Remote Remote `yaml:"remote"`
|
||||
Failfast bool `yaml:"failfast"`
|
||||
Experiments map[string]int `yaml:"experiments"`
|
||||
}
|
||||
|
||||
type Remote struct {
|
||||
Insecure *bool `yaml:"insecure"`
|
||||
Offline *bool `yaml:"offline"`
|
||||
Timeout *time.Duration `yaml:"timeout"`
|
||||
CacheExpiry *time.Duration `yaml:"cache-expiry"`
|
||||
Insecure *bool `yaml:"insecure"`
|
||||
Offline *bool `yaml:"offline"`
|
||||
Timeout *time.Duration `yaml:"timeout"`
|
||||
CacheExpiry *time.Duration `yaml:"cache-expiry"`
|
||||
CacheDir *string `yaml:"cache-dir"`
|
||||
TrustedHosts []string `yaml:"trusted-hosts"`
|
||||
CACert *string `yaml:"cacert"`
|
||||
Cert *string `yaml:"cert"`
|
||||
CertKey *string `yaml:"cert-key"`
|
||||
}
|
||||
|
||||
// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
|
||||
@@ -42,7 +53,21 @@ func (t *TaskRC) Merge(other *TaskRC) {
|
||||
t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline)
|
||||
t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)
|
||||
t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)
|
||||
t.Remote.CacheDir = cmp.Or(other.Remote.CacheDir, t.Remote.CacheDir)
|
||||
if len(other.Remote.TrustedHosts) > 0 {
|
||||
merged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts)
|
||||
slices.Sort(merged)
|
||||
t.Remote.TrustedHosts = slices.Compact(merged)
|
||||
}
|
||||
t.Remote.CACert = cmp.Or(other.Remote.CACert, t.Remote.CACert)
|
||||
t.Remote.Cert = cmp.Or(other.Remote.Cert, t.Remote.Cert)
|
||||
t.Remote.CertKey = cmp.Or(other.Remote.CertKey, t.Remote.CertKey)
|
||||
|
||||
t.Verbose = cmp.Or(other.Verbose, t.Verbose)
|
||||
t.Silent = cmp.Or(other.Silent, t.Silent)
|
||||
t.Color = cmp.Or(other.Color, t.Color)
|
||||
t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)
|
||||
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
|
||||
t.Interactive = cmp.Or(other.Interactive, t.Interactive)
|
||||
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package taskrc
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/fsext"
|
||||
"github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
@@ -59,11 +60,14 @@ func GetConfig(dir string) (*ast.TaskRC, error) {
|
||||
// Find all the nodes from the given directory up to the users home directory
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return config, err
|
||||
}
|
||||
entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs)
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return config, err
|
||||
}
|
||||
|
||||
// Reverse the entrypoints since we want the child files to override parent ones
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -135,3 +136,174 @@ func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in pa
|
||||
},
|
||||
}, cfg)
|
||||
}
|
||||
|
||||
func TestGetConfig_RemoteTrustedHosts(t *testing.T) { //nolint:paralleltest // cannot run in parallel
|
||||
_, _, localDir := setupDirs(t)
|
||||
|
||||
// Test with single host
|
||||
configYAML := `
|
||||
remote:
|
||||
trusted-hosts:
|
||||
- github.com
|
||||
`
|
||||
writeFile(t, localDir, ".taskrc.yml", configYAML)
|
||||
|
||||
cfg, err := GetConfig(localDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
assert.Equal(t, []string{"github.com"}, cfg.Remote.TrustedHosts)
|
||||
|
||||
// Test with multiple hosts
|
||||
configYAML = `
|
||||
remote:
|
||||
trusted-hosts:
|
||||
- github.com
|
||||
- gitlab.com
|
||||
- example.com:8080
|
||||
`
|
||||
writeFile(t, localDir, ".taskrc.yml", configYAML)
|
||||
|
||||
cfg, err = GetConfig(localDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
assert.Equal(t, []string{"github.com", "gitlab.com", "example.com:8080"}, cfg.Remote.TrustedHosts)
|
||||
}
|
||||
|
||||
func TestGetConfig_RemoteTrustedHostsMerge(t *testing.T) { //nolint:paralleltest // cannot run in parallel
|
||||
t.Run("file-based merge precedence", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
|
||||
xdgConfigDir, homeDir, localDir := setupDirs(t)
|
||||
|
||||
// XDG config has github.com and gitlab.com
|
||||
xdgConfig := `
|
||||
remote:
|
||||
trusted-hosts:
|
||||
- github.com
|
||||
- gitlab.com
|
||||
timeout: "30s"
|
||||
`
|
||||
writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfig)
|
||||
|
||||
// Home config has example.com (should be combined with XDG)
|
||||
homeConfig := `
|
||||
remote:
|
||||
trusted-hosts:
|
||||
- example.com
|
||||
`
|
||||
writeFile(t, homeDir, ".taskrc.yml", homeConfig)
|
||||
|
||||
cfg, err := GetConfig(localDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
// Home config entries come first, then XDG
|
||||
assert.Equal(t, []string{"example.com", "github.com", "gitlab.com"}, cfg.Remote.TrustedHosts)
|
||||
|
||||
// Test with local config too
|
||||
localConfig := `
|
||||
remote:
|
||||
trusted-hosts:
|
||||
- local.dev
|
||||
`
|
||||
writeFile(t, localDir, ".taskrc.yml", localConfig)
|
||||
|
||||
cfg, err = GetConfig(localDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cfg)
|
||||
// Local config entries come first
|
||||
assert.Equal(t, []string{"example.com", "github.com", "gitlab.com", "local.dev"}, cfg.Remote.TrustedHosts)
|
||||
})
|
||||
|
||||
t.Run("merge edge cases", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
|
||||
tests := []struct {
|
||||
name string
|
||||
base *ast.TaskRC
|
||||
other *ast.TaskRC
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "merge hosts into empty",
|
||||
base: &ast.TaskRC{},
|
||||
other: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: []string{"github.com"},
|
||||
},
|
||||
},
|
||||
expected: []string{"github.com"},
|
||||
},
|
||||
{
|
||||
name: "merge combines lists",
|
||||
base: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: []string{"base.com"},
|
||||
},
|
||||
},
|
||||
other: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: []string{"other.com"},
|
||||
},
|
||||
},
|
||||
expected: []string{"base.com", "other.com"},
|
||||
},
|
||||
{
|
||||
name: "merge empty list does not override",
|
||||
base: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: []string{"base.com"},
|
||||
},
|
||||
},
|
||||
other: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: []string{},
|
||||
},
|
||||
},
|
||||
expected: []string{"base.com"},
|
||||
},
|
||||
{
|
||||
name: "merge nil does not override",
|
||||
base: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: []string{"base.com"},
|
||||
},
|
||||
},
|
||||
other: &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
TrustedHosts: nil,
|
||||
},
|
||||
},
|
||||
expected: []string{"base.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
|
||||
tt.base.Merge(tt.other)
|
||||
assert.Equal(t, tt.expected, tt.base.Remote.TrustedHosts)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all remote fields merge", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
|
||||
insecureTrue := true
|
||||
offlineTrue := true
|
||||
timeout := 30 * time.Second
|
||||
cacheExpiry := 1 * time.Hour
|
||||
|
||||
base := &ast.TaskRC{}
|
||||
other := &ast.TaskRC{
|
||||
Remote: ast.Remote{
|
||||
Insecure: &insecureTrue,
|
||||
Offline: &offlineTrue,
|
||||
Timeout: &timeout,
|
||||
CacheExpiry: &cacheExpiry,
|
||||
TrustedHosts: []string{"github.com", "gitlab.com"},
|
||||
},
|
||||
}
|
||||
|
||||
base.Merge(other)
|
||||
|
||||
assert.Equal(t, &insecureTrue, base.Remote.Insecure)
|
||||
assert.Equal(t, &offlineTrue, base.Remote.Offline)
|
||||
assert.Equal(t, &timeout, base.Remote.Timeout)
|
||||
assert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry)
|
||||
assert.Equal(t, []string{"github.com", "gitlab.com"}, base.Remote.TrustedHosts)
|
||||
})
|
||||
}
|
||||
|
||||
14
testdata/failfast/default/Taskfile.yaml
vendored
Normal file
14
testdata/failfast/default/Taskfile.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
deps:
|
||||
- dep1
|
||||
- dep2
|
||||
- dep3
|
||||
- dep4
|
||||
|
||||
dep1: sleep 0.1 && echo 'dep1'
|
||||
dep2: sleep 0.2 && echo 'dep2'
|
||||
dep3: sleep 0.3 && echo 'dep3'
|
||||
dep4: exit 1
|
||||
1
testdata/failfast/default/testdata/TestFailfast-Default-default-err-run.golden
vendored
Normal file
1
testdata/failfast/default/testdata/TestFailfast-Default-default-err-run.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
||||
3
testdata/failfast/default/testdata/TestFailfast-Default-default.golden
vendored
Normal file
3
testdata/failfast/default/testdata/TestFailfast-Default-default.golden
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
dep1
|
||||
dep2
|
||||
dep3
|
||||
1
testdata/failfast/default/testdata/TestFailfast-Option-default-err-run.golden
vendored
Normal file
1
testdata/failfast/default/testdata/TestFailfast-Option-default-err-run.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
||||
1
testdata/failfast/default/testdata/TestFailfast-Option-default.golden
vendored
Normal file
1
testdata/failfast/default/testdata/TestFailfast-Option-default.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
15
testdata/failfast/task/Taskfile.yaml
vendored
Normal file
15
testdata/failfast/task/Taskfile.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
deps:
|
||||
- dep1
|
||||
- dep2
|
||||
- dep3
|
||||
- dep4
|
||||
failfast: true
|
||||
|
||||
dep1: sleep 0.1 && echo 'dep1'
|
||||
dep2: sleep 0.2 && echo 'dep2'
|
||||
dep3: sleep 0.3 && echo 'dep3'
|
||||
dep4: exit 1
|
||||
1
testdata/failfast/task/testdata/TestFailfast-Task-task-err-run.golden
vendored
Normal file
1
testdata/failfast/task/testdata/TestFailfast-Task-task-err-run.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1
|
||||
1
testdata/failfast/task/testdata/TestFailfast-Task-task.golden
vendored
Normal file
1
testdata/failfast/task/testdata/TestFailfast-Task-task.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
178
testdata/if/Taskfile.yml
vendored
Normal file
178
testdata/if/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
SHOULD_RUN: "yes"
|
||||
ENV: "prod"
|
||||
FEATURE_ENABLED: "true"
|
||||
FEATURE_DISABLED: "false"
|
||||
|
||||
tasks:
|
||||
# Basic command-level if (condition met)
|
||||
cmd-if-true:
|
||||
cmds:
|
||||
- cmd: echo "executed"
|
||||
if: "true"
|
||||
|
||||
# Basic command-level if (condition not met)
|
||||
cmd-if-false:
|
||||
cmds:
|
||||
- cmd: echo "should not appear"
|
||||
if: "false"
|
||||
- echo "this runs"
|
||||
|
||||
# Task-level if (condition met)
|
||||
task-if-true:
|
||||
if: "true"
|
||||
cmds:
|
||||
- echo "task executed"
|
||||
|
||||
# Task-level if (condition not met)
|
||||
task-if-false:
|
||||
if: "false"
|
||||
cmds:
|
||||
- echo "should not appear"
|
||||
|
||||
# With template variables
|
||||
if-with-template:
|
||||
cmds:
|
||||
- cmd: echo "Running because SHOULD_RUN={{.SHOULD_RUN}}"
|
||||
if: '[ "{{.SHOULD_RUN}}" = "yes" ]'
|
||||
|
||||
# If inside for loop
|
||||
if-in-for-loop:
|
||||
cmds:
|
||||
- for: ["a", "b", "c"]
|
||||
cmd: echo "processing {{.ITEM}}"
|
||||
if: '[ "{{.ITEM}}" != "b" ]'
|
||||
|
||||
# If on task call
|
||||
if-on-task-call:
|
||||
cmds:
|
||||
- task: subtask
|
||||
if: "true"
|
||||
|
||||
subtask:
|
||||
internal: true
|
||||
cmds:
|
||||
- echo "subtask ran"
|
||||
|
||||
# If combined with platforms (both must pass)
|
||||
if-with-platforms:
|
||||
cmds:
|
||||
- cmd: echo "condition and platform met"
|
||||
platforms: [linux, darwin, windows]
|
||||
if: "true"
|
||||
|
||||
# Skip task call
|
||||
skip-task-call:
|
||||
cmds:
|
||||
- task: subtask
|
||||
if: "false"
|
||||
- echo "after skipped task call"
|
||||
|
||||
# Task call in cmds with if condition met
|
||||
task-call-if-true:
|
||||
cmds:
|
||||
- task: subtask
|
||||
if: "true"
|
||||
- echo "after task call"
|
||||
|
||||
# Task call in cmds with if condition not met
|
||||
task-call-if-false:
|
||||
cmds:
|
||||
- task: subtask
|
||||
if: "false"
|
||||
- echo "continues after skipped task"
|
||||
|
||||
# Template eq - condition met
|
||||
template-eq-true:
|
||||
cmds:
|
||||
- cmd: echo "env is prod"
|
||||
if: '{{ eq .ENV "prod" }}'
|
||||
|
||||
# Template eq - condition not met
|
||||
template-eq-false:
|
||||
cmds:
|
||||
- cmd: echo "should not appear"
|
||||
if: '{{ eq .ENV "dev" }}'
|
||||
- echo "this runs"
|
||||
|
||||
# Template ne (not equal)
|
||||
template-ne:
|
||||
cmds:
|
||||
- cmd: echo "env is not dev"
|
||||
if: '{{ ne .ENV "dev" }}'
|
||||
|
||||
# Template with boolean-like variable
|
||||
template-bool-true:
|
||||
cmds:
|
||||
- cmd: echo "feature enabled"
|
||||
if: '{{ eq .FEATURE_ENABLED "true" }}'
|
||||
|
||||
# Template with boolean-like variable (false)
|
||||
template-bool-false:
|
||||
cmds:
|
||||
- cmd: echo "should not appear"
|
||||
if: '{{ eq .FEATURE_DISABLED "true" }}'
|
||||
- echo "feature was disabled"
|
||||
|
||||
# Direct true/false from template
|
||||
template-direct-true:
|
||||
cmds:
|
||||
- cmd: echo "direct true works"
|
||||
if: '{{ .FEATURE_ENABLED }}'
|
||||
|
||||
# Direct true/false from template (false case)
|
||||
template-direct-false:
|
||||
cmds:
|
||||
- cmd: echo "should not appear"
|
||||
if: '{{ .FEATURE_DISABLED }}'
|
||||
- echo "direct false skipped correctly"
|
||||
|
||||
# Template with CLI variable override
|
||||
template-cli-var:
|
||||
cmds:
|
||||
- cmd: echo "MY_VAR is yes"
|
||||
if: '{{ eq .MY_VAR "yes" }}'
|
||||
|
||||
# Combined template conditions with and
|
||||
template-and:
|
||||
cmds:
|
||||
- cmd: echo "both conditions met"
|
||||
if: '{{ and (eq .ENV "prod") (eq .FEATURE_ENABLED "true") }}'
|
||||
|
||||
# Combined template conditions with or
|
||||
template-or:
|
||||
cmds:
|
||||
- cmd: echo "at least one condition met"
|
||||
if: '{{ or (eq .ENV "dev") (eq .ENV "prod") }}'
|
||||
|
||||
# Task-level if with template
|
||||
task-level-template:
|
||||
if: '{{ eq .ENV "prod" }}'
|
||||
cmds:
|
||||
- echo "task runs in prod"
|
||||
|
||||
# Task-level if with template (not met)
|
||||
task-level-template-false:
|
||||
if: '{{ eq .ENV "dev" }}'
|
||||
cmds:
|
||||
- echo "should not appear"
|
||||
|
||||
# Task-level if with dynamic variable (condition met)
|
||||
task-if-dynamic-true:
|
||||
vars:
|
||||
ENABLE_FEATURE:
|
||||
sh: 'echo "true"'
|
||||
if: '{{ eq .ENABLE_FEATURE "true" }}'
|
||||
cmds:
|
||||
- echo "dynamic feature enabled"
|
||||
|
||||
# Task-level if with dynamic variable (condition not met)
|
||||
task-if-dynamic-false:
|
||||
vars:
|
||||
ENABLE_FEATURE:
|
||||
sh: 'echo "false"'
|
||||
if: '{{ eq .ENABLE_FEATURE "true" }}'
|
||||
cmds:
|
||||
- echo "should not appear"
|
||||
1
testdata/if/testdata/TestIf-cmd-if-false.golden
vendored
Normal file
1
testdata/if/testdata/TestIf-cmd-if-false.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
this runs
|
||||
1
testdata/if/testdata/TestIf-cmd-if-true.golden
vendored
Normal file
1
testdata/if/testdata/TestIf-cmd-if-true.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
executed
|
||||
7
testdata/if/testdata/TestIf-if-in-for-loop.golden
vendored
Normal file
7
testdata/if/testdata/TestIf-if-in-for-loop.golden
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
task: "if-in-for-loop" started
|
||||
task: [if-in-for-loop] echo "processing a"
|
||||
processing a
|
||||
task: [if-in-for-loop] if condition not met - skipped
|
||||
task: [if-in-for-loop] echo "processing c"
|
||||
processing c
|
||||
task: "if-in-for-loop" finished
|
||||
5
testdata/if/testdata/TestIf-task-call-if-false.golden
vendored
Normal file
5
testdata/if/testdata/TestIf-task-call-if-false.golden
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
task: "task-call-if-false" started
|
||||
task: [task-call-if-false] if condition not met - skipped
|
||||
task: [task-call-if-false] echo "continues after skipped task"
|
||||
continues after skipped task
|
||||
task: "task-call-if-false" finished
|
||||
2
testdata/if/testdata/TestIf-task-call-if-true.golden
vendored
Normal file
2
testdata/if/testdata/TestIf-task-call-if-true.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
subtask ran
|
||||
after task call
|
||||
2
testdata/if/testdata/TestIf-task-if-dynamic-false.golden
vendored
Normal file
2
testdata/if/testdata/TestIf-task-if-dynamic-false.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
task: dynamic variable: "echo \"false\"" result: "false"
|
||||
task: if condition not met - skipped: "task-if-dynamic-false"
|
||||
1
testdata/if/testdata/TestIf-task-if-dynamic-true.golden
vendored
Normal file
1
testdata/if/testdata/TestIf-task-if-dynamic-true.golden
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dynamic feature enabled
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user