mirror of
https://github.com/go-task/task.git
synced 2026-05-18 13:15:41 +02:00
Compare commits
165 Commits
better-gh-
...
feat/gitla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8abadb4f0 | ||
|
|
542fe465e9 | ||
|
|
70b6cd8ee0 | ||
|
|
1eb5720e7e | ||
|
|
1b06da16f6 | ||
|
|
6e37e3d7a7 | ||
|
|
4bea638b05 | ||
|
|
8f2d17a387 | ||
|
|
f7d17fffad | ||
|
|
697ef35303 | ||
|
|
8fe3d048fa | ||
|
|
d61d92dfdf | ||
|
|
114ac1eedc | ||
|
|
a016b7b72b | ||
|
|
219bf3e5a5 | ||
|
|
b36fcfd8bb | ||
|
|
ef8fb84c8f | ||
|
|
ddecd51715 | ||
|
|
88fc6d4f24 | ||
|
|
07fbd9887e | ||
|
|
f2e32beabd | ||
|
|
8fa9dc04ac | ||
|
|
0c98f1ad13 | ||
|
|
a12cc6e843 | ||
|
|
44e1350d0c | ||
|
|
2973dd75f9 | ||
|
|
a10f1d2ee7 | ||
|
|
f727b55fbc | ||
|
|
363153cbf3 | ||
|
|
6b436eda48 | ||
|
|
2ddebca4e1 | ||
|
|
b6ab6153a2 | ||
|
|
bca99520bf | ||
|
|
1312ee8a81 | ||
|
|
b7cb204f10 | ||
|
|
921f84157a | ||
|
|
470ef30f8f | ||
|
|
87b12e663e | ||
|
|
e61700f36d | ||
|
|
8b6aca5722 | ||
|
|
19d8fae5f9 | ||
|
|
c387048f8f | ||
|
|
f2b3ba1263 | ||
|
|
2139e32426 | ||
|
|
d4e168128b | ||
|
|
54bdcba369 | ||
|
|
c55c969474 | ||
|
|
73f9879421 | ||
|
|
aa83651da2 | ||
|
|
4ddad9f9f7 | ||
|
|
080ee8869f | ||
|
|
af943b064b | ||
|
|
962eada344 | ||
|
|
a0d9750edf | ||
|
|
a1b8985df0 | ||
|
|
21daf6160a | ||
|
|
c70d28f7b8 | ||
|
|
90e6ef88dc | ||
|
|
a788034148 | ||
|
|
90bcbe9ba5 | ||
|
|
60a808ca23 | ||
|
|
edc80aed81 | ||
|
|
68bea7f273 | ||
|
|
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 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1,5 @@
|
||||
* text=auto
|
||||
*.mdx -linguist-detectable
|
||||
|
||||
# Keep LF line endings in testdata for consistent checksums across platforms
|
||||
testdata/** text eol=lf
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -7,3 +7,5 @@ Please understand that it may take some time to be reviewed.
|
||||
Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
|
||||
|
||||
-->
|
||||
|
||||
- [ ] I have read and followed the [Contribution Guide](https://taskfile.dev/contributing/)
|
||||
|
||||
112
.github/workflows/ci.yml
vendored
112
.github/workflows/ci.yml
vendored
@@ -1,112 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 🔨 Build (${{ matrix.go-version }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ⬇️ Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: 🔨 Build
|
||||
run: go build -v ./cmd/task
|
||||
|
||||
test:
|
||||
name: 🧪 Test (${{ matrix.go-version }}, ${{ matrix.platform }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ⬇️ Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: ⬇️ Setup Task
|
||||
uses: go-task/setup-task@v1
|
||||
|
||||
- name: 🧪 Test
|
||||
run: task test --output group --output-group-begin '::group::{{.TASK}}' --output-group-end '::endgroup::'
|
||||
|
||||
lint:
|
||||
name: 🔍 Lint (${{ matrix.go-version }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ⬇️ Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: 🔍 Lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.7.1
|
||||
|
||||
lint-jsonschema:
|
||||
name: 📋 Lint JSON Schema
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: ⬇️ Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.14
|
||||
|
||||
- name: ⬇️ Install check-jsonschema
|
||||
run: python -m pip install 'check-jsonschema==0.27.3'
|
||||
|
||||
- name: 📋 Validate JSON Schema
|
||||
run: check-jsonschema --check-metaschema website/src/public/schema.json
|
||||
|
||||
ci-status:
|
||||
name: ✅ CI
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, test, lint, lint-jsonschema]
|
||||
if: always()
|
||||
steps:
|
||||
- name: ✅ Check CI status
|
||||
run: |
|
||||
if [[ "${{ needs.build.result }}" != "success" ]] || \
|
||||
[[ "${{ needs.test.result }}" != "success" ]] || \
|
||||
[[ "${{ needs.lint.result }}" != "success" ]] || \
|
||||
[[ "${{ needs.lint-jsonschema.result }}" != "success" ]]; then
|
||||
echo "CI failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "CI passed"
|
||||
@@ -4,13 +4,16 @@ on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
issue-awaiting-response:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
|
||||
7
.github/workflows/issue-closed.yml
vendored
7
.github/workflows/issue-closed.yml
vendored
@@ -4,13 +4,16 @@ on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
issue-closed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const labels = await github.paginate(
|
||||
github.rest.issues.listLabelsOnIssue, {
|
||||
|
||||
49
.github/workflows/issue-experiment.yml
vendored
49
.github/workflows/issue-experiment.yml
vendored
@@ -2,16 +2,19 @@ name: issue experiment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
types: [field_added]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
issue-experiment-proposed:
|
||||
if: github.event.label.name == format('status{0} proposed', ':')
|
||||
issue-experiment-proposal:
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'proposal'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
@@ -20,12 +23,12 @@ jobs:
|
||||
body: 'This issue has been marked as an experiment proposal! :test_tube: It will now enter a period of consultation during which we encourage the community to provide feedback on the proposed design. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-draft:
|
||||
if: github.event.label.name == format('status{0} draft', ':')
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'draft'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
@@ -34,12 +37,12 @@ jobs:
|
||||
body: 'This experiment has been marked as a draft! :sparkles: This means that an initial implementation has been added to the latest release of Task! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-candidate:
|
||||
if: github.event.label.name == format('status{0} candidate', ':')
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'candidate'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
@@ -48,12 +51,12 @@ jobs:
|
||||
body: 'This experiment has been marked as a candidate! :fire: This means that the implementation is nearing completion and we are entering a period for final comments and feedback! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-stable:
|
||||
if: github.event.label.name == format('status{0} stable', ':')
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'stable'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
@@ -62,12 +65,12 @@ jobs:
|
||||
body: 'This experiment has been marked as stable! :metal: This means that the implementation is now final and ready to be released. No more changes will be made and the experiment is safe to use in production! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
|
||||
})
|
||||
issue-experiment-released:
|
||||
if: github.event.label.name == format('status{0} released', ':')
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'released'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
@@ -82,12 +85,12 @@ jobs:
|
||||
state: 'closed'
|
||||
})
|
||||
issue-experiment-abandoned:
|
||||
if: github.event.label.name == format('status{0} abandoned', ':')
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'abandoned'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
@@ -102,12 +105,12 @@ jobs:
|
||||
state: 'closed'
|
||||
})
|
||||
issue-experiment-superseded:
|
||||
if: github.event.label.name == format('status{0} superseded', ':')
|
||||
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'superseded'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
|
||||
7
.github/workflows/issue-needs-triage.yml
vendored
7
.github/workflows/issue-needs-triage.yml
vendored
@@ -4,13 +4,16 @@ on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
issue-needs-triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const labels = await github.paginate(
|
||||
github.rest.issues.listLabelsOnIssue, {
|
||||
|
||||
46
.github/workflows/lint.yml
vendored
Normal file
46
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.25.x, 1.26.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
|
||||
with:
|
||||
version: v2.11.4
|
||||
|
||||
lint-jsonschema:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.14
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: install check-jsonschema
|
||||
run: python -m pip install 'check-jsonschema==0.27.3'
|
||||
|
||||
- name: check-jsonschema (metaschema)
|
||||
run: check-jsonschema --check-metaschema website/src/public/schema.json
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "1.26.x"
|
||||
cache: true
|
||||
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: release --snapshot --clean --config .goreleaser-pr.yml
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: task_linux_amd64
|
||||
path: dist/task_linux_amd64.tar.gz
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: task_linux_arm64
|
||||
path: dist/task_linux_arm64.tar.gz
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: task_darwin_amd64
|
||||
path: dist/task_darwin_amd64.tar.gz
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: task_darwin_arm64
|
||||
path: dist/task_darwin_arm64.tar.gz
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: task_windows_amd64
|
||||
path: dist/task_windows_amd64.zip
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: checksums
|
||||
path: dist/task_checksums.txt
|
||||
- uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
token: ${{secrets.GITHUB_TOKEN}}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: "📦 Build artifacts ready!"
|
||||
- uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
token: ${{secrets.GITHUB_TOKEN}}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
## 📦 Build artifacts ready!
|
||||
|
||||
Download binaries from [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
|
||||
|
||||
Available platforms: Linux, macOS, Windows (amd64, arm64)
|
||||
edit-mode: replace
|
||||
14
.github/workflows/release-nightly.yml
vendored
14
.github/workflows/release-nightly.yml
vendored
@@ -4,27 +4,31 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 0 * * *
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean --nightly -f .goreleaser-nightly.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GH_PAT}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
|
||||
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
|
||||
|
||||
32
.github/workflows/release.yml
vendored
32
.github/workflows/release.yml
vendored
@@ -3,51 +3,51 @@ name: goreleaser
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: read
|
||||
id-token: write # Required for OIDC
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: 1.26.x
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
|
||||
with:
|
||||
package_json_file: 'website/package.json'
|
||||
run_install: 'true'
|
||||
package_json_file: "website/package.json"
|
||||
run_install: "true"
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean --draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GH_PAT}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
|
||||
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
|
||||
|
||||
@@ -55,3 +55,5 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
task website:deploy:prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
19
.github/workflows/security.yml
vendored
Normal file
19
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Security
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
govulncheck:
|
||||
name: govulncheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4
|
||||
33
.github/workflows/test.yml
vendored
Normal file
33
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.25.x, 1.26.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Go ${{matrix.go-version}}
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
|
||||
- name: Test
|
||||
run: go run ./cmd/task test
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,3 +35,4 @@ tags
|
||||
/testdata/vars/v1
|
||||
/tmp
|
||||
node_modules
|
||||
website/.netlify/
|
||||
|
||||
@@ -33,6 +33,7 @@ formatters:
|
||||
linters:
|
||||
enable:
|
||||
- depguard
|
||||
- gosec
|
||||
- mirror
|
||||
- misspell
|
||||
- noctx
|
||||
@@ -41,6 +42,9 @@ linters:
|
||||
- tparallel
|
||||
- usetesting
|
||||
settings:
|
||||
gosec:
|
||||
excludes:
|
||||
- G306
|
||||
depguard:
|
||||
rules:
|
||||
main:
|
||||
|
||||
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:
|
||||
@@ -80,13 +82,14 @@ nfpms:
|
||||
|
||||
brews:
|
||||
- name: go-task
|
||||
description: Task runner / simpler Make alternative written in Go
|
||||
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
|
||||
license: MIT
|
||||
homepage: https://taskfile.dev
|
||||
directory: Formula
|
||||
repository:
|
||||
owner: go-task
|
||||
name: homebrew-tap
|
||||
token: "{{secrets.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
|
||||
test: system "#{bin}/task", "--help"
|
||||
install: |-
|
||||
bin.install "task"
|
||||
@@ -100,8 +103,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/
|
||||
@@ -128,6 +131,7 @@ winget:
|
||||
owner: go-task
|
||||
name: winget-pkgs
|
||||
branch: 'task-{{.Version}}'
|
||||
token: "{{secrets.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
|
||||
pull_request:
|
||||
enabled: true
|
||||
draft: false
|
||||
@@ -139,12 +143,11 @@ winget:
|
||||
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"
|
||||
@@ -155,7 +158,6 @@ npms:
|
||||
- "build-tool"
|
||||
- "task-runner"
|
||||
|
||||
|
||||
cloudsmiths:
|
||||
- organization: "task"
|
||||
repository: "{{if not .IsNightly}}task{{end}}"
|
||||
@@ -169,6 +171,6 @@ cloudsmiths:
|
||||
rpm:
|
||||
- "any-distro/any-version"
|
||||
alpine:
|
||||
- "any-distro/any-version"
|
||||
- "alpine/any-version"
|
||||
component: main
|
||||
republish: true
|
||||
|
||||
145
CHANGELOG.md
145
CHANGELOG.md
@@ -2,38 +2,157 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added `absPath` template function that resolves a path to its absolute form,
|
||||
cleaning `..` and `.` components (#2681, #2788 by @mateenanjum).
|
||||
|
||||
## v3.50.0 - 2026-04-13
|
||||
|
||||
- Added `enum.ref` support in `requires`: enum constraints can now reference
|
||||
variables or template pipelines (e.g., `ref: .ALLOWED_ENVS`) instead of
|
||||
duplicating static lists. Combined with `sh:` variables, this enables fully
|
||||
dynamic enum validation (#2678 by @vmaerten).
|
||||
- Fixed Fish completion using hardcoded `task` binary name instead of
|
||||
`$GO_TASK_PROGNAME` for experiments cache (#2730, #2727 by @SergioChan).
|
||||
- Fixed watch mode ignoring SIGHUP signal, causing the watcher to exit instead
|
||||
of restarting (#2764, #2642).
|
||||
- Fixed a long time bug where the task wouldn't re-run as it should when using
|
||||
`method: timestamp` and the files listed on `generates:` were deleted.
|
||||
This makes `method: timestamp` behaves the same as `method: checksum`
|
||||
(#1230, #2716 by @drichardson).
|
||||
|
||||
## v3.49.1 - 2026-03-08
|
||||
|
||||
* Reverted #2632 for now, which caused some regressions. That change will be
|
||||
reworked (#2720, #2722, #2723).
|
||||
|
||||
## v3.49.0 - 2026-03-07
|
||||
|
||||
- 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
|
||||
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).
|
||||
- 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).
|
||||
- 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 shell completion scripts (Zsh, Fish, PowerShell) by adding missing
|
||||
flags and dynamic experimental feature detection (#2532 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).
|
||||
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
|
||||
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
|
||||
@boiledfroginthewell).
|
||||
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
|
||||
#1944 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
|
||||
|
||||
|
||||
23
README.md
23
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/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>
|
||||
<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>
|
||||
@@ -22,6 +22,11 @@
|
||||
<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" />
|
||||
@@ -29,4 +34,16 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>Community Sponsors</h2>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a target="_blank" href="https://cloudsmith.com/">
|
||||
<img src="website/src/public/img/cloudsmith.svg" height="100px" width="200px" title="Cloudsmith" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -117,7 +117,7 @@ func changelog(version *semver.Version) error {
|
||||
changelog = changelogReleaseRegex.ReplaceAllString(changelog, fmt.Sprintf("## v%s - %s", version, date))
|
||||
|
||||
// Write the changelog to the source file
|
||||
if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil {
|
||||
if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil { //nolint:gosec
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func changelog(version *semver.Version) error {
|
||||
changelogWithFrontmatter := fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelogWithVPre)
|
||||
|
||||
// Write the changelog to the target file
|
||||
return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644)
|
||||
return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644) //nolint:gosec
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
15
compiler.go
15
compiler.go
@@ -198,18 +198,21 @@ func (c *Compiler) ResetCache() {
|
||||
}
|
||||
|
||||
func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) {
|
||||
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
|
||||
// across platforms. This prevents issues with backslashes being interpreted
|
||||
// as escape sequences when paths are used in shell commands on Windows.
|
||||
allVars := map[string]string{
|
||||
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
||||
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
|
||||
"ROOT_DIR": c.Dir,
|
||||
"USER_WORKING_DIR": c.UserWorkingDir,
|
||||
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
|
||||
"ROOT_DIR": filepath.ToSlash(c.Dir),
|
||||
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
|
||||
"TASK_VERSION": version.GetVersion(),
|
||||
}
|
||||
if t != nil {
|
||||
allVars["TASK"] = t.Task
|
||||
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
|
||||
allVars["TASKFILE"] = t.Location.Taskfile
|
||||
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
|
||||
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
|
||||
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
|
||||
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
|
||||
} else {
|
||||
allVars["TASK"] = ""
|
||||
allVars["TASK_DIR"] = ""
|
||||
|
||||
@@ -22,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
|
||||
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
function __task_get_experiments --inherit-variable GO_TASK_PROGNAME
|
||||
set -l now (date +%s)
|
||||
set -l ttl 1 # Cache for 1 second only
|
||||
|
||||
@@ -16,7 +16,7 @@ function __task_get_experiments
|
||||
end
|
||||
|
||||
# Refresh cache
|
||||
set -g __task_experiments_cache (task --experiments 2>/dev/null)
|
||||
set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null)
|
||||
set -g __task_experiments_cache_time $now
|
||||
printf '%s\n' $__task_experiments_cache
|
||||
end
|
||||
@@ -54,7 +54,7 @@ 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
|
||||
@@ -86,6 +86,7 @@ complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task
|
||||
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'
|
||||
@@ -106,9 +107,13 @@ complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes t
|
||||
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 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'
|
||||
|
||||
@@ -40,6 +40,7 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
|
||||
[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'),
|
||||
@@ -75,6 +76,10 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
|
||||
$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')
|
||||
|
||||
@@ -33,6 +33,10 @@ function __task_list() {
|
||||
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
|
||||
@@ -41,12 +45,27 @@ 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() {
|
||||
@@ -71,6 +90,7 @@ _task() {
|
||||
'(-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: '
|
||||
@@ -96,6 +116,10 @@ _task() {
|
||||
'(--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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -50,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", message.Err.Error()))
|
||||
fmt.Fprintln(buf, color.RedString("- %s", message))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0].Err.Error()))
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0]))
|
||||
}
|
||||
} else {
|
||||
// Otherwise print the error message normally
|
||||
|
||||
@@ -195,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()
|
||||
|
||||
@@ -3,6 +3,7 @@ package errors
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
@@ -11,20 +12,23 @@ import (
|
||||
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when
|
||||
// searching the filesystem.
|
||||
type TaskfileNotFoundError struct {
|
||||
URI string
|
||||
Walk bool
|
||||
AskInit bool
|
||||
URI string
|
||||
Walk bool
|
||||
AskInit bool
|
||||
OwnerChange bool
|
||||
}
|
||||
|
||||
func (err TaskfileNotFoundError) Error() string {
|
||||
var walkText string
|
||||
if err.Walk {
|
||||
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)
|
||||
return fmt.Sprintf(`task: No Taskfile found at %q%s`, filepath.ToSlash(err.URI), walkText)
|
||||
}
|
||||
|
||||
func (err TaskfileNotFoundError) Code() int {
|
||||
@@ -51,7 +55,7 @@ type TaskfileInvalidError struct {
|
||||
}
|
||||
|
||||
func (err TaskfileInvalidError) Error() string {
|
||||
return fmt.Sprintf("task: Failed to parse %s:\n%v", err.URI, err.Err)
|
||||
return fmt.Sprintf("task: Failed to parse %s:\n%v", filepath.ToSlash(err.URI), err.Err)
|
||||
}
|
||||
|
||||
func (err TaskfileInvalidError) Code() int {
|
||||
@@ -70,7 +74,7 @@ func (err TaskfileFetchFailedError) Error() string {
|
||||
if err.HTTPStatusCode != 0 {
|
||||
statusText = fmt.Sprintf(" with status code %d (%s)", err.HTTPStatusCode, http.StatusText(err.HTTPStatusCode))
|
||||
}
|
||||
return fmt.Sprintf(`task: Download of %q failed%s`, err.URI, statusText)
|
||||
return fmt.Sprintf(`task: Download of %q failed%s`, filepath.ToSlash(err.URI), statusText)
|
||||
}
|
||||
|
||||
func (err TaskfileFetchFailedError) Code() int {
|
||||
@@ -86,7 +90,7 @@ type TaskfileNotTrustedError struct {
|
||||
func (err *TaskfileNotTrustedError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
`task: Taskfile %q not trusted by user`,
|
||||
err.URI,
|
||||
filepath.ToSlash(err.URI),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,7 +107,7 @@ type TaskfileNotSecureError struct {
|
||||
func (err *TaskfileNotSecureError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
`task: Taskfile %q cannot be downloaded over an insecure connection. You can override this by using the --insecure flag`,
|
||||
err.URI,
|
||||
filepath.ToSlash(err.URI),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,7 +124,7 @@ type TaskfileCacheNotFoundError struct {
|
||||
func (err *TaskfileCacheNotFoundError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
`task: Taskfile %q was not found in the cache. Remove the --offline flag to use a remote copy or download it using the --download flag`,
|
||||
err.URI,
|
||||
filepath.ToSlash(err.URI),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,12 +145,12 @@ func (err *TaskfileVersionCheckError) Error() string {
|
||||
if err.SchemaVersion == nil {
|
||||
return fmt.Sprintf(
|
||||
`task: Missing schema version in Taskfile %q`,
|
||||
err.URI,
|
||||
filepath.ToSlash(err.URI),
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"task: Invalid schema version in Taskfile %q:\nSchema version (%s) %s",
|
||||
err.URI,
|
||||
filepath.ToSlash(err.URI),
|
||||
err.SchemaVersion.String(),
|
||||
err.Message,
|
||||
)
|
||||
@@ -166,7 +170,7 @@ type TaskfileNetworkTimeoutError struct {
|
||||
func (err *TaskfileNetworkTimeoutError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
`task: Network connection timed out after %s while attempting to download Taskfile %q`,
|
||||
err.Timeout, err.URI,
|
||||
err.Timeout, filepath.ToSlash(err.URI),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,8 +187,8 @@ type TaskfileCycleError struct {
|
||||
|
||||
func (err TaskfileCycleError) Error() string {
|
||||
return fmt.Sprintf("task: include cycle detected between %s <--> %s",
|
||||
err.Source,
|
||||
err.Destination,
|
||||
filepath.ToSlash(err.Source),
|
||||
filepath.ToSlash(err.Destination),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -203,7 +207,7 @@ type TaskfileDoesNotMatchChecksum struct {
|
||||
func (err *TaskfileDoesNotMatchChecksum) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
|
||||
err.URI,
|
||||
filepath.ToSlash(err.URI),
|
||||
err.ActualChecksum,
|
||||
err.ExpectedChecksum,
|
||||
)
|
||||
|
||||
87
executor.go
87
executor.go
@@ -37,12 +37,17 @@ type (
|
||||
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
|
||||
@@ -62,6 +67,7 @@ type (
|
||||
Compiler *Compiler
|
||||
Output output.Output
|
||||
OutputStyle ast.Output
|
||||
OutputCIAuto bool
|
||||
TaskSorter sort.Sorter
|
||||
UserWorkingDir string
|
||||
EnableVersionCheck bool
|
||||
@@ -69,6 +75,7 @@ type (
|
||||
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
|
||||
@@ -271,6 +278,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.
|
||||
@@ -353,6 +412,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 {
|
||||
@@ -451,6 +523,21 @@ func (o *outputStyleOption) ApplyToExecutor(e *Executor) {
|
||||
e.OutputStyle = o.outputStyle
|
||||
}
|
||||
|
||||
// WithOutputCIAuto enables automatic selection of a CI-aware output style
|
||||
// (e.g. "gitlab") when a supported CI environment is detected and no explicit
|
||||
// output style is configured in the Taskfile or via CLI.
|
||||
func WithOutputCIAuto(enabled bool) ExecutorOption {
|
||||
return &outputCIAutoOption{enabled}
|
||||
}
|
||||
|
||||
type outputCIAutoOption struct {
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (o *outputCIAutoOption) ApplyToExecutor(e *Executor) {
|
||||
e.OutputCIAuto = o.enabled
|
||||
}
|
||||
|
||||
// WithTaskSorter sets the sorter that the [Executor] will use to sort tasks. By
|
||||
// default, the sorter is set to sort tasks alphabetically, but with tasks with
|
||||
// no namespace (in the root Taskfile) first.
|
||||
|
||||
153
executor_test.go
153
executor_test.go
@@ -165,6 +165,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
|
||||
// Create a golden fixture file for the output
|
||||
g := goldie.New(t,
|
||||
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
|
||||
goldie.WithEqualFn(NormalizedEqual),
|
||||
)
|
||||
|
||||
// Call setup and check for errors
|
||||
@@ -263,6 +264,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) {
|
||||
@@ -334,6 +352,41 @@ func TestRequires(t *testing.T) {
|
||||
),
|
||||
WithTask("var-defined-in-task"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("enum ref - passes validation"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var-ref"),
|
||||
WithVar("ENV", "dev"),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("enum ref - fails validation"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var-ref"),
|
||||
WithVar("ENV", "invalid"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("enum ref - ref to non-list"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var-ref-invalid"),
|
||||
WithVar("VALUE", "test"),
|
||||
WithRunError(),
|
||||
)
|
||||
NewExecutorTest(t,
|
||||
WithName("enum ref - ref to nonexistent var"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/requires"),
|
||||
),
|
||||
WithTask("validation-var-ref-nonexistent"),
|
||||
WithVar("ENV", "dev"),
|
||||
WithRunError(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: mock fs
|
||||
@@ -347,6 +400,7 @@ func TestSpecialVars(t *testing.T) {
|
||||
// Root
|
||||
"print-task",
|
||||
"print-root-dir",
|
||||
"print-root-taskfile",
|
||||
"print-taskfile",
|
||||
"print-taskfile-dir",
|
||||
"print-task-dir",
|
||||
@@ -700,6 +754,27 @@ func TestLabel(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1021,6 +1096,18 @@ func TestIncludeChecksum(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -1067,3 +1154,69 @@ func TestFailfast(t *testing.T) {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ func (tt *FormatterTest) run(t *testing.T) {
|
||||
// Create a golden fixture file for the output
|
||||
g := goldie.New(t,
|
||||
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
|
||||
goldie.WithEqualFn(NormalizedEqual),
|
||||
)
|
||||
|
||||
// Call setup and check for errors
|
||||
|
||||
139
go.mod
139
go.mod
@@ -1,67 +1,136 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.8
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.1.0
|
||||
charm.land/bubbletea/v2 v2.0.2
|
||||
charm.land/lipgloss/v2 v2.0.2
|
||||
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/fatih/color v1.19.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-git/go-billy/v5 v5.7.0
|
||||
github.com/go-git/go-git/v5 v5.16.4
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0
|
||||
github.com/go-task/template v0.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-getter v1.8.6
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0
|
||||
github.com/sajari/fuzzy v1.0.0
|
||||
github.com/sebdah/goldie/v2 v2.8.0
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/zeebo/xxh3 v1.0.2
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/term v0.37.0
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b
|
||||
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.20.0
|
||||
golang.org/x/term v0.42.0
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
|
||||
mvdan.cc/sh/v3 v3.13.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.2 // 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.3 // indirect
|
||||
cloud.google.com/go/storage v1.61.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
|
||||
github.com/aws/smithy-go v1.24.2 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/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.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // 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.14 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // 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.5 // 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.21 // 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.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da // 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.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.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
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.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.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/api v0.271.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
342
go.sum
342
go.sum
@@ -1,87 +1,188 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
|
||||
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
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.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
|
||||
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
|
||||
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/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.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
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.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.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
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.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/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.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
|
||||
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
|
||||
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.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/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-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||
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.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
|
||||
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.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY=
|
||||
github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo=
|
||||
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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/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=
|
||||
@@ -91,28 +192,32 @@ 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.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/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/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0 h1:vlSN6/CkEY0pY8KaB0yqo/pCLZvp9nhdbBdjipT4gWo=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.4.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/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=
|
||||
@@ -122,70 +227,97 @@ github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK
|
||||
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.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
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.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da h1:Vst9Tvq3G6f6pYBvxy7coi2arDsnOZ3Mkj8MkNarSK8=
|
||||
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da/go.mod h1:R49zft13memK20EgFAvmTbXBS0t29UvglnM0BCA1ldQ=
|
||||
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/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
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.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
|
||||
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.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.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/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
|
||||
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/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-20251109230715-65adef8e2c5b h1:vTpx76nZDTP/BAGnnhEXYjM+8nPKe9+I86qCErBvjCw=
|
||||
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b/go.mod h1:bDyKbUYKqkFunWmxxuSPrkYpln9QZcUsqu7W128qYW4=
|
||||
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.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
|
||||
mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -2,6 +2,7 @@ package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
@@ -50,7 +51,8 @@ func collectKeys(m map[string]bool) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
if v {
|
||||
keys = append(keys, k)
|
||||
// Normalize path separators for consistent sorting across platforms
|
||||
keys = append(keys, filepath.ToSlash(k))
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
@@ -53,6 +53,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
if len(t.Generates) > 0 {
|
||||
// For each specified 'generates' field, check whether the files actually exist
|
||||
for _, g := range t.Generates {
|
||||
// Exclusion patterns don't represent output files; skip them.
|
||||
if g.Negate {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -32,6 +32,28 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If generates are declared, ensure they all exist. A missing generated
|
||||
// file means the task must run regardless of timestamps.
|
||||
if len(t.Generates) > 0 {
|
||||
for _, g := range t.Generates {
|
||||
// Exclusion patterns don't represent output files; skip them.
|
||||
if g.Negate {
|
||||
continue
|
||||
}
|
||||
files, err := glob(t.Dir, g.Glob)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generates, err := Globs(t.Dir, t.Generates)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
|
||||
@@ -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"
|
||||
@@ -68,6 +71,7 @@ var (
|
||||
Dir string
|
||||
Entrypoint string
|
||||
Output ast.Output
|
||||
OutputCIAuto bool
|
||||
Color bool
|
||||
Interval time.Duration
|
||||
Failfast bool
|
||||
@@ -79,6 +83,11 @@ var (
|
||||
ClearCache bool
|
||||
Timeout time.Duration
|
||||
CacheExpiryDuration time.Duration
|
||||
RemoteCacheDir string
|
||||
CACert string
|
||||
Cert string
|
||||
CertKey string
|
||||
Interactive bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -122,26 +131,28 @@ 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.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
|
||||
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.")
|
||||
pflag.StringVarP(&Entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
|
||||
pflag.StringVarP(&Output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed|gitlab].")
|
||||
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.")
|
||||
OutputCIAuto = getConfig(config, "OUTPUT_CI_AUTO", func() *bool { return config.OutputCIAuto }, false)
|
||||
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, func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.")
|
||||
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.")
|
||||
|
||||
@@ -156,13 +167,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.StringSliceVar(&TrustedHosts, "trusted-hosts", config.Remote.TrustedHosts, "List of trusted hosts for remote Taskfiles (comma-separated).")
|
||||
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 {
|
||||
@@ -206,6 +244,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
|
||||
}
|
||||
|
||||
@@ -247,11 +290,16 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
|
||||
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),
|
||||
@@ -259,21 +307,52 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
|
||||
task.WithConcurrency(Concurrency),
|
||||
task.WithInterval(Interval),
|
||||
task.WithOutputStyle(Output),
|
||||
task.WithOutputCIAuto(OutputCIAuto),
|
||||
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,
|
||||
@@ -153,13 +153,16 @@ func SearchPath(path string, possibleFilenames []string) (string, error) {
|
||||
// also check if the user ID of the directory changes and abort if it does.
|
||||
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
|
||||
paths, err := SearchNPathRecursively(path, possibleFilenames, 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if len(paths) > 0 {
|
||||
// Regardless of the error, return the first possible filename.
|
||||
return paths[0], nil
|
||||
} else {
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return paths[0], nil
|
||||
}
|
||||
|
||||
// SearchNPathRecursively walks up the directory tree starting at the given
|
||||
@@ -188,10 +191,13 @@ 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 path == parentPath {
|
||||
return paths, nil
|
||||
} else if parentOwner != owner {
|
||||
return paths, os.ErrPermission
|
||||
}
|
||||
|
||||
owner = parentOwner
|
||||
|
||||
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...)
|
||||
|
||||
116
internal/output/gitlab.go
Normal file
116
internal/output/gitlab.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
)
|
||||
|
||||
// GitLab renders a task's output wrapped in [GitLab CI collapsible
|
||||
// section markers]. Section IDs are generated automatically so that
|
||||
// start and end markers always match and stay unique per invocation.
|
||||
//
|
||||
// GitLab wraps output at the task level via the [TaskWrapper] interface,
|
||||
// so each task (including its command announcements and all its cmds)
|
||||
// appears inside a single collapsible section. Nested task invocations
|
||||
// produce nested sections.
|
||||
//
|
||||
// [GitLab CI collapsible section markers]: https://docs.gitlab.com/ci/jobs/job_logs/#create-custom-collapsible-sections
|
||||
type GitLab struct {
|
||||
Collapsed bool
|
||||
ErrorOnly bool
|
||||
}
|
||||
|
||||
// WrapWriter is a passthrough for GitLab: wrapping happens at the task
|
||||
// level via WrapTask, not per command.
|
||||
func (g GitLab) WrapWriter(stdOut, stdErr io.Writer, _ string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
|
||||
return stdOut, stdErr, func(error) error { return nil }
|
||||
}
|
||||
|
||||
// WrapTask wraps an entire task's output in a single collapsible section.
|
||||
func (g GitLab) WrapTask(stdOut, _ io.Writer, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
|
||||
header := ""
|
||||
if cache != nil {
|
||||
header = templater.Replace("{{.TASK}}", cache)
|
||||
}
|
||||
if header == "" {
|
||||
header = "task"
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("%s_%s", gitlabSectionSlug(header), uuid.New().String()[:8])
|
||||
|
||||
gw := &gitlabWriter{
|
||||
writer: stdOut,
|
||||
id: id,
|
||||
header: header,
|
||||
collapsed: g.Collapsed,
|
||||
startTS: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return gw, gw, func(err error) error {
|
||||
if g.ErrorOnly && err == nil {
|
||||
return nil
|
||||
}
|
||||
return gw.close()
|
||||
}
|
||||
}
|
||||
|
||||
type gitlabWriter struct {
|
||||
mu sync.Mutex
|
||||
writer io.Writer
|
||||
buff bytes.Buffer
|
||||
id string
|
||||
header string
|
||||
collapsed bool
|
||||
startTS int64
|
||||
}
|
||||
|
||||
func (gw *gitlabWriter) Write(p []byte) (int, error) {
|
||||
gw.mu.Lock()
|
||||
defer gw.mu.Unlock()
|
||||
return gw.buff.Write(p)
|
||||
}
|
||||
|
||||
func (gw *gitlabWriter) close() error {
|
||||
gw.mu.Lock()
|
||||
defer gw.mu.Unlock()
|
||||
|
||||
if gw.buff.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
b.WriteString(gitlabSectionStart(gw.startTS, gw.id, gw.header, gw.collapsed))
|
||||
if _, err := io.Copy(&b, &gw.buff); err != nil {
|
||||
return err
|
||||
}
|
||||
b.WriteString(gitlabSectionEnd(time.Now().Unix(), gw.id))
|
||||
|
||||
_, err := io.Copy(gw.writer, &b)
|
||||
return err
|
||||
}
|
||||
|
||||
func gitlabSectionStart(ts int64, id, header string, collapsed bool) string {
|
||||
options := ""
|
||||
if collapsed {
|
||||
options = "[collapsed=true]"
|
||||
}
|
||||
return fmt.Sprintf("\x1b[0Ksection_start:%d:%s%s\r\x1b[0K%s\n", ts, id, options, header)
|
||||
}
|
||||
|
||||
func gitlabSectionEnd(ts int64, id string) string {
|
||||
return fmt.Sprintf("\x1b[0Ksection_end:%d:%s\r\x1b[0K\n", ts, id)
|
||||
}
|
||||
|
||||
var gitlabSlugDisallowed = regexp.MustCompile(`[^a-zA-Z0-9_.-]`)
|
||||
|
||||
func gitlabSectionSlug(s string) string {
|
||||
return gitlabSlugDisallowed.ReplaceAllString(s, "_")
|
||||
}
|
||||
@@ -13,6 +13,14 @@ type Output interface {
|
||||
WrapWriter(stdOut, stdErr io.Writer, prefix string, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc)
|
||||
}
|
||||
|
||||
// TaskWrapper is an optional interface that Output implementations can satisfy
|
||||
// to wrap an entire task's execution in a single enclosing block — including
|
||||
// the task's command announcements and all its commands' output — instead of
|
||||
// wrapping each command individually via WrapWriter.
|
||||
type TaskWrapper interface {
|
||||
WrapTask(stdOut, stdErr io.Writer, cache *templater.Cache) (io.Writer, io.Writer, CloseFunc)
|
||||
}
|
||||
|
||||
type CloseFunc func(err error) error
|
||||
|
||||
// Build the Output for the requested ast.Output.
|
||||
@@ -34,6 +42,14 @@ func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) {
|
||||
return nil, err
|
||||
}
|
||||
return NewPrefixed(logger), nil
|
||||
case "gitlab":
|
||||
if err := checkOutputGroupUnset(o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GitLab{
|
||||
Collapsed: o.GitLab.Collapsed,
|
||||
ErrorOnly: o.GitLab.ErrorOnly,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -121,6 +126,238 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
|
||||
assert.Equal(t, "std-out\nstd-err\n", b.String())
|
||||
}
|
||||
|
||||
func gitlabTaskCache(taskName string) *templater.Cache {
|
||||
return &templater.Cache{
|
||||
Vars: ast.NewVars(
|
||||
&ast.VarElement{
|
||||
Key: "TASK",
|
||||
Value: ast.Var{Value: taskName},
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
var gitlabMarkerPattern = regexp.MustCompile(
|
||||
`\x1b\[0Ksection_start:(\d+):(\S+?)(\[[^\]]+\])?\r\x1b\[0K(.*)\n` +
|
||||
`(?s)(.*)` +
|
||||
`\x1b\[0Ksection_end:(\d+):(\S+)\r\x1b\[0K\n$`,
|
||||
)
|
||||
|
||||
func TestGitLab(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("build"))
|
||||
|
||||
fmt.Fprintln(w, "hello")
|
||||
assert.Equal(t, "", b.String(), "output must be buffered until close")
|
||||
require.NoError(t, cleanup(nil))
|
||||
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m, "output should match GitLab section markers, got: %q", b.String())
|
||||
assert.Equal(t, m[2], m[7], "start and end section IDs must match")
|
||||
assert.Empty(t, m[3], "collapsed option should not be present by default")
|
||||
assert.Equal(t, "build", m[4], "section header should be the task name")
|
||||
assert.Equal(t, "hello\n", m[5], "wrapped content must be preserved")
|
||||
assert.Contains(t, m[2], "build_", "section ID should be prefixed with slugged task name")
|
||||
}
|
||||
|
||||
func TestGitLabUniqueSectionIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
o := output.GitLab{}
|
||||
|
||||
ids := make([]string, 3)
|
||||
for i := range ids {
|
||||
var b bytes.Buffer
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("build"))
|
||||
fmt.Fprintln(w, "x")
|
||||
require.NoError(t, cleanup(nil))
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m)
|
||||
ids[i] = m[2]
|
||||
}
|
||||
|
||||
assert.NotEqual(t, ids[0], ids[1])
|
||||
assert.NotEqual(t, ids[1], ids[2])
|
||||
assert.NotEqual(t, ids[0], ids[2])
|
||||
}
|
||||
|
||||
func TestGitLabCollapsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{Collapsed: true}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("build"))
|
||||
fmt.Fprintln(w, "x")
|
||||
require.NoError(t, cleanup(nil))
|
||||
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m)
|
||||
assert.Equal(t, "[collapsed=true]", m[3])
|
||||
}
|
||||
|
||||
func TestGitLabErrorOnlySwallowsOutputOnNoError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{ErrorOnly: true}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("build"))
|
||||
fmt.Fprintln(w, "hello")
|
||||
require.NoError(t, cleanup(nil))
|
||||
assert.Empty(t, b.String())
|
||||
}
|
||||
|
||||
func TestGitLabErrorOnlyShowsOutputOnError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{ErrorOnly: true}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("build"))
|
||||
fmt.Fprintln(w, "hello")
|
||||
require.NoError(t, cleanup(errors.New("boom")))
|
||||
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m)
|
||||
assert.Equal(t, "hello\n", m[5])
|
||||
}
|
||||
|
||||
func TestGitLabSlugSanitizesTaskName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("my task:with spaces"))
|
||||
fmt.Fprintln(w, "x")
|
||||
require.NoError(t, cleanup(nil))
|
||||
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m)
|
||||
assert.Regexp(t, `^[a-zA-Z0-9_.-]+$`, m[2], "section ID must only contain GitLab-allowed chars")
|
||||
}
|
||||
|
||||
func TestGitLabWrapWriterIsPassthrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{}
|
||||
w, _, cleanup := o.WrapWriter(&b, io.Discard, "", nil)
|
||||
|
||||
fmt.Fprintln(w, "hello")
|
||||
assert.Equal(t, "hello\n", b.String(), "WrapWriter must be a passthrough for GitLab")
|
||||
assert.NoError(t, cleanup(nil))
|
||||
assert.Equal(t, "hello\n", b.String(), "closer must be a no-op")
|
||||
}
|
||||
|
||||
func TestGitLabWrapTaskSingleSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("build"))
|
||||
|
||||
// Simulate multiple cmd outputs being written during a task's execution.
|
||||
fmt.Fprintln(w, "cmd 1 output")
|
||||
fmt.Fprintln(w, "cmd 2 output")
|
||||
fmt.Fprintln(w, "cmd 3 output")
|
||||
require.NoError(t, cleanup(nil))
|
||||
|
||||
// There must be exactly one section_start and one section_end.
|
||||
assert.Equal(t, 1, strings.Count(b.String(), "section_start:"))
|
||||
assert.Equal(t, 1, strings.Count(b.String(), "section_end:"))
|
||||
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m)
|
||||
assert.Equal(t, "cmd 1 output\ncmd 2 output\ncmd 3 output\n", m[5])
|
||||
}
|
||||
|
||||
func TestGitLabWrapTaskDurationElapsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
o := output.GitLab{}
|
||||
w, _, cleanup := o.WrapTask(&b, io.Discard, gitlabTaskCache("slow"))
|
||||
|
||||
fmt.Fprintln(w, "started")
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
fmt.Fprintln(w, "done")
|
||||
require.NoError(t, cleanup(nil))
|
||||
|
||||
m := gitlabMarkerPattern.FindStringSubmatch(b.String())
|
||||
require.NotNil(t, m)
|
||||
startTS, err := strconv.ParseInt(m[1], 10, 64)
|
||||
require.NoError(t, err)
|
||||
endTS, err := strconv.ParseInt(m[6], 10, 64)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, endTS-startTS, int64(1),
|
||||
"end TS must be at least 1 second after start TS when task takes >1s")
|
||||
}
|
||||
|
||||
func TestGitLabWrapTaskNested(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var root bytes.Buffer
|
||||
parent := output.GitLab{}
|
||||
parentW, _, parentClose := parent.WrapTask(&root, io.Discard, gitlabTaskCache("parent"))
|
||||
|
||||
fmt.Fprintln(parentW, "before child")
|
||||
|
||||
child := output.GitLab{}
|
||||
childW, _, childClose := child.WrapTask(parentW, io.Discard, gitlabTaskCache("child"))
|
||||
fmt.Fprintln(childW, "inside child")
|
||||
require.NoError(t, childClose(nil))
|
||||
|
||||
fmt.Fprintln(parentW, "after child")
|
||||
require.NoError(t, parentClose(nil))
|
||||
|
||||
out := root.String()
|
||||
// Two section_start and two section_end
|
||||
assert.Equal(t, 2, strings.Count(out, "section_start:"))
|
||||
assert.Equal(t, 2, strings.Count(out, "section_end:"))
|
||||
|
||||
// Order: parent start → child start → child end → parent end
|
||||
parentStart := strings.Index(out, "section_start:") // first
|
||||
childStart := strings.Index(out[parentStart+1:], "section_start:") + parentStart + 1
|
||||
childEnd := strings.Index(out, "section_end:")
|
||||
parentEnd := strings.LastIndex(out, "section_end:")
|
||||
assert.Less(t, parentStart, childStart, "child_start must come after parent_start")
|
||||
assert.Less(t, childStart, childEnd, "child_end must come after child_start")
|
||||
assert.Less(t, childEnd, parentEnd, "parent_end must come after child_end")
|
||||
}
|
||||
|
||||
func TestGitLabWrapTaskConcurrentWrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var root bytes.Buffer
|
||||
parent := output.GitLab{}
|
||||
parentW, _, parentClose := parent.WrapTask(&root, io.Discard, gitlabTaskCache("parent"))
|
||||
|
||||
const numChildren = 10
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numChildren; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
child := output.GitLab{}
|
||||
childW, _, childClose := child.WrapTask(parentW, io.Discard, gitlabTaskCache(fmt.Sprintf("child%d", i)))
|
||||
fmt.Fprintf(childW, "child %d output\n", i)
|
||||
_ = childClose(nil)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
require.NoError(t, parentClose(nil))
|
||||
|
||||
out := root.String()
|
||||
// 1 parent + 10 children = 11 section_start and 11 section_end
|
||||
assert.Equal(t, 11, strings.Count(out, "section_start:"))
|
||||
assert.Equal(t, 11, strings.Count(out, "section_end:"))
|
||||
// All 10 child outputs present
|
||||
for i := 0; i < numChildren; i++ {
|
||||
assert.Contains(t, out, fmt.Sprintf("child %d output", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixed(t *testing.T) { //nolint:paralleltest // cannot run in parallel
|
||||
var b bytes.Buffer
|
||||
l := &logger.Logger{
|
||||
|
||||
@@ -231,7 +231,7 @@ func formatMap(m map[string]any, indent int) string {
|
||||
spaces := strings.Repeat(" ", indent)
|
||||
|
||||
for k, v := range m {
|
||||
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v))
|
||||
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v)) //nolint:staticcheck
|
||||
}
|
||||
|
||||
return result.String()
|
||||
@@ -247,15 +247,17 @@ func printTaskRequires(l *logger.Logger, t *ast.Task) {
|
||||
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 {
|
||||
if v.Enum != nil && len(v.Enum.Value) > 0 {
|
||||
l.Outf(logger.Yellow, " - %s:\n", v.Name)
|
||||
l.Outf(logger.Yellow, " enum:\n")
|
||||
for _, enumValue := range v.Enum {
|
||||
for _, enumValue := range v.Enum.Value {
|
||||
l.Outf(logger.Yellow, " - %s\n", enumValue)
|
||||
}
|
||||
} else if v.Enum != nil && v.Enum.Ref != "" {
|
||||
l.Outf(logger.Yellow, " - %s:\n", v.Name)
|
||||
l.Outf(logger.Yellow, " enum:\n")
|
||||
l.Outf(logger.Yellow, " ref: %s\n", v.Enum.Ref)
|
||||
} else {
|
||||
// Simple required variable
|
||||
l.Outf(logger.Yellow, " - %s\n", v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/uuid"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
|
||||
@@ -34,6 +34,7 @@ func init() {
|
||||
"IsSH": IsSH, // Deprecated
|
||||
"joinPath": filepath.Join,
|
||||
"relPath": filepath.Rel,
|
||||
"absPath": filepath.Abs,
|
||||
"merge": merge,
|
||||
"spew": spew.Sdump,
|
||||
"fromYaml": fromYaml,
|
||||
|
||||
@@ -7,5 +7,5 @@ import (
|
||||
)
|
||||
|
||||
func IsTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
|
||||
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) //nolint:gosec
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.45.5
|
||||
3.50.0
|
||||
|
||||
186
requires.go
186
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, getEnumValues(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, getEnumValues(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: getEnumValues(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
|
||||
@@ -42,15 +187,15 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
||||
for _, requiredVar := range t.Requires.Vars {
|
||||
varValue, _ := t.Vars.Get(requiredVar.Name)
|
||||
|
||||
enumValues := getEnumValues(requiredVar.Enum)
|
||||
value, isString := varValue.Value.(string)
|
||||
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
|
||||
if isString && len(enumValues) > 0 && !slices.Contains(enumValues, value) {
|
||||
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
|
||||
Value: value,
|
||||
Enum: requiredVar.Enum,
|
||||
Enum: enumValues,
|
||||
Name: requiredVar.Name,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(notAllowedValuesVars) > 0 {
|
||||
@@ -62,3 +207,10 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnumValues(e *ast.Enum) []string {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Value
|
||||
}
|
||||
|
||||
49
setup.go
49
setup.go
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/fsext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/version"
|
||||
@@ -55,18 +55,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)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errors.TaskfileNotFoundError{
|
||||
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
|
||||
Walk: true,
|
||||
AskInit: true,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -86,6 +89,9 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
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),
|
||||
)
|
||||
@@ -153,16 +159,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +203,27 @@ func (e *Executor) setupOutput() error {
|
||||
if !e.OutputStyle.IsSet() {
|
||||
e.OutputStyle = e.Taskfile.Output
|
||||
}
|
||||
if !e.OutputStyle.IsSet() && e.OutputCIAuto {
|
||||
if name := detectCIOutput(); name != "" {
|
||||
e.OutputStyle.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
|
||||
return err
|
||||
}
|
||||
|
||||
// detectCIOutput returns the name of a CI-aware output style to use based
|
||||
// on environment variables set by common CI runners. Returns an empty string
|
||||
// when no supported CI environment is detected.
|
||||
func detectCIOutput() string {
|
||||
if isGitLab, _ := strconv.ParseBool(os.Getenv("GITLAB_CI")); isGitLab {
|
||||
return "gitlab"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *Executor) setupCompiler() error {
|
||||
if e.UserWorkingDir == "" {
|
||||
var err error
|
||||
|
||||
97
setup_test.go
Normal file
97
setup_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func TestDetectCIOutput(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
want string
|
||||
}{
|
||||
{name: "no CI detected", env: nil, want: ""},
|
||||
{name: "GITLAB_CI=true", env: map[string]string{"GITLAB_CI": "true"}, want: "gitlab"},
|
||||
{name: "GITLAB_CI=1", env: map[string]string{"GITLAB_CI": "1"}, want: "gitlab"},
|
||||
{name: "GITLAB_CI=false", env: map[string]string{"GITLAB_CI": "false"}, want: ""},
|
||||
{name: "GITLAB_CI empty", env: map[string]string{"GITLAB_CI": ""}, want: ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("GITLAB_CI", "") // reset
|
||||
for k, v := range tc.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
assert.Equal(t, tc.want, detectCIOutput())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupOutputPriority(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cliStyle ast.Output
|
||||
taskfileStyle ast.Output
|
||||
ciAuto bool
|
||||
gitlabEnv string
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
name: "CLI wins over everything",
|
||||
cliStyle: ast.Output{Name: "prefixed"},
|
||||
taskfileStyle: ast.Output{Name: "group", Group: ast.OutputGroup{
|
||||
Begin: "b", End: "e",
|
||||
}},
|
||||
ciAuto: true,
|
||||
gitlabEnv: "true",
|
||||
wantName: "prefixed",
|
||||
},
|
||||
{
|
||||
name: "Taskfile wins over auto-detect",
|
||||
taskfileStyle: ast.Output{Name: "prefixed"},
|
||||
ciAuto: true,
|
||||
gitlabEnv: "true",
|
||||
wantName: "prefixed",
|
||||
},
|
||||
{
|
||||
name: "auto-detect activates when nothing explicit",
|
||||
ciAuto: true,
|
||||
gitlabEnv: "true",
|
||||
wantName: "gitlab",
|
||||
},
|
||||
{
|
||||
name: "auto-detect disabled does nothing",
|
||||
ciAuto: false,
|
||||
gitlabEnv: "true",
|
||||
wantName: "",
|
||||
},
|
||||
{
|
||||
name: "auto-detect without CI env does nothing",
|
||||
ciAuto: true,
|
||||
gitlabEnv: "",
|
||||
wantName: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("GITLAB_CI", tc.gitlabEnv)
|
||||
|
||||
e := &Executor{
|
||||
OutputStyle: tc.cliStyle,
|
||||
OutputCIAuto: tc.ciAuto,
|
||||
Taskfile: &ast.Taskfile{Output: tc.taskfileStyle},
|
||||
Logger: &logger.Logger{},
|
||||
}
|
||||
require.NoError(t, e.setupOutput())
|
||||
assert.Equal(t, tc.wantName, e.OutputStyle.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ const maxInterruptSignals = 3
|
||||
// time to do cleanup work.
|
||||
func (e *Executor) InterceptInterruptSignals() {
|
||||
ch := make(chan os.Signal, maxInterruptSignals)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
|
||||
go func() {
|
||||
for i := range maxInterruptSignals {
|
||||
|
||||
175
task.go
175
task.go
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -73,6 +74,11 @@ 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
|
||||
@@ -120,6 +126,19 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
|
||||
|
||||
// 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
|
||||
@@ -129,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)
|
||||
@@ -138,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
|
||||
}
|
||||
@@ -152,9 +204,9 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
release := e.acquireConcurrencyLimit()
|
||||
defer release()
|
||||
|
||||
if err = e.startExecution(ctx, t, func(ctx context.Context) error {
|
||||
if err = e.startExecution(ctx, t, func(ctx context.Context) (err error) {
|
||||
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
|
||||
if err := e.runDeps(ctx, t); err != nil {
|
||||
if err = e.runDeps(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -185,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,6 +266,9 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
|
||||
var deferredExitCode uint8
|
||||
|
||||
ctx, taskOutCloser := e.wrapTaskOutput(ctx, t, call)
|
||||
defer func() { taskOutCloser(err) }()
|
||||
|
||||
for i := range t.Cmds {
|
||||
if t.Cmds[i].Defer {
|
||||
defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode)
|
||||
@@ -295,6 +354,7 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
|
||||
|
||||
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 {
|
||||
@@ -305,24 +365,38 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
|
||||
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) {
|
||||
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
|
||||
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
|
||||
e.printCmdAnnouncement(ctx, t, cmd.Cmd)
|
||||
}
|
||||
|
||||
if e.Dry {
|
||||
@@ -338,7 +412,8 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
|
||||
if err != nil {
|
||||
return fmt.Errorf("task: failed to get variables: %w", err)
|
||||
}
|
||||
stdOut, stdErr, closer := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
|
||||
taskStdOut, taskStdErr := e.writersFromCtx(ctx)
|
||||
stdOut, stdErr, closer := outputWrapper.WrapWriter(taskStdOut, taskStdErr, t.Prefix, outputTemplater)
|
||||
|
||||
err = execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: cmd.Cmd,
|
||||
@@ -370,7 +445,7 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
|
||||
return err
|
||||
}
|
||||
|
||||
if h == "" {
|
||||
if h == "" || t.Watch {
|
||||
return execute(ctx)
|
||||
}
|
||||
|
||||
@@ -398,19 +473,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 {
|
||||
@@ -420,7 +516,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.
|
||||
@@ -428,7 +524,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()
|
||||
@@ -437,37 +537,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 := ""
|
||||
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,
|
||||
didYouMean := ""
|
||||
if !e.DisableFuzzy {
|
||||
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
|
||||
if e.fuzzyModel != nil {
|
||||
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
|
||||
}
|
||||
}
|
||||
return matchingTask, nil
|
||||
return nil, &errors.TaskNotFoundError{
|
||||
TaskName: call.Task,
|
||||
DidYouMean: didYouMean,
|
||||
}
|
||||
}
|
||||
|
||||
type FilterFunc func(task *ast.Task) bool
|
||||
|
||||
70
task_output.go
Normal file
70
task_output.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
type taskWritersKey struct{}
|
||||
|
||||
type taskWriters struct {
|
||||
stdout, stderr io.Writer
|
||||
}
|
||||
|
||||
// writersFromCtx returns the task-scoped writers if set, otherwise the
|
||||
// Executor's own stdout/stderr.
|
||||
func (e *Executor) writersFromCtx(ctx context.Context) (io.Writer, io.Writer) {
|
||||
if tw, ok := ctx.Value(taskWritersKey{}).(*taskWriters); ok && tw != nil {
|
||||
return tw.stdout, tw.stderr
|
||||
}
|
||||
return e.Stdout, e.Stderr
|
||||
}
|
||||
|
||||
// wrapTaskOutput wraps a task's output in a task-scoped block if e.Output
|
||||
// implements [output.TaskWrapper] and the task is not interactive. Returns
|
||||
// the (possibly updated) ctx and a closer that flushes the block. The closer
|
||||
// is always safe to call — it is a no-op when no wrapping took place.
|
||||
func (e *Executor) wrapTaskOutput(ctx context.Context, t *ast.Task, call *Call) (context.Context, func(error)) {
|
||||
noop := func(error) {}
|
||||
if t.Interactive {
|
||||
return ctx, noop
|
||||
}
|
||||
tw, ok := e.Output.(output.TaskWrapper)
|
||||
if !ok {
|
||||
return ctx, noop
|
||||
}
|
||||
stdOut, stdErr := e.writersFromCtx(ctx)
|
||||
vars, err := e.Compiler.FastGetVariables(t, call)
|
||||
if err != nil {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: output setup: %v\n", err)
|
||||
return ctx, noop
|
||||
}
|
||||
wOut, wErr, closer := tw.WrapTask(stdOut, stdErr, &templater.Cache{Vars: vars})
|
||||
ctx = context.WithValue(ctx, taskWritersKey{}, &taskWriters{stdout: wOut, stderr: wErr})
|
||||
return ctx, func(loopErr error) {
|
||||
if err := closer(loopErr); err != nil {
|
||||
e.Logger.Errf(logger.Red, "task: output close: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// printCmdAnnouncement prints the "task: [NAME] CMD" line using the
|
||||
// task-scoped stderr if available, so the announcement ends up inside the
|
||||
// task's output block.
|
||||
func (e *Executor) printCmdAnnouncement(ctx context.Context, t *ast.Task, cmdStr string) {
|
||||
_, stdErr := e.writersFromCtx(ctx)
|
||||
if stdErr == e.Stderr {
|
||||
// No task-scoped writer — fall back to the Logger to preserve existing
|
||||
// behavior (respects Logger's color config, etc.).
|
||||
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmdStr)
|
||||
return
|
||||
}
|
||||
_, _ = color.New(color.FgGreen).Fprintf(stdErr, "task: [%s] %s\n", t.Name(), cmdStr)
|
||||
}
|
||||
234
task_test.go
234
task_test.go
@@ -88,13 +88,14 @@ func (tt *TaskTest) writeFixture(
|
||||
if tt.fixtureTemplatingEnabled {
|
||||
fixtureTemplateData := map[string]any{
|
||||
"TEST_NAME": t.Name(),
|
||||
"TEST_DIR": wd,
|
||||
"TEST_DIR": filepath.ToSlash(wd),
|
||||
}
|
||||
// If the test has additional template data, copy it into the map
|
||||
if tt.fixtureTemplateData != nil {
|
||||
maps.Copy(fixtureTemplateData, tt.fixtureTemplateData)
|
||||
}
|
||||
g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, b)
|
||||
// Normalize output before comparison (CRLF→LF, backslash→forward slash)
|
||||
g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, normalizeOutput(b))
|
||||
} else {
|
||||
g.Assert(t, goldenFileName, b)
|
||||
}
|
||||
@@ -308,6 +309,73 @@ func PPSortedLines(t *testing.T, b []byte) []byte {
|
||||
return []byte(strings.Join(lines, "\n") + "\n")
|
||||
}
|
||||
|
||||
// normalizeOutput normalizes cross-platform differences for byte slice comparison:
|
||||
// - Converts CRLF and CR to LF (line endings)
|
||||
// - Converts backslashes to forward slashes (Windows paths)
|
||||
// - Handles escaped backslashes in JSON (\\) by converting to single forward slash
|
||||
func normalizeOutput(b []byte) []byte {
|
||||
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
|
||||
b = bytes.ReplaceAll(b, []byte("\r"), []byte("\n"))
|
||||
// First replace escaped backslashes (common in JSON), then single backslashes
|
||||
b = bytes.ReplaceAll(b, []byte("\\\\"), []byte("/"))
|
||||
b = bytes.ReplaceAll(b, []byte("\\"), []byte("/"))
|
||||
return b
|
||||
}
|
||||
|
||||
// normalizePathSeparators converts backslashes to forward slashes for cross-platform path comparison.
|
||||
func normalizePathSeparators(s string) string {
|
||||
return strings.ReplaceAll(s, "\\", "/")
|
||||
}
|
||||
|
||||
// NormalizedEqual compares two byte slices after normalizing output.
|
||||
// This is used as a custom goldie.EqualFn for cross-platform golden file tests.
|
||||
func NormalizedEqual(actual, expected []byte) bool {
|
||||
return bytes.Equal(normalizeOutput(actual), normalizeOutput(expected))
|
||||
}
|
||||
|
||||
func TestNormalizeOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{"CRLF to LF", []byte("line1\r\nline2\r\n"), []byte("line1\nline2\n")},
|
||||
{"CR to LF", []byte("line1\rline2\r"), []byte("line1\nline2\n")},
|
||||
{"Windows path", []byte(`D:\a\task\task`), []byte(`D:/a/task/task`)},
|
||||
{"JSON escaped backslash", []byte(`{"path":"D:\\a\\task"}`), []byte(`{"path":"D:/a/task"}`)},
|
||||
{"Mixed", []byte("D:\\a\\task\r\n"), []byte("D:/a/task\n")},
|
||||
{"Unix path unchanged", []byte("/home/user/task\n"), []byte("/home/user/task\n")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := normalizeOutput(tt.input)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePathSeparators(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"Windows path", `D:\a\task\task`, `D:/a/task/task`},
|
||||
{"Unix path unchanged", `/home/user/task`, `/home/user/task`},
|
||||
{"Mixed separators", `C:\Users/name\file`, `C:/Users/name/file`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := normalizePathSeparators(tt.input)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SyncBuffer is a threadsafe buffer for testing.
|
||||
// Some times replace stdout/stderr with a buffer to capture output.
|
||||
// stdout and stderr are threadsafe, but a regular bytes.Buffer is not.
|
||||
@@ -487,6 +555,104 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatusTimestamp is a regression test for https://github.com/go-task/task/issues/1230.
|
||||
// When using method: timestamp, deleting a generated file should cause the task to re-run,
|
||||
// not be skipped because the timestamp file is still present.
|
||||
func TestStatusTimestamp(t *testing.T) { // nolint:paralleltest // cannot run in parallel
|
||||
const dir = "testdata/timestamp"
|
||||
|
||||
generatedFile := filepathext.SmartJoin(dir, "generated.txt")
|
||||
tempDir := task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
}
|
||||
|
||||
// Clean up any state from previous runs.
|
||||
_ = os.Remove(generatedFile)
|
||||
_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithTempDir(tempDir),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
// First run: task should execute and create generated.txt.
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
_, err := os.Stat(generatedFile)
|
||||
require.NoError(t, err, "generated.txt should exist after first run")
|
||||
buff.Reset()
|
||||
|
||||
// Second run: task should be up to date.
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
|
||||
buff.Reset()
|
||||
|
||||
// Delete the generated file (simulate a clean), but leave the timestamp file.
|
||||
require.NoError(t, os.Remove(generatedFile))
|
||||
_, err = os.Stat(generatedFile)
|
||||
require.Error(t, err, "generated.txt should be gone")
|
||||
|
||||
// Third run: task MUST re-run because generated.txt is missing.
|
||||
// This is the regression: previously the task was incorrectly skipped.
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing")
|
||||
_, err = os.Stat(generatedFile)
|
||||
require.NoError(t, err, "generated.txt should be recreated after third run")
|
||||
}
|
||||
|
||||
// TestStatusChecksumMissingGenerated is a regression test for https://github.com/go-task/task/issues/1230.
|
||||
// When using method: checksum, deleting a generated file should cause the task to re-run,
|
||||
// not be skipped because the checksum file still matches.
|
||||
func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest // cannot run in parallel
|
||||
const dir = "testdata/checksum"
|
||||
|
||||
generatedFile := filepathext.SmartJoin(dir, "generated.txt")
|
||||
tempDir := task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
}
|
||||
|
||||
// Clean up any state from previous runs.
|
||||
_ = os.Remove(generatedFile)
|
||||
_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithTempDir(tempDir),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
// First run: task should execute and create generated.txt.
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
_, err := os.Stat(generatedFile)
|
||||
require.NoError(t, err, "generated.txt should exist after first run")
|
||||
buff.Reset()
|
||||
|
||||
// Second run: task should be up to date.
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
|
||||
buff.Reset()
|
||||
|
||||
// Delete the generated file (simulate a clean), but leave the checksum file.
|
||||
require.NoError(t, os.Remove(generatedFile))
|
||||
_, err = os.Stat(generatedFile)
|
||||
require.Error(t, err, "generated.txt should be gone")
|
||||
|
||||
// Third run: task MUST re-run because generated.txt is missing.
|
||||
// This is the regression: previously the task was incorrectly skipped.
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing")
|
||||
_, err = os.Stat(generatedFile)
|
||||
require.NoError(t, err, "generated.txt should be recreated after third run")
|
||||
}
|
||||
|
||||
func TestStatusVariables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -856,7 +1022,7 @@ func TestIncludesRemote(t *testing.T) {
|
||||
|
||||
for k, taskCall := range taskCalls {
|
||||
t.Run(taskCall.Task, func(t *testing.T) {
|
||||
expectedContent := fmt.Sprint(rand.Int64())
|
||||
expectedContent := fmt.Sprint(rand.Int64()) //nolint:gosec
|
||||
t.Setenv("CONTENT", expectedContent)
|
||||
|
||||
outputFile := fmt.Sprintf("%d.%d.txt", i, k)
|
||||
@@ -1078,7 +1244,7 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
|
||||
expected := fmt.Sprintf(message, wd, dir)
|
||||
expected := fmt.Sprintf(message, filepath.ToSlash(wd), dir)
|
||||
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
@@ -1098,7 +1264,7 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
|
||||
expected := fmt.Sprintf(message, wd, dir)
|
||||
expected := fmt.Sprintf(message, filepath.ToSlash(wd), dir)
|
||||
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
@@ -1146,11 +1312,11 @@ func TestIncludesRelativePath(t *testing.T) {
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "common:pwd"}))
|
||||
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
|
||||
assert.Contains(t, filepath.ToSlash(buff.String()), "testdata/includes_rel_path/common")
|
||||
|
||||
buff.Reset()
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:common:pwd"}))
|
||||
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
|
||||
assert.Contains(t, filepath.ToSlash(buff.String()), "testdata/includes_rel_path/common")
|
||||
}
|
||||
|
||||
func TestIncludesInternal(t *testing.T) {
|
||||
@@ -1328,7 +1494,7 @@ func TestIncludedTaskfileVarMerging(t *testing.T) {
|
||||
|
||||
err := e.Run(t.Context(), &task.Call{Task: test.task})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, buff.String(), test.expectedOutput)
|
||||
assert.Contains(t, filepath.ToSlash(buff.String()), test.expectedOutput)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1475,7 +1641,9 @@ func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"}))
|
||||
|
||||
// got should be the "dir" part of "testdata/dir"
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
||||
normalized := normalizePathSeparators(out.String())
|
||||
got := strings.TrimSuffix(filepath.Base(normalized), "\n")
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
}
|
||||
|
||||
@@ -1494,7 +1662,9 @@ func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"}))
|
||||
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
||||
normalized := normalizePathSeparators(out.String())
|
||||
got := strings.TrimSuffix(filepath.Base(normalized), "\n")
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
}
|
||||
|
||||
@@ -1520,7 +1690,9 @@ func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))
|
||||
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
||||
normalized := normalizePathSeparators(out.String())
|
||||
got := strings.TrimSuffix(filepath.Base(normalized), "\n")
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
|
||||
// Clean-up after ourselves only if no error.
|
||||
@@ -1549,7 +1721,11 @@ func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) {
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))
|
||||
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
||||
// Take only the first line as Windows may output additional debug info
|
||||
normalized := normalizePathSeparators(out.String())
|
||||
firstLine := strings.Split(normalized, "\n")[0]
|
||||
got := filepath.Base(firstLine)
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
|
||||
// Clean-up after ourselves only if no error.
|
||||
@@ -2268,7 +2444,8 @@ func TestUserWorkingDirectory(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
|
||||
// Use filepath.ToSlash because USER_WORKING_DIR uses forward slashes on all platforms
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", filepath.ToSlash(wd)), buff.String())
|
||||
}
|
||||
|
||||
func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
|
||||
@@ -2277,7 +2454,7 @@ func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
|
||||
wd = filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir")
|
||||
wd = filepath.ToSlash(filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir"))
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.NewExecutor(
|
||||
@@ -2290,7 +2467,8 @@ func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:echo"}))
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
|
||||
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", wd), normalizePathSeparators(buff.String()))
|
||||
}
|
||||
|
||||
func TestPlatforms(t *testing.T) {
|
||||
@@ -2423,6 +2601,27 @@ func TestSplitArgs(t *testing.T) {
|
||||
assert.Equal(t, "3\n", buff.String())
|
||||
}
|
||||
|
||||
func TestAbsPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.NewExecutor(
|
||||
task.WithDir("testdata/abs_path"),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithSilent(true),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(t.Context(), &task.Call{Task: "default"})
|
||||
require.NoError(t, err)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
expected := filepath.Join(cwd, "bar") + "\n"
|
||||
assert.Equal(t, expected, buff.String())
|
||||
}
|
||||
|
||||
func TestSingleCmdDep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2612,6 +2811,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 (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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 (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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 (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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 (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
@@ -12,6 +12,8 @@ type Output struct {
|
||||
Name string `yaml:"-"`
|
||||
// Group specific style
|
||||
Group OutputGroup
|
||||
// GitLab specific style
|
||||
GitLab OutputGitLab
|
||||
}
|
||||
|
||||
// IsSet returns true if and only if a custom output style is set.
|
||||
@@ -32,19 +34,30 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
|
||||
|
||||
case yaml.MappingNode:
|
||||
var tmp struct {
|
||||
Group *OutputGroup
|
||||
Group *OutputGroup
|
||||
GitLab *OutputGitLab `yaml:"gitlab"`
|
||||
}
|
||||
if err := node.Decode(&tmp); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if tmp.Group == nil {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" key when in mapping form`)
|
||||
switch {
|
||||
case tmp.Group != nil && tmp.GitLab != nil:
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style cannot set both "group" and "gitlab"`)
|
||||
case tmp.Group != nil:
|
||||
*s = Output{
|
||||
Name: "group",
|
||||
Group: *tmp.Group,
|
||||
}
|
||||
return nil
|
||||
case tmp.GitLab != nil:
|
||||
*s = Output{
|
||||
Name: "gitlab",
|
||||
GitLab: *tmp.GitLab,
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" or "gitlab" key when in mapping form`)
|
||||
}
|
||||
*s = Output{
|
||||
Name: "group",
|
||||
Group: *tmp.Group,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("output")
|
||||
@@ -63,3 +76,9 @@ func (g *OutputGroup) IsSet() bool {
|
||||
}
|
||||
return g.Begin != "" || g.End != ""
|
||||
}
|
||||
|
||||
// OutputGitLab is the style options specific to the GitLab style.
|
||||
type OutputGitLab struct {
|
||||
Collapsed bool
|
||||
ErrorOnly bool `yaml:"error_only"`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
@@ -22,9 +22,56 @@ func (r *Requires) DeepCopy() *Requires {
|
||||
}
|
||||
}
|
||||
|
||||
// Enum represents an enum constraint for a required variable.
|
||||
// It can either be a static list of values or a reference to another variable.
|
||||
type Enum struct {
|
||||
Ref string
|
||||
Value []string
|
||||
}
|
||||
|
||||
func (e *Enum) DeepCopy() *Enum {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &Enum{
|
||||
Ref: e.Ref,
|
||||
Value: deepcopy.Slice(e.Value),
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (e *Enum) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.SequenceNode:
|
||||
// Static list of values: enum: ["a", "b"]
|
||||
var values []string
|
||||
if err := node.Decode(&values); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
e.Value = values
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
// Reference to another variable: enum: { ref: .VAR }
|
||||
var refStruct struct {
|
||||
Ref string
|
||||
}
|
||||
if err := node.Decode(&refStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if refStruct.Ref == "" {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("enum")
|
||||
}
|
||||
e.Ref = refStruct.Ref
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("enum")
|
||||
}
|
||||
|
||||
type VarsWithValidation struct {
|
||||
Name string
|
||||
Enum []string
|
||||
Enum *Enum
|
||||
}
|
||||
|
||||
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
||||
@@ -33,7 +80,7 @@ func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
||||
}
|
||||
return &VarsWithValidation{
|
||||
Name: v.Name,
|
||||
Enum: v.Enum,
|
||||
Enum: v.Enum.DeepCopy(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +100,7 @@ func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.MappingNode:
|
||||
var vv struct {
|
||||
Name string
|
||||
Enum []string
|
||||
Enum *Enum
|
||||
}
|
||||
if err := node.Decode(&vv); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
@@ -32,7 +32,7 @@ type Task struct {
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
Silent bool
|
||||
Silent *bool
|
||||
Interactive bool
|
||||
Internal bool
|
||||
Method string
|
||||
@@ -40,6 +40,7 @@ type Task struct {
|
||||
IgnoreError bool
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
Watch bool
|
||||
Location *Location
|
||||
Failfast bool
|
||||
@@ -68,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 {
|
||||
@@ -134,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
|
||||
@@ -142,6 +152,7 @@ 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
|
||||
@@ -173,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
|
||||
@@ -181,6 +192,7 @@ 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
|
||||
@@ -215,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,
|
||||
@@ -225,10 +237,12 @@ 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"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"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"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/elliotchance/orderedmap/v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
@@ -118,7 +118,7 @@ func (vars *Vars) ToCacheMap() (m map[string]any) {
|
||||
|
||||
// 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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error)
|
||||
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -103,8 +192,8 @@ func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
dir, _ := filepath.Split(node.path)
|
||||
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
|
||||
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
|
||||
}
|
||||
@@ -138,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) {
|
||||
|
||||
@@ -102,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, //nolint:gosec
|
||||
}
|
||||
|
||||
// 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,7 +106,7 @@ 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
|
||||
}
|
||||
@@ -58,7 +115,7 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dominikbraun/graph"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
@@ -47,6 +47,9 @@ type (
|
||||
trustedHosts []string
|
||||
tempDir string
|
||||
cacheExpiryDuration time.Duration
|
||||
caCert string
|
||||
cert string
|
||||
certKey string
|
||||
debugFunc DebugFunc
|
||||
promptFunc PromptFunc
|
||||
promptMutex sync.Mutex
|
||||
@@ -199,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
|
||||
}
|
||||
|
||||
@@ -308,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 {
|
||||
|
||||
@@ -38,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 {
|
||||
@@ -46,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())
|
||||
@@ -78,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()}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,12 @@ import (
|
||||
type TaskRC struct {
|
||||
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"`
|
||||
OutputCIAuto *bool `yaml:"output-ci-auto"`
|
||||
Remote Remote `yaml:"remote"`
|
||||
Failfast bool `yaml:"failfast"`
|
||||
Experiments map[string]int `yaml:"experiments"`
|
||||
@@ -24,7 +28,11 @@ type Remote struct {
|
||||
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.
|
||||
@@ -46,15 +54,22 @@ 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.OutputCIAuto = cmp.Or(other.OutputCIAuto, t.OutputCIAuto)
|
||||
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package taskrc
|
||||
import (
|
||||
"os"
|
||||
|
||||
"go.yaml.in/yaml/v4"
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/taskrc/ast"
|
||||
)
|
||||
@@ -65,7 +65,7 @@ func (r *Reader) Read(node *Node) (*ast.TaskRC, error) {
|
||||
}
|
||||
|
||||
// Read the file
|
||||
b, err := os.ReadFile(node.entrypoint)
|
||||
b, err := os.ReadFile(node.entrypoint) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -62,6 +63,9 @@ func GetConfig(dir string) (*ast.TaskRC, error) {
|
||||
return config, err
|
||||
}
|
||||
entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs)
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
@@ -306,4 +306,27 @@ remote:
|
||||
assert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry)
|
||||
assert.Equal(t, []string{"github.com", "gitlab.com"}, base.Remote.TrustedHosts)
|
||||
})
|
||||
|
||||
t.Run("output-ci-auto merge", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
|
||||
trueVal := true
|
||||
falseVal := false
|
||||
|
||||
t.Run("other overrides nil base", func(t *testing.T) { //nolint:paralleltest
|
||||
base := &ast.TaskRC{}
|
||||
base.Merge(&ast.TaskRC{OutputCIAuto: &trueVal})
|
||||
assert.Equal(t, &trueVal, base.OutputCIAuto)
|
||||
})
|
||||
|
||||
t.Run("other overrides base", func(t *testing.T) { //nolint:paralleltest
|
||||
base := &ast.TaskRC{OutputCIAuto: &falseVal}
|
||||
base.Merge(&ast.TaskRC{OutputCIAuto: &trueVal})
|
||||
assert.Equal(t, &trueVal, base.OutputCIAuto)
|
||||
})
|
||||
|
||||
t.Run("nil other does not override base", func(t *testing.T) { //nolint:paralleltest
|
||||
base := &ast.TaskRC{OutputCIAuto: &trueVal}
|
||||
base.Merge(&ast.TaskRC{})
|
||||
assert.Equal(t, &trueVal, base.OutputCIAuto)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
6
testdata/abs_path/Taskfile.yml
vendored
Normal file
6
testdata/abs_path/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- cmd: echo '{{absPath "foo/../bar"}}'
|
||||
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