Compare commits

..

5 Commits

Author SHA1 Message Date
Pete Davison
5128a98ee2 feat: test wildcards 2025-08-11 12:00:34 +00:00
Pete Davison
8353dffc7a fix: executor and formatter tests 2025-08-11 12:00:34 +00:00
Pete Davison
6e80b401e6 feat: remove entrypoint from the executor 2025-08-11 12:00:34 +00:00
Pete Davison
1402e2baaf refactor: merge Setup into NewExecutor 2025-08-11 12:00:34 +00:00
Pete Davison
4b99f60039 refactor: split executor and reader 2025-08-11 12:00:34 +00:00
370 changed files with 22788 additions and 27857 deletions

View File

@@ -8,6 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_style = tab indent_style = tab
[*.{md,mdx,yml,yaml,json,toml,htm,html,js,ts,vue,css,svg,sh,bash,fish}] [*.{md,mdx,yml,yaml,json,toml,htm,html,js,ts,css,svg,sh,bash,fish}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

3
.gitattributes vendored
View File

@@ -1,5 +1,2 @@
* text=auto * text=auto
*.mdx -linguist-detectable *.mdx -linguist-detectable
# Keep LF line endings in testdata for consistent checksums across platforms
testdata/** text eol=lf

View File

@@ -7,5 +7,3 @@ Please understand that it may take some time to be reviewed.
Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/). Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
--> -->
- [ ] I have read and followed the [Contribution Guide](https://taskfile.dev/contributing/)

11
.github/renovate.json vendored
View File

@@ -8,17 +8,6 @@
], ],
"mode": "full", "mode": "full",
"addLabels":["area: dependencies"], "addLabels":["area: dependencies"],
"customManagers": [
{
"customType": "regex",
"fileMatch": ["^\\.github/workflows/.*\\.ya?ml$"],
"matchStrings": [
"uses:\\s*golangci/golangci-lint-action@\\S+\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
],
"datasourceTemplate": "github-releases",
"depNameTemplate": "golangci/golangci-lint"
}
],
"packageRules": [ "packageRules": [
{ {
"matchManagers": ["github-actions"], "matchManagers": ["github-actions"],

View File

@@ -4,16 +4,13 @@ on:
issue_comment: issue_comment:
types: [created] types: [created]
permissions:
issues: write
jobs: jobs:
issue-awaiting-response: issue-awaiting-response:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
const issue = await github.rest.issues.get({ const issue = await github.rest.issues.get({
owner: context.repo.owner, owner: context.repo.owner,

View File

@@ -4,16 +4,13 @@ on:
issues: issues:
types: [closed] types: [closed]
permissions:
issues: write
jobs: jobs:
issue-closed: issue-closed:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
const labels = await github.paginate( const labels = await github.paginate(
github.rest.issues.listLabelsOnIssue, { github.rest.issues.listLabelsOnIssue, {

View File

@@ -2,19 +2,16 @@ name: issue experiment
on: on:
issues: issues:
types: [field_added] types: [labeled]
permissions:
issues: write
jobs: jobs:
issue-experiment-proposal: issue-experiment-proposed:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'proposal' if: github.event.label.name == format('status{0} proposed', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
@@ -23,12 +20,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.' 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: issue-experiment-draft:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'draft' if: github.event.label.name == format('status{0} draft', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
@@ -37,12 +34,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.' 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: issue-experiment-candidate:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'candidate' if: github.event.label.name == format('status{0} candidate', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
@@ -51,12 +48,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.' 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: issue-experiment-stable:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'stable' if: github.event.label.name == format('status{0} stable', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
@@ -65,12 +62,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.' 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: issue-experiment-released:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'released' if: github.event.label.name == format('status{0} released', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
@@ -85,12 +82,12 @@ jobs:
state: 'closed' state: 'closed'
}) })
issue-experiment-abandoned: issue-experiment-abandoned:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'abandoned' if: github.event.label.name == format('status{0} abandoned', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,
@@ -105,12 +102,12 @@ jobs:
state: 'closed' state: 'closed'
}) })
issue-experiment-superseded: issue-experiment-superseded:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'superseded' if: github.event.label.name == format('status{0} superseded', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
github.rest.issues.createComment({ github.rest.issues.createComment({
issue_number: context.issue.number, issue_number: context.issue.number,

View File

@@ -4,16 +4,13 @@ on:
issues: issues:
types: [opened] types: [opened]
permissions:
issues: write
jobs: jobs:
issue-needs-triage: issue-needs-triage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GITHUB_TOKEN}} github-token: ${{secrets.GH_PAT}}
script: | script: |
const labels = await github.paginate( const labels = await github.paginate(
github.rest.issues.listLabelsOnIssue, { github.rest.issues.listLabelsOnIssue, {

View File

@@ -8,39 +8,67 @@ on:
branches: branches:
- main - main
permissions:
contents: read
jobs: jobs:
lint: lint:
name: Lint name: Lint
strategy: strategy:
matrix: matrix:
go-version: [1.25.10, 1.26.x] go-version: [1.23.x, 1.24.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1 uses: golangci/golangci-lint-action@v8
with: with:
version: v2.12.2 version: v2.1.0
lint-jsonschema: lint-jsonschema:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@v5
with: with:
python-version: 3.14 python-version: 3.12
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@v4
- name: install check-jsonschema - name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3' run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema) - name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/src/public/schema.json run: check-jsonschema --check-metaschema website/static/schema.json
check_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v46
with:
files: website/versioned_docs/**
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.')
check_schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v46
with:
files: |
website/static/schema.json
website/static/schema-taskrc.json
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('schema.json or schema-taskrc.json has changed. Instead you need to update next-schema.json or next-schema-taskrc.json.')

View File

@@ -1,69 +0,0 @@
name: PR Build
on:
pull_request_target:
types: [labeled, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
build:
if: contains(github.event.pull_request.labels.*.name, 'needs-build')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.x"
cache: true
- uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
with:
version: "~> v2"
args: release --snapshot --clean --config .goreleaser-pr.yml
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_linux_amd64
path: dist/task_linux_amd64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_linux_arm64
path: dist/task_linux_arm64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_darwin_amd64
path: dist/task_darwin_amd64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_darwin_arm64
path: dist/task_darwin_arm64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_windows_amd64
path: dist/task_windows_amd64.zip
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: checksums
path: dist/task_checksums.txt
- uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
token: ${{secrets.GITHUB_TOKEN}}
issue-number: ${{ github.event.pull_request.number }}
body-includes: "📦 Build artifacts ready!"
- uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
token: ${{secrets.GITHUB_TOKEN}}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## 📦 Build artifacts ready!
Download binaries from [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
Available platforms: Linux, macOS, Windows (amd64, arm64)
edit-mode: replace

View File

@@ -1,34 +1,29 @@
name: Release nightly name: Realease nightly
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: 0 0 * * * - cron: 0 0 * * *
permissions:
contents: write
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@v5
with: with:
go-version: 1.26.x go-version: 1.24.x
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7 uses: goreleaser/goreleaser-action@v6
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: latest version: latest
args: release --clean --nightly -f .goreleaser-nightly.yml args: release --clean --nightly -f .goreleaser-nightly.yml
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}

View File

@@ -3,58 +3,28 @@ name: goreleaser
on: on:
push: push:
tags: tags:
- "v*" - 'v*'
permissions:
id-token: write # Required for OIDC
contents: write
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@v5
with: with:
go-version: 1.26.x go-version: 1.24.x
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
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@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2
- name: Install pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
package_json_file: "website/package.json"
run_install: "true"
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7 uses: goreleaser/goreleaser-action@v6
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: latest version: latest
args: release --clean --draft args: release --clean --draft
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GH_PAT}}
GH_GORELEASER_TOKEN: ${{secrets.GH_GORELEASER_TOKEN}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
- name: Deploy Website
shell: bash
run: |
task website:deploy:prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

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

View File

@@ -8,26 +8,31 @@ on:
branches: branches:
- main - main
permissions:
contents: read
jobs: jobs:
test: test:
name: Test name: Test
strategy: strategy:
fail-fast: false
matrix: matrix:
go-version: [1.25.10, 1.26.x] go-version: [1.23.x, 1.24.x]
platform: [ubuntu-latest, macos-latest, windows-latest] platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}} runs-on: ${{matrix.platform}}
steps: steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go ${{matrix.go-version}} - name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Download Go modules
run: go mod download
env:
GOPROXY: https://proxy.golang.org
- name: Build
run: go build -o ./bin/task -v ./cmd/task
- name: Test - name: Test
run: go run ./cmd/task test run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::'

1
.gitignore vendored
View File

@@ -35,4 +35,3 @@ tags
/testdata/vars/v1 /testdata/vars/v1
/tmp /tmp
node_modules node_modules
website/.netlify/

View File

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

View File

@@ -1,31 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
builds:
- binary: task
main: ./cmd/task
goos: [windows, darwin, linux]
goarch: [amd64, arm64]
env:
- CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}'
flags:
- -trimpath
ldflags:
- "-s -w"
archives:
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
files:
- README.md
- LICENSE
- completion/**/*
format_overrides:
- goos: windows
formats: [zip]
snapshot:
version_template: 'pr-{{ .ShortCommit }}'
checksum:
name_template: 'task_checksums.txt'

View File

@@ -22,8 +22,6 @@ builds:
goarch: '386' goarch: '386'
- goos: darwin - goos: darwin
goarch: riscv64 goarch: riscv64
- goos: windows
goarch: arm
- goos: windows - goos: windows
goarch: riscv64 goarch: riscv64
env: env:
@@ -62,7 +60,7 @@ nfpms:
- vendor: Task - vendor: Task
homepage: https://taskfile.dev homepage: https://taskfile.dev
maintainer: The Task authors <task@taskfile.dev> maintainer: The Task authors <task@taskfile.dev>
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. description: Simple task runner written in Go
section: golang section: golang
license: MIT license: MIT
conflicts: conflicts:
@@ -70,26 +68,24 @@ nfpms:
formats: formats:
- deb - deb
- rpm - rpm
- apk file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
file_name_template: '{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}'
contents: contents:
- src: completion/bash/task.bash - src: completion/bash/task.bash
dst: /etc/bash_completion.d/task dst: /etc/bash_completion.d/task
- src: completion/fish/task.fish - src: completion/fish/task.fish
dst: /usr/share/fish/vendor_completions.d/task.fish dst: /usr/share/fish/completions/task.fish
- src: completion/zsh/_task - src: completion/zsh/_task
dst: /usr/local/share/zsh/site-functions/_task dst: /usr/local/share/zsh/site-functions/_task
brews: brews:
- name: go-task - name: go-task
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. description: Task runner / simpler Make alternative written in Go
license: MIT license: MIT
homepage: https://taskfile.dev homepage: https://taskfile.dev
directory: Formula directory: Formula
repository: repository:
owner: go-task owner: go-task
name: homebrew-tap name: homebrew-tap
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
test: system "#{bin}/task", "--help" test: system "#{bin}/task", "--help"
install: |- install: |-
bin.install "task" bin.install "task"
@@ -103,8 +99,8 @@ brews:
winget: winget:
- name: Task - name: Task
publisher: Task publisher: Task
short_description: The modern task runner. short_description: A task runner / simpler Make alternative written in Go
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows. description: Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.
license: MIT license: MIT
homepage: https://taskfile.dev/ homepage: https://taskfile.dev/
publisher_url: https://taskfile.dev/ publisher_url: https://taskfile.dev/
@@ -130,8 +126,7 @@ winget:
repository: repository:
owner: go-task owner: go-task
name: winget-pkgs name: winget-pkgs
branch: 'task-{{.Version}}' branch: 'chore/task-{{.Version}}'
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
pull_request: pull_request:
enabled: true enabled: true
draft: false draft: false
@@ -140,37 +135,3 @@ winget:
owner: microsoft owner: microsoft
name: winget-pkgs name: winget-pkgs
branch: master branch: master
body: |
/cc @andreynering @pd93 @vmaerten
npms:
- name: "@go-task/cli"
repository: "git+https://github.com/go-task/task.git"
bugs: https://github.com/go-task/task/issues
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
homepage: https://taskfile.dev
license: MIT
author: "The Task authors"
access: public
keywords:
- "task"
- "taskfile"
- "build-tool"
- "task-runner"
cloudsmiths:
- organization: "task"
repository: "{{if not .IsNightly}}task{{end}}"
formats:
- deb
- rpm
- apk
distributions:
deb:
- "any-distro/any-version"
rpm:
- "any-distro/any-version"
alpine:
- "alpine/any-version"
component: main
republish: true

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
22.18.0

View File

@@ -1,12 +1,15 @@
{ {
"yaml.schemas": { "yaml.schemas": {
"./website/src/public/schema.json": [ "./website/static/schema.json": [
"Taskfile.yml", "Taskfile.yml",
"Taskfile.yaml", "tmp/**/*.yml"
"taskfile.yml",
"taskfile.yaml"
] ]
}, },
"search.exclude": {
"**/versioned_docs": true,
"**/versioned_sidesbars": true,
"**/i18n": true
},
"gopls": { "gopls": {
"formatting.local": "github.com/go-task" "formatting.local": "github.com/go-task"
}, },

View File

@@ -1,266 +1,5 @@
# Changelog # Changelog
## Unreleased
- Fixed Fish completions not being picked up correctly by installing them to
Fish's `vendor_completions.d` directory instead of `completions` (#2850, #2859
by @Legimity).
- PowerShell completions now work with aliases of the `task` command, not just
the `task` binary itself (#2852 by @kojiishi).
## v3.51.1 - 2026-05-16
- A significant performance boost was achieved for large Taskfiles (monorepos)
by skipping templating altogether when the string is static (#2820 by @romnn).
- Added `absPath` template function that resolves a path to its absolute form,
cleaning `..` and `.` components (#2681, #2788 by @mateenanjum).
- Added `joinEnv` function to join paths based on your oprating system: `;` for
Windows and `:` elsewhere, and `joinUrl` to join URL paths. Also, added two
new special variables: `FILE_PATH_SEPARATOR` which returns `\` on Windows
and `/` elsewhere, and `PATH_LIST_SEPARATOR` which returns `;` on Windows and
`:` elsewhere (#2406, #2408 by @solvingj).
- Update the shell interpreter with a regression fix (#2812, #2832 by
@andreynering).
- Fix potential panic with the shell interpreter (#2810 by @trulede).
## v3.50.0 - 2026-04-13
- Added `enum.ref` support in `requires`: enum constraints can now reference
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
`.taskrc.yml` or per task, or use the `--failfast` flag, which will also work
for `--parallel` (#1246, #2525 by @andreynering).
- The `--summary` flag now displays `vars:` (both global and task-level),
`env:`, and `requires:` sections. Dynamic variables show their shell command
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
@vmaerten).
- Improved performance of fuzzy task name matching by implementing lazy
initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option
to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).
- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`
and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).
- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to
skip confirmation prompts for specified hosts when using Remote Taskfiles
(#2491, #2473 by @maciejlech).
- When running in GitHub Actions, Task now automatically emits error annotations
on failure, improving visibility in workflow summaries (#2568 by @vmaerten).
- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`
variable (#2577, #2479 by @semihbkgr).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- Shell completion now works when Task is installed or aliased under a different
binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).
- Some small fixes and improvements were made to `task --init` and to the
default Taskfile it generates (#2433 by @andreynering).
- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to
customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).
- Zsh completion now supports zstyle verbose option to show or hide task
descriptions (#2571 by @vmaerten).
- Task now automatically enables colored output in CI environments (GitHub
Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by
@vmaerten).
- Added color taskrc option to explicitly enable or disable colored output
globally (#2569 by @vmaerten).
- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication
now works out of the box and `applyOf` is properly supported (#2512 by
@vmaerten).
### 🐛 Fixes
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or
`run: when_changed` settings (#2566, #1388 by @trulede).
- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in
root-level vars section (#2403, #2397 by @trulede, @vmaerten).
- Fixed a bug where `ignore_error` was ignored when using `task:` to call
another task (#2552, #363 by @trulede).
- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`
flag (#1574, #2574 by @vmaerten).
- Fixed Fish completion failing to parse task descriptions containing colons
(e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).
- Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when
using `for` loops in Taskfiles (#2576 by @vmaerten).
## v3.45.5 - 2025-11-11
- Fixed bug that made a generic message, instead of an useful one, appear when a
Taskfile could not be found (#2431 by @andreynering).
- Fixed a bug that caused an error when including a Remote Git Taskfile (#2438
by @twelvelabs).
- Fixed issue where `.taskrc.yml` was not returned if reading it failed, and
corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten).
- Improved performance of `--list` and `--list-all` by introducing a faster
compilation method that skips source globbing and checksum updates (#1322,
#2053 by @vmaerten).
- Fixed a concurrency bug with `output: group`. This ensures that begin/end
parts won't be mixed up from different tasks (#1208, #2349, #2350 by
@trulede).
- Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede).
- Improve error message when a Taskfile is not found (#2441, #2494 by
@vmaerten).
- Fixed generic error message `exit status 1` when a dependency task failed
(#2286 by @GrahamDennis).
- Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork
maintained by the official YAML org (#2171, #2434 by @andreynering).
- On Windows, the built-in version of the `rm` core utils contains a fix related
to the `-f` flag (#2426,
[u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464),
[mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199), #2506 by
@andreynering).
## v3.45.4 - 2025-09-17
- Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423
by @vmaerten).
- Fixed a bug where `.taskrc.yml` files in parent folders were not read
correctly (#2424 by @vmaerten).
- Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by
@vmaerten).
## v3.45.3 - 2025-09-15
- Task now includes built-in core utilities to greatly improve compatibility on
Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any
other common core utility will now work by default on Windows, without extra
setup. This is something we wanted to address for many many years, and it's
finally being shipped!
[Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils).
(#197, #2360 by @andreynering).
- :sparkles: Built and deployed a [brand new website](https://taskfile.dev)
using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by
@vmaerten, @andreynering, @pd93).
- Began releasing
[nightly builds](https://github.com/go-task/task/releases/tag/nightly). This
will allow people to test our changes before they are fully released and
without having to install Go to build them (#2358 by @vmaerten).
- Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or
`$HOME/.taskrc.yml`. Check out our new
[configuration guide](https://taskfile.dev/docs/reference/config) for more
details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93).
- Added experiments to the taskrc schema to clarify the expected keys and values
(#2235 by @vmaerten).
- Added support for new properties in `.taskrc.yml`: insecure, verbose,
concurrency, remote offline, remote timeout, and remote expiry. :warning:
Note: setting offline via environment variable is no longer supported. (#2389
by @vmaerten)
- Added a `--nested` flag when outputting tasks using `--list --json`. This will
output tasks in a nested structure when tasks are namespaced (#2415 by @pd93).
- Enhanced support for tasks with wildcards: they are now logged correctly, and
wildcard parameters are fully considered during fingerprinting (#1808, #1795
by @vmaerten).
- Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417
by @trulede).
#### Package API
- Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten).
#### Other news
We recently released our
[official GitHub Action](https://github.com/go-task/setup-task). This is based
on the fantastic work by the Arduino team who created and maintained the
community version. Now that this is officially adopted, fixes/updates should be
more timely. We have already merged a couple of longstanding PRs in our
[first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by
@pd93, @shrink, @trim21 and all the previous contributors to
[arduino/setup-task](https://github.com/arduino/setup-task/)).
## v3.45.0-v3.45.2 - 2025-09-15
Failed due to an issue with our release process.
## v3.44.1 - 2025-07-23 ## v3.44.1 - 2025-07-23
- Internal tasks will no longer be shown as suggestions since they cannot be - Internal tasks will no longer be shown as suggestions since they cannot be

View File

@@ -1,16 +1,16 @@
<div align="center"> <div align="center">
<a href="https://taskfile.dev"> <a href="https://taskfile.dev">
<img src="website/src/public/img/logo.svg" width="200px" height="200px" /> <img src="website/static/img/logo.svg" width="200px" height="200px" />
</a> </a>
<h1>Task: The Modern Task Runner</h1> <h1>Task</h1>
<p> <p>
A fast, cross-platform build tool inspired by Make, designed for modern workflows. 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>.
</p> </p>
<p> <p>
<a href="https://taskfile.dev/docs/installation">Installation</a> &bullet; <a href="https://taskfile.dev/docs/getting-started">Getting Started</a> &bullet; <a href="https://taskfile.dev/docs/guide">Docs</a> &bullet; <a href="https://twitter.com/taskfiledev">Twitter</a> &bullet; <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> &bullet; <a href="https://fosstodon.org/@task">Mastodon</a> &bullet; <a href="https://discord.gg/6TY36E39UK">Discord</a> <a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p> </p>
<h1>Gold Sponsors</h1> <h1>Gold Sponsors</h1>
@@ -19,29 +19,7 @@
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a target="_blank" href="https://devowl.io"> <a target="_blank" href="https://devowl.io">
<img src="website/src/public/img/devowl.io.svg" height="100px" width="200px" title="devowl.io" /> <img src="/website/static/img/devowl.io.svg" height="100px" title="devowl.io" />
</a>
</td>
<td align="center" valign="middle">
<a target="_blank" href="https://goodx.international/">
<img src="website/src/public/img/goodx.svg" height="80px" width="200px" title="GoodX" />
</a>
</td>
<td align="center" valign="middle">
<a target="_blank" href="https://magic.dev/">
<img src="website/src/public/img/magic.png" height="100px" width="200px" title="Magic" />
</a>
</td>
</tr>
</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> </a>
</td> </td>
</tr> </tr>

View File

@@ -8,7 +8,6 @@ includes:
vars: vars:
BIN: "{{.ROOT_DIR}}/bin" BIN: "{{.ROOT_DIR}}/bin"
GOTESTSUM_FORMAT: '{{if .CI}}github-actions{{else}}pkgname{{end}}'
env: env:
CGO_ENABLED: '0' CGO_ENABLED: '0'
@@ -29,7 +28,6 @@ tasks:
aliases: [i] aliases: [i]
sources: sources:
- './**/*.go' - './**/*.go'
- go.mod
cmds: cmds:
- go install -v ./cmd/task - go install -v ./cmd/task
@@ -133,37 +131,29 @@ tasks:
test: test:
desc: Runs test suite desc: Runs test suite
aliases: [t] aliases: [t]
deps: [gotestsum:install]
sources: sources:
- "**/*.go" - "**/*.go"
- "testdata/**/*" - "testdata/**/*"
cmds: cmds:
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... - go test ./...
test:watch: test:watch:
desc: Runs test suite with watch tests included desc: Runs test suite with watch tests included
deps: [sleepit:build, gotestsum:install] deps: [sleepit:build]
cmds: cmds:
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... -tags 'watch' - go test ./... -tags 'watch'
test:all: test:all:
desc: Runs test suite with signals and watch tests included desc: Runs test suite with signals and watch tests included
deps: [sleepit:build, gotestsum:install] deps: [sleepit:build]
cmds: cmds:
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./... - go test -tags 'signals watch' ./...
goreleaser:test: goreleaser:test:
desc: Tests release process without publishing desc: Tests release process without publishing
cmds: cmds:
- goreleaser --snapshot --clean - goreleaser --snapshot --clean
gotestsum:install:
desc: Installs gotestsum
status:
- command -v gotestsum
cmds:
- go install gotest.tools/gotestsum@latest
goreleaser:install: goreleaser:install:
desc: Installs goreleaser desc: Installs goreleaser
cmds: cmds:
@@ -213,6 +203,7 @@ tasks:
Please wait for the CI to finish and then do the following: Please wait for the CI to finish and then do the following:
- Copy the changelog for v{{.VERSION}} to the GitHub release - Copy the changelog for v{{.VERSION}} to the GitHub release
- Publish the package to NPM with `task npm:publish`
- Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml - Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml
preconditions: preconditions:
- sh: test $(git rev-parse --abbrev-ref HEAD) = "main" - sh: test $(git rev-parse --abbrev-ref HEAD) = "main"
@@ -227,7 +218,12 @@ tasks:
- "git add --all" - "git add --all"
- "git commit -m v{{.VERSION}}" - "git commit -m v{{.VERSION}}"
- "git push" - "git push"
- "git tag -a v{{.VERSION}} -m v{{.VERSION}}" - "git tag v{{.VERSION}}"
- "git push origin tag v{{.VERSION}}" - "git push origin tag v{{.VERSION}}"
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}' - cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
silent: true silent: true
npm:publish:
desc: Publish release to npm
cmds:
- npm publish --access=public

View File

@@ -3,23 +3,33 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/otiai10/copy"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )
const ( const (
changelogSource = "CHANGELOG.md" changelogSource = "CHANGELOG.md"
changelogTarget = "website/src/docs/changelog.md" changelogTarget = "website/docs/changelog.mdx"
versionFile = "internal/version/version.txt" docsSource = "website/docs"
docsTarget = "website/versioned_docs/version-latest"
schemaSource = "website/static/next-schema.json"
schemaTarget = "website/static/schema.json"
schemaTaskrcSource = "website/static/next-schema-taskrc.json"
schemaTaskrcTarget = "website/static/schema-taskrc.json"
) )
var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`) var (
changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`)
)
// Flags // Flags
var ( var (
@@ -43,7 +53,7 @@ func release() error {
return errors.New("error: expected version number") return errors.New("error: expected version number")
} }
version, err := getVersion(versionFile) version, err := getVersion()
if err != nil { if err != nil {
return err return err
} }
@@ -61,18 +71,36 @@ func release() error {
return err return err
} }
if err := setVersionFile(versionFile, version); err != nil { if err := setVersionFile("internal/version/version.txt", version); err != nil {
return err
}
if err := setJSONVersion("package.json", version); err != nil {
return err
}
if err := setJSONVersion("package-lock.json", version); err != nil {
return err
}
if err := docs(); err != nil {
return err
}
if err := schema(); err != nil {
return err return err
} }
return nil return nil
} }
func getVersion(filename string) (*semver.Version, error) { func getVersion() (*semver.Version, error) {
b, err := os.ReadFile(filename) cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
b, err := cmd.Output()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return semver.NewVersion(strings.TrimSpace(string(b))) return semver.NewVersion(strings.TrimSpace(string(b)))
} }
@@ -117,21 +145,51 @@ func changelog(version *semver.Version) error {
changelog = changelogReleaseRegex.ReplaceAllString(changelog, fmt.Sprintf("## v%s - %s", version, date)) changelog = changelogReleaseRegex.ReplaceAllString(changelog, fmt.Sprintf("## v%s - %s", version, date))
// Write the changelog to the source file // Write the changelog to the source file
if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil { //nolint:gosec if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil {
return err return err
} }
// Wrap the changelog content with v-pre directive for VitePress to prevent
// Vue from interpreting template syntax like {{.TASK_VERSION}}
changelogWithVPre := strings.Replace(changelog, "# Changelog\n\n", "# Changelog\n\n::: v-pre\n\n", 1) + "\n:::"
// Add the frontmatter to the changelog // Add the frontmatter to the changelog
changelogWithFrontmatter := fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelogWithVPre) changelog = fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelog)
// Write the changelog to the target file // Write the changelog to the target file
return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644) //nolint:gosec return os.WriteFile(changelogTarget, []byte(changelog), 0o644)
} }
func setVersionFile(fileName string, version *semver.Version) error { func setVersionFile(fileName string, version *semver.Version) error {
return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644) return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644)
} }
func setJSONVersion(fileName string, version *semver.Version) error {
// Read the JSON file
b, err := os.ReadFile(fileName)
if err != nil {
return err
}
// Replace the version
new := versionRegex.ReplaceAllString(string(b), fmt.Sprintf(` "version": "%s",`, version.String()))
// Write the JSON file
return os.WriteFile(fileName, []byte(new), 0o644)
}
func docs() error {
if err := os.RemoveAll(docsTarget); err != nil {
return err
}
if err := copy.Copy(docsSource, docsTarget); err != nil {
return err
}
return nil
}
func schema() error {
if err := copy.Copy(schemaSource, schemaTarget); err != nil {
return err
}
if err := copy.Copy(schemaTaskrcSource, schemaTaskrcTarget); err != nil {
return err
}
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -17,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/flags" "github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -29,34 +29,19 @@ func main() {
Color: flags.Color, Color: flags.Color,
} }
if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode { if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode {
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err) l.Errf(logger.Red, "%v\n", err)
os.Exit(err.TaskExitCode()) os.Exit(err.TaskExitCode())
} }
if err, ok := err.(errors.TaskError); ok { if err, ok := err.(errors.TaskError); ok {
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err) l.Errf(logger.Red, "%v\n", err)
os.Exit(err.Code()) os.Exit(err.Code())
} }
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err) l.Errf(logger.Red, "%v\n", err)
os.Exit(errors.CodeUnknown) os.Exit(errors.CodeUnknown)
} }
os.Exit(errors.CodeOk) 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 { func run() error {
log := &logger.Logger{ log := &logger.Logger{
Stdout: os.Stdout, Stdout: os.Stdout,
@@ -126,16 +111,63 @@ func run() error {
return nil return nil
} }
e := task.NewExecutor( if err := experiments.Validate(); err != nil {
flags.WithFlags(), log.Warnf("%s\n", err.Error())
task.WithVersionCheck(true), }
// Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
flags.Entrypoint,
flags.Dir,
flags.Insecure,
) )
if err := e.Setup(); err != nil { if err != nil {
return err return err
} }
tempDir, err := task.NewTempDir(node.Dir())
if err != nil {
return err
}
reader := taskfile.NewReader(
flags.WithFlags(),
taskfile.WithTempDir(tempDir.Remote),
taskfile.WithDebugFunc(func(s string) {
log.VerboseOutf(logger.Magenta, s)
}),
taskfile.WithPromptFunc(func(s string) error {
return log.Prompt(logger.Yellow, s, "n", "y", "yes")
}),
)
ctx, cf := context.WithTimeout(context.Background(), flags.Timeout)
defer cf()
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: flags.Timeout}
}
return err
}
executor, err := task.NewExecutor(graph,
flags.WithFlags(),
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
)
if err != nil {
return err
}
// If the download flag is specified, we should stop execution as soon as
// taskfile is downloaded
if flags.Download {
return nil
}
if flags.ClearCache { if flags.ClearCache {
cachePath := filepath.Join(e.TempDir.Remote, "remote") cachePath := filepath.Join(executor.TempDir.Remote, "remote")
return os.RemoveAll(cachePath) return os.RemoveAll(cachePath)
} }
@@ -144,13 +176,12 @@ func run() error {
flags.ListAll, flags.ListAll,
flags.ListJson, flags.ListJson,
flags.NoStatus, flags.NoStatus,
flags.Nested,
) )
if listOptions.ShouldListTasks() { if listOptions.ShouldListTasks() {
if flags.Silent { if flags.Silent {
return e.ListTaskNames(flags.ListAll) return executor.ListTaskNames(flags.ListAll)
} }
foundTasks, err := e.ListTasks(listOptions) foundTasks, err := executor.ListTasks(listOptions)
if err != nil { if err != nil {
return err return err
} }
@@ -172,32 +203,27 @@ func run() error {
calls = append(calls, &task.Call{Task: "default"}) 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) cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
if err != nil { if err != nil {
return err return err
} }
specialVars := ast.NewVars() globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
specialVars.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted}) globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
specialVars.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash}) globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
specialVars.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll}) globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
specialVars.Set("CLI_SILENT", ast.Var{Value: flags.Silent}) globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
specialVars.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose}) globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
specialVars.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline}) executor.Taskfile.Vars.Merge(globals, nil)
specialVars.Set("CLI_ASSUME_YES", ast.Var{Value: flags.AssumeYes})
e.Taskfile.Vars.ReverseMerge(specialVars, nil)
if !flags.Watch { if !flags.Watch {
e.InterceptInterruptSignals() executor.InterceptInterruptSignals()
} }
ctx := context.Background() ctx = context.Background()
if flags.Status { if flags.Status {
return e.Status(ctx, calls...) return executor.Status(ctx, calls...)
} }
return e.Run(ctx, calls...) return executor.Run(ctx, calls...)
} }

38
cmd/tmp/main.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
if err := run(ctx); err != nil {
fmt.Println(ctx.Err())
fmt.Println(err)
}
}
func run(ctx context.Context) error {
req, err := http.NewRequest("GET", "https://taskfile.dev/schema.json", nil)
if err != nil {
fmt.Println(1)
return err
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
if ctx.Err() != nil {
fmt.Println(2)
return err
}
fmt.Println(3)
return err
}
defer resp.Body.Close()
return nil
}

View File

@@ -61,14 +61,13 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
newVar := templater.ReplaceVar(v, cache) newVar := templater.ReplaceVar(v, cache)
// If the variable should not be evaluated, but is nil, set it to an empty string // If the variable should not be evaluated, but is nil, set it to an empty string
// This stops empty interface errors when using the templater to replace values later // This stops empty interface errors when using the templater to replace values later
// Preserve the Sh field so it can be displayed in summary
if !evaluateShVars && newVar.Value == nil { if !evaluateShVars && newVar.Value == nil {
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh}) result.Set(k, ast.Var{Value: ""})
return nil return nil
} }
// If the variable should not be evaluated and it is set, we can set it and return // If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars { if !evaluateShVars {
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh}) result.Set(k, ast.Var{Value: newVar.Value})
return nil return nil
} }
// Now we can check for errors since we've handled all the cases when we don't want to evaluate // Now we can check for errors since we've handled all the cases when we don't want to evaluate
@@ -198,23 +197,18 @@ func (c *Compiler) ResetCache() {
} }
func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) { 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{ allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]), "TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": filepath.ToSlash(c.Dir), "ROOT_DIR": c.Dir,
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir), "USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(), "TASK_VERSION": version.GetVersion(),
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
"FILE_PATH_SEPARATOR": string(os.PathSeparator),
} }
if t != nil { if t != nil {
allVars["TASK"] = t.Task allVars["TASK"] = t.Task
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir)) allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile) allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile)) allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
} else { } else {
allVars["TASK"] = "" allVars["TASK"] = ""
allVars["TASK_DIR"] = "" allVars["TASK_DIR"] = ""

View File

@@ -1,7 +1,6 @@
# vim: set tabstop=2 shiftwidth=2 expandtab: # vim: set tabstop=2 shiftwidth=2 expandtab:
_GO_TASK_COMPLETION_LIST_OPTION='--list-all' _GO_TASK_COMPLETION_LIST_OPTION='--list-all'
TASK_CMD="${TASK_EXE:-task}"
function _task() function _task()
{ {
@@ -22,14 +21,10 @@ function _task()
# Handle special arguments of options. # Handle special arguments of options.
case "$prev" in case "$prev" in
-d|--dir|--remote-cache-dir) -d|--dir)
_filedir -d _filedir -d
return $? return $?
;; ;;
--cacert|--cert|--cert-key)
_filedir
return $?
;;
-t|--taskfile) -t|--taskfile)
_filedir yaml || return $? _filedir yaml || return $?
_filedir yml _filedir yml
@@ -57,4 +52,4 @@ function _task()
__ltrim_colon_completions "$cur" __ltrim_colon_completions "$cur"
} }
complete -F _task "$TASK_CMD" complete -F _task task

View File

@@ -1,31 +1,4 @@
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end) set -l GO_TASK_PROGNAME task
# Cache variables for experiments (global)
set -g __task_experiments_cache ""
set -g __task_experiments_cache_time 0
# Helper function to get experiments with 1-second cache
function __task_get_experiments --inherit-variable GO_TASK_PROGNAME
set -l now (date +%s)
set -l ttl 1 # Cache for 1 second only
# Return cached value if still valid
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
printf '%s\n' $__task_experiments_cache
return
end
# Refresh cache
set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null)
set -g __task_experiments_cache_time $now
printf '%s\n' $__task_experiments_cache
end
# Helper function to check if an experiment is enabled
function __task_is_experiment_enabled
set -l experiment $argv[1]
__task_get_experiments | string match -qr "^\* $experiment:.*on"
end
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
# Check if the global task is requested # Check if the global task is requested
@@ -54,67 +27,28 @@ function __task_get_tasks --description "Prints all available tasks with their d
end end
# Grab names and descriptions (if any) of the tasks # Grab names and descriptions (if any) of the tasks
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) 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)
if test $output if test $output
echo $output echo $output
end end
end end
complete -c $GO_TASK_PROGNAME \ complete -c $GO_TASK_PROGNAME -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was
-d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \ specified.' -xa "(__task_get_tasks)"
-xa "(__task_get_tasks)" \
-n "not __fish_seen_subcommand_from --"
# Standard flags complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks' complete -c $GO_TASK_PROGNAME -s d -l dir -d 'sets directory of execution'
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)' complete -c $GO_TASK_PROGNAME -l dry -d 'compiles and prints tasks in the order that they would be run, without executing them'
complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks' complete -c $GO_TASK_PROGNAME -s f -l force -d 'forces execution even when the task is up-to-date'
complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell" complete -c $GO_TASK_PROGNAME -s h -l help -d 'shows Task usage'
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution' complete -c $GO_TASK_PROGNAME -s i -l init -d 'creates a new Taskfile.yml in the current folder'
complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names' complete -c $GO_TASK_PROGNAME -s l -l list -d 'lists tasks with description of current Taskfile'
complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing' complete -c $GO_TASK_PROGNAME -s o -l output -d 'sets output style: [interleaved|group|prefixed]' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command' complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'executes tasks provided on command line in parallel'
complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments' complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disables echoing'
complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails' complete -c $GO_TASK_PROGNAME -l status -d 'exits with non-zero exit code if any of the given tasks is not up-to-date'
complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date' complete -c $GO_TASK_PROGNAME -l summary -d 'show summary about a task'
complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory' complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose which Taskfile to run. Defaults to "Taskfile.yml"'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help' complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'enables verbose mode'
complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile' complete -c $GO_TASK_PROGNAME -l version -d 'show Task version'
complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads' complete -c $GO_TASK_PROGNAME -s w -l watch -d 'enables watch of the given task'
complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes'
complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON'
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks'
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing'
complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none"
complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date'
complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary'
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run'
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output'
complete -c $GO_TASK_PROGNAME -l version -d 'show version'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes'
complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts'
# Experimental flags (dynamically checked at completion time via -n condition)
# GentleForce experiment
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'
# RemoteTaskfiles experiment - Options
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
# RemoteTaskfiles experiment - Operations
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l clear-cache -d 'clear remote Taskfile cache'

View File

@@ -1,92 +1,26 @@
using namespace System.Management.Automation using namespace System.Management.Automation
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique Register-ArgumentCompleter -CommandName task -ScriptBlock {
Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
if ($commandName.StartsWith('-')) { if ($commandName.StartsWith('-')) {
$completions = @( $completions = @(
# Standard flags (alphabetical order) [CompletionResult]::new('--list-all ', '--list-all ', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'), [CompletionResult]::new('--color ', '--color', [CompletionResultType]::ParameterName, '--color'),
[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'), [CompletionResult]::new('--concurrency=', '--concurrency=', [CompletionResultType]::ParameterName, 'concurrency'),
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'), [CompletionResult]::new('--interval=', '--interval=', [CompletionResultType]::ParameterName, 'interval'),
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'), [CompletionResult]::new('--output=interleaved ', '--output=interleaved', [CompletionResultType]::ParameterName, '--output='),
[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'), [CompletionResult]::new('--output=group ', '--output=group', [CompletionResultType]::ParameterName, '--output='),
[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'), [CompletionResult]::new('--output=prefixed ', '--output=prefixed', [CompletionResultType]::ParameterName, '--output='),
[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'), [CompletionResult]::new('--dry ', '--dry', [CompletionResultType]::ParameterName, '--dry'),
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'), [CompletionResult]::new('--force ', '--force', [CompletionResultType]::ParameterName, '--force'),
[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'), [CompletionResult]::new('--parallel ', '--parallel', [CompletionResultType]::ParameterName, '--parallel'),
[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'), [CompletionResult]::new('--silent ', '--silent', [CompletionResultType]::ParameterName, '--silent'),
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'), [CompletionResult]::new('--status ', '--status', [CompletionResultType]::ParameterName, '--status'),
[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'), [CompletionResult]::new('--verbose ', '--verbose', [CompletionResultType]::ParameterName, '--verbose'),
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'), [CompletionResult]::new('--watch ', '--watch', [CompletionResultType]::ParameterName, '--watch')
[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),
[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),
[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),
[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),
[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),
[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),
[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes')
) )
# Experimental flags (dynamically added based on enabled experiments)
$experiments = & task --experiments 2>$null | Out-String
if ($experiments -match '\* GENTLE_FORCE:.*on') {
$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')
}
if ($experiments -match '\* REMOTE_TASKFILES:.*on') {
# Options
$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
# Operations
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')
}
return $completions.Where{ $_.CompletionText.StartsWith($commandName) } return $completions.Where{ $_.CompletionText.StartsWith($commandName) }
} }

View File

@@ -1,151 +1,67 @@
#compdef task #compdef task
compdef _task task
typeset -A opt_args typeset -A opt_args
TASK_CMD="${TASK_EXE:-task}"
compdef _task "$TASK_CMD"
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}" _GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
# Check if an experiment is enabled
function __task_is_experiment_enabled() {
local experiment=$1
task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on"
}
# Listing commands from Taskfile.yml # Listing commands from Taskfile.yml
function __task_list() { function __task_list() {
local -a scripts cmd local -a scripts cmd
local -i enabled=0 local -i enabled=0
local taskfile item task desc local taskfile item task desc
cmd=($TASK_CMD) cmd=(task)
taskfile=${(Qv)opt_args[(i)-t|--taskfile]} taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
taskfile=${taskfile//\~/$HOME} taskfile=${taskfile//\~/$HOME}
for arg in "${words[@]:0:$CURRENT}"; do
if [[ "$arg" = "--" ]]; then
# Use default completion for words after `--` as they are CLI_ARGS.
_default
return 0
fi
done
if [[ -n "$taskfile" && -f "$taskfile" ]]; then if [[ -n "$taskfile" && -f "$taskfile" ]]; then
cmd+=(--taskfile "$taskfile")
fi
# Check if global flag is set
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
cmd+=(--global)
fi
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
enabled=1 enabled=1
cmd+=(--taskfile "$taskfile")
else
for taskfile in {T,t}askfile{,.dist}.{yaml,yml}; do
if [[ -f "$taskfile" ]]; then
enabled=1
break
fi
done
fi fi
(( enabled )) || return 0 (( enabled )) || return 0
scripts=() scripts=()
for item in "${(@)${(f)$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION)}[2,-1]#\* }"; do
# 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:]]*}" task="${item%%:[[:space:]]*}"
desc="${item##[^[:space:]]##[[:space:]]##}"
if [[ "$show_desc" == "true" ]]; then scripts+=( "${task//:/\\:}:$desc" )
local desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
else
scripts+=( "$task" )
fi
done done
_describe 'Task to run' scripts
if [[ "$show_desc" == "true" ]]; then
_describe 'Task to run' scripts
else
compadd -Q -a scripts
fi
} }
_task() { _task() {
local -a standard_args operation_args _arguments \
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
standard_args=( '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' '(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' '(-c --color)'{-c,--color}'[colored output]' \
'(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]' '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' '(--dry)--dry[dry-run mode, compile and print tasks only]' \
'(-c --color)'{-c,--color}'[colored output]' '(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
'(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)' '(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' '(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
'(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]' '(-s --silent)'{-s,--silent}'[disable echoing]' \
'(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]' '(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
'(--dry)--dry[dry-run mode, compile and print tasks only]' '(--summary)--summary[show summary\: field from tasks instead of running them]' \
'(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]' '(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
'(--experiments)--experiments[list available experiments]' '(-v --verbose)'{-v,--verbose}'[verbose mode]' \
'(-g --global)'{-g,--global}'[run global Taskfile from home directory]' '(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
'(--insecure)--insecure[allow insecure Taskfile downloads]' + '(operation)' \
'(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: ' {-l,--list}'[list describable tasks]' \
'(-j --json)'{-j,--json}'[format task list as JSON]' {-a,--list-all}'[list all tasks]' \
'(--nested)--nested[nest namespaces when listing as JSON]' {-i,--init}'[create new Taskfile.yml]' \
'(--no-status)--no-status[ignore status when listing as JSON]' '(-*)'{-h,--help}'[show help]' \
'(--interactive)--interactive[prompt for missing required variables]' '(-*)--version[show version and exit]' \
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' '*: :__task_list'
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
'(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'
'(-s --silent)'{-s,--silent}'[disable echoing]'
'(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'
'(--status)--status[exit non-zero if supplied tasks not up-to-date]'
'(--summary)--summary[show summary\: field from tasks instead of running them]'
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'
'(-v --verbose)'{-v,--verbose}'[verbose mode]'
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'
'(-y --yes)'{-y,--yes}'[assume yes to all prompts]'
)
# Experimental flags (dynamically added based on enabled experiments)
# Options (modify behavior)
if __task_is_experiment_enabled "GENTLE_FORCE"; then
standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')
fi
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
standard_args+=(
'(--offline --download)--offline[use only local or cached Taskfiles]'
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
'(--expiry)--expiry[cache expiry duration]:duration: '
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
'(--cert)--cert[client certificate for mTLS]:file:_files'
'(--cert-key)--cert-key[client certificate private key]:file:_files'
)
fi
operation_args=(
# Task names completion (can be specified multiple times)
'(operation)*: :__task_list'
# Operational args completion (mutually exclusive)
+ '(operation)'
'(*)'{-l,--list}'[list describable tasks]'
'(*)'{-a,--list-all}'[list all tasks]'
'(*)'{-i,--init}'[create new Taskfile.yml]'
'(- *)'{-h,--help}'[show help]'
'(- *)--version[show version and exit]'
)
# Experimental operations (dynamically added based on enabled experiments)
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
standard_args+=(
'(--offline --clear-cache)--download[download remote Taskfile]'
)
operation_args+=(
'(* --download)--clear-cache[clear remote Taskfile cache]'
)
fi
_arguments -S $standard_args $operation_args
} }
# don't run the completion function when being source-ed or eval-ed # don't run the completion function when being source-ed or eval-ed

View File

@@ -5,12 +5,15 @@ import (
"cmp" "cmp"
"errors" "errors"
"fmt" "fmt"
"regexp"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
) )
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
type ( type (
TaskfileDecodeError struct { TaskfileDecodeError struct {
Message string Message string
@@ -50,10 +53,10 @@ func (err *TaskfileDecodeError) Error() string {
if len(te.Errors) > 1 { if len(te.Errors) > 1 {
fmt.Fprintln(buf, color.RedString("errs:")) fmt.Fprintln(buf, color.RedString("errs:"))
for _, message := range te.Errors { for _, message := range te.Errors {
fmt.Fprintln(buf, color.RedString("- %s", message)) fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
} }
} else { } else {
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0])) fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
} }
} else { } else {
// Otherwise print the error message normally // Otherwise print the error message normally
@@ -125,3 +128,11 @@ func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *T
err.Snippet = snippet err.Snippet = snippet
return err return err
} }
func extractTypeErrorMessage(message string) string {
matches := typeErrorRegex.FindStringSubmatch(message)
if len(matches) == 2 {
return matches[1]
}
return message
}

View File

@@ -54,10 +54,6 @@ func (err *TaskRunError) TaskExitCode() int {
return err.Code() return err.Code()
} }
func (err *TaskRunError) Unwrap() error {
return err.Err
}
// TaskInternalError when the user attempts to invoke a task that is internal. // TaskInternalError when the user attempts to invoke a task that is internal.
type TaskInternalError struct { type TaskInternalError struct {
TaskName string TaskName string
@@ -166,7 +162,7 @@ func (v MissingVar) String() string {
} }
func (err *TaskMissingRequiredVarsError) Error() string { func (err *TaskMissingRequiredVarsError) Error() string {
vars := make([]string, 0, len(err.MissingVars)) var vars []string
for _, v := range err.MissingVars { for _, v := range err.MissingVars {
vars = append(vars, v.String()) vars = append(vars, v.String())
} }
@@ -195,9 +191,9 @@ type TaskNotAllowedVarsError struct {
func (err *TaskNotAllowedVarsError) Error() string { func (err *TaskNotAllowedVarsError) Error() string {
var builder strings.Builder var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName)) //nolint:staticcheck builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
for _, s := range err.NotAllowedVars { 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)) //nolint:staticcheck builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
} }
return builder.String() return builder.String()

View File

@@ -3,7 +3,6 @@ package errors
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
@@ -12,23 +11,16 @@ import (
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when // TaskfileNotFoundError is returned when no appropriate Taskfile is found when
// searching the filesystem. // searching the filesystem.
type TaskfileNotFoundError struct { type TaskfileNotFoundError struct {
URI string URI string
Walk bool Walk bool
AskInit bool
OwnerChange bool
} }
func (err TaskfileNotFoundError) Error() string { func (err TaskfileNotFoundError) Error() string {
var walkText string var walkText string
if err.OwnerChange { if err.Walk {
walkText = " (or any of the parent directories until ownership changed)." walkText = " (or any of the parent directories)"
} else if err.Walk {
walkText = " (or any of the parent directories)."
} }
if err.AskInit { return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
walkText += " Run `task --init` to create a new Taskfile."
}
return fmt.Sprintf(`task: No Taskfile found at %q%s`, filepath.ToSlash(err.URI), walkText)
} }
func (err TaskfileNotFoundError) Code() int { func (err TaskfileNotFoundError) Code() int {
@@ -55,7 +47,7 @@ type TaskfileInvalidError struct {
} }
func (err TaskfileInvalidError) Error() string { func (err TaskfileInvalidError) Error() string {
return fmt.Sprintf("task: Failed to parse %s:\n%v", filepath.ToSlash(err.URI), err.Err) return fmt.Sprintf("task: Failed to parse %s:\n%v", err.URI, err.Err)
} }
func (err TaskfileInvalidError) Code() int { func (err TaskfileInvalidError) Code() int {
@@ -74,7 +66,7 @@ func (err TaskfileFetchFailedError) Error() string {
if err.HTTPStatusCode != 0 { if err.HTTPStatusCode != 0 {
statusText = fmt.Sprintf(" with status code %d (%s)", err.HTTPStatusCode, http.StatusText(err.HTTPStatusCode)) statusText = fmt.Sprintf(" with status code %d (%s)", err.HTTPStatusCode, http.StatusText(err.HTTPStatusCode))
} }
return fmt.Sprintf(`task: Download of %q failed%s`, filepath.ToSlash(err.URI), statusText) return fmt.Sprintf(`task: Download of %q failed%s`, err.URI, statusText)
} }
func (err TaskfileFetchFailedError) Code() int { func (err TaskfileFetchFailedError) Code() int {
@@ -90,7 +82,7 @@ type TaskfileNotTrustedError struct {
func (err *TaskfileNotTrustedError) Error() string { func (err *TaskfileNotTrustedError) Error() string {
return fmt.Sprintf( return fmt.Sprintf(
`task: Taskfile %q not trusted by user`, `task: Taskfile %q not trusted by user`,
filepath.ToSlash(err.URI), err.URI,
) )
} }
@@ -107,7 +99,7 @@ type TaskfileNotSecureError struct {
func (err *TaskfileNotSecureError) Error() string { func (err *TaskfileNotSecureError) Error() string {
return fmt.Sprintf( return fmt.Sprintf(
`task: Taskfile %q cannot be downloaded over an insecure connection. You can override this by using the --insecure flag`, `task: Taskfile %q cannot be downloaded over an insecure connection. You can override this by using the --insecure flag`,
filepath.ToSlash(err.URI), err.URI,
) )
} }
@@ -124,7 +116,7 @@ type TaskfileCacheNotFoundError struct {
func (err *TaskfileCacheNotFoundError) Error() string { func (err *TaskfileCacheNotFoundError) Error() string {
return fmt.Sprintf( 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`, `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`,
filepath.ToSlash(err.URI), err.URI,
) )
} }
@@ -145,12 +137,12 @@ func (err *TaskfileVersionCheckError) Error() string {
if err.SchemaVersion == nil { if err.SchemaVersion == nil {
return fmt.Sprintf( return fmt.Sprintf(
`task: Missing schema version in Taskfile %q`, `task: Missing schema version in Taskfile %q`,
filepath.ToSlash(err.URI), err.URI,
) )
} }
return fmt.Sprintf( return fmt.Sprintf(
"task: Invalid schema version in Taskfile %q:\nSchema version (%s) %s", "task: Invalid schema version in Taskfile %q:\nSchema version (%s) %s",
filepath.ToSlash(err.URI), err.URI,
err.SchemaVersion.String(), err.SchemaVersion.String(),
err.Message, err.Message,
) )
@@ -170,7 +162,7 @@ type TaskfileNetworkTimeoutError struct {
func (err *TaskfileNetworkTimeoutError) Error() string { func (err *TaskfileNetworkTimeoutError) Error() string {
return fmt.Sprintf( return fmt.Sprintf(
`task: Network connection timed out after %s while attempting to download Taskfile %q`, `task: Network connection timed out after %s while attempting to download Taskfile %q`,
err.Timeout, filepath.ToSlash(err.URI), err.Timeout, err.URI,
) )
} }
@@ -187,8 +179,8 @@ type TaskfileCycleError struct {
func (err TaskfileCycleError) Error() string { func (err TaskfileCycleError) Error() string {
return fmt.Sprintf("task: include cycle detected between %s <--> %s", return fmt.Sprintf("task: include cycle detected between %s <--> %s",
filepath.ToSlash(err.Source), err.Source,
filepath.ToSlash(err.Destination), err.Destination,
) )
} }
@@ -207,7 +199,7 @@ type TaskfileDoesNotMatchChecksum struct {
func (err *TaskfileDoesNotMatchChecksum) Error() string { func (err *TaskfileDoesNotMatchChecksum) Error() string {
return fmt.Sprintf( return fmt.Sprintf(
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q", "task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
filepath.ToSlash(err.URI), err.URI,
err.ActualChecksum, err.ActualChecksum,
err.ExpectedChecksum, err.ExpectedChecksum,
) )

View File

@@ -4,10 +4,11 @@ import (
"context" "context"
"io" "io"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
"github.com/puzpuzpuz/xsync/v4" "github.com/puzpuzpuz/xsync/v3"
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -26,35 +27,21 @@ type (
// within them. // within them.
Executor struct { Executor struct {
// Flags // Flags
Dir string Dir string
Entrypoint string TempDir *TempDir
TempDir TempDir Force bool
Force bool ForceAll bool
ForceAll bool Watch bool
Insecure bool Verbose bool
Download bool Silent bool
Offline bool AssumeYes bool
TrustedHosts []string AssumeTerm bool // Used for testing
Timeout time.Duration Dry bool
CacheExpiryDuration time.Duration Summary bool
RemoteCacheDir string Parallel bool
CACert string Color bool
Cert string Concurrency int
CertKey string Interval time.Duration
Watch bool
Verbose bool
Silent bool
DisableFuzzy bool
AssumeYes bool
AssumeTerm bool // Used for testing
Interactive bool
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
Failfast bool
// I/O // I/O
Stdin io.Reader Stdin io.Reader
@@ -71,28 +58,26 @@ type (
UserWorkingDir string UserWorkingDir string
EnableVersionCheck bool EnableVersionCheck bool
fuzzyModel *fuzzy.Model fuzzyModel *fuzzy.Model
fuzzyModelOnce sync.Once
promptedVars *ast.Vars // vars collected via interactive prompts
concurrencySemaphore chan struct{} concurrencySemaphore chan struct{}
taskCallCount map[string]*int32 taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context executionHashes map[string]context.Context
executionHashesMutex sync.Mutex executionHashesMutex sync.Mutex
watchedDirs *xsync.Map[string, bool] watchedDirs *xsync.MapOf[string, bool]
}
TempDir struct {
Remote string
Fingerprint string
} }
) )
// NewExecutor creates a new [Executor] and applies the given functional options // NewExecutor creates a new [Executor] and applies the given functional options
// to it. // to it.
func NewExecutor(opts ...ExecutorOption) *Executor { func NewExecutor(graph *ast.TaskfileGraph, opts ...ExecutorOption) (*Executor, error) {
tf, err := graph.Merge()
if err != nil {
return nil, err
}
e := &Executor{ e := &Executor{
Timeout: time.Second * 10, Taskfile: tf,
Stdin: os.Stdin, Stdin: os.Stdin,
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
@@ -110,7 +95,10 @@ func NewExecutor(opts ...ExecutorOption) *Executor {
executionHashesMutex: sync.Mutex{}, executionHashesMutex: sync.Mutex{},
} }
e.Options(opts...) e.Options(opts...)
return e if err := e.setup(); err != nil {
return nil, err
}
return e, nil
} }
// Options loops through the given [ExecutorOption] functions and applies them // Options loops through the given [ExecutorOption] functions and applies them
@@ -132,33 +120,23 @@ type dirOption struct {
} }
func (o *dirOption) ApplyToExecutor(e *Executor) { func (o *dirOption) ApplyToExecutor(e *Executor) {
e.Dir = o.dir absDir, err := filepath.Abs(o.dir)
} if err != nil {
e.Dir = o.dir
// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By return
// default, Task will search for one of the default Taskfiles in the given }
// directory. e.Dir = absDir
func WithEntrypoint(entrypoint string) ExecutorOption {
return &entrypointOption{entrypoint}
}
type entrypointOption struct {
entrypoint string
}
func (o *entrypointOption) ApplyToExecutor(e *Executor) {
e.Entrypoint = o.entrypoint
} }
// WithTempDir sets the temporary directory that will be used by [Executor] for // WithTempDir sets the temporary directory that will be used by [Executor] for
// storing temporary files like checksums and cached remote files. By default, // storing temporary files like checksums and cached remote files. By default,
// the temporary directory is set to the user's temporary directory. // the temporary directory is set to the user's temporary directory.
func WithTempDir(tempDir TempDir) ExecutorOption { func WithTempDir(tempDir *TempDir) ExecutorOption {
return &tempDirOption{tempDir} return &tempDirOption{tempDir}
} }
type tempDirOption struct { type tempDirOption struct {
tempDir TempDir tempDir *TempDir
} }
func (o *tempDirOption) ApplyToExecutor(e *Executor) { func (o *tempDirOption) ApplyToExecutor(e *Executor) {
@@ -193,142 +171,6 @@ func (o *forceAllOption) ApplyToExecutor(e *Executor) {
e.ForceAll = o.forceAll e.ForceAll = o.forceAll
} }
// WithInsecure allows the [Executor] to make insecure connections when reading
// remote taskfiles. By default, insecure connections are rejected.
func WithInsecure(insecure bool) ExecutorOption {
return &insecureOption{insecure}
}
type insecureOption struct {
insecure bool
}
func (o *insecureOption) ApplyToExecutor(e *Executor) {
e.Insecure = o.insecure
}
// WithDownload forces the [Executor] to download a fresh copy of the taskfile
// from the remote source.
func WithDownload(download bool) ExecutorOption {
return &downloadOption{download}
}
type downloadOption struct {
download bool
}
func (o *downloadOption) ApplyToExecutor(e *Executor) {
e.Download = o.download
}
// WithOffline stops the [Executor] from being able to make network connections.
// It will still be able to read local files and cached copies of remote files.
func WithOffline(offline bool) ExecutorOption {
return &offlineOption{offline}
}
type offlineOption struct {
offline bool
}
func (o *offlineOption) ApplyToExecutor(e *Executor) {
e.Offline = o.offline
}
// WithTrustedHosts configures the [Executor] with a list of trusted hosts for remote
// Taskfiles. Hosts in this list will not prompt for user confirmation.
func WithTrustedHosts(trustedHosts []string) ExecutorOption {
return &trustedHostsOption{trustedHosts}
}
type trustedHostsOption struct {
trustedHosts []string
}
func (o *trustedHostsOption) ApplyToExecutor(e *Executor) {
e.TrustedHosts = o.trustedHosts
}
// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
// default, the timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ExecutorOption {
return &timeoutOption{timeout}
}
type timeoutOption struct {
timeout time.Duration
}
func (o *timeoutOption) ApplyToExecutor(e *Executor) {
e.Timeout = o.timeout
}
// WithCacheExpiryDuration sets the duration after which the cache is considered
// expired. By default, the cache is 0 (disabled).
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration}
}
type cacheExpiryDurationOption struct {
duration time.Duration
}
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 // 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 // for changes to the fingerprint of the tasks that are run. When changes are
// detected, a new task run is triggered. // detected, a new task run is triggered.
@@ -372,19 +214,6 @@ func (o *silentOption) ApplyToExecutor(e *Executor) {
e.Silent = o.silent e.Silent = o.silent
} }
// WithDisableFuzzy tells the [Executor] to disable fuzzy matching for task names.
func WithDisableFuzzy(disableFuzzy bool) ExecutorOption {
return &disableFuzzyOption{disableFuzzy}
}
type disableFuzzyOption struct {
disableFuzzy bool
}
func (o *disableFuzzyOption) ApplyToExecutor(e *Executor) {
e.DisableFuzzy = o.disableFuzzy
}
// WithAssumeYes tells the [Executor] to assume "yes" for all prompts. // WithAssumeYes tells the [Executor] to assume "yes" for all prompts.
func WithAssumeYes(assumeYes bool) ExecutorOption { func WithAssumeYes(assumeYes bool) ExecutorOption {
return &assumeYesOption{assumeYes} return &assumeYesOption{assumeYes}
@@ -411,19 +240,6 @@ func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
e.AssumeTerm = o.assumeTerm 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 // WithDry tells the [Executor] to output the commands that would be run without
// actually running them. // actually running them.
func WithDry(dry bool) ExecutorOption { func WithDry(dry bool) ExecutorOption {
@@ -604,16 +420,3 @@ type versionCheckOption struct {
func (o *versionCheckOption) ApplyToExecutor(e *Executor) { func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
e.EnableVersionCheck = o.enableVersionCheck e.EnableVersionCheck = o.enableVersionCheck
} }
// WithFailfast tells the [Executor] whether or not to check the version of
func WithFailfast(failfast bool) ExecutorOption {
return &failfastOption{failfast}
}
type failfastOption struct {
failfast bool
}
func (o *failfastOption) ApplyToExecutor(e *Executor) {
e.Failfast = o.failfast
}

View File

@@ -4,18 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"path/filepath"
"slices" "slices"
"strings"
"sync" "sync"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/internal/version"
@@ -23,18 +18,9 @@ import (
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func (e *Executor) Setup() error { func (e *Executor) setup() error {
e.setupLogger() e.setupLogger()
node, err := e.getRootNode() e.setupFuzzyModel()
if err != nil {
return err
}
if err := e.setupTempDir(); err != nil {
return err
}
if err := e.readTaskfile(node); err != nil {
return err
}
e.setupStdFiles() e.setupStdFiles()
if err := e.setupOutput(); err != nil { if err := e.setupOutput(); err != nil {
return err return err
@@ -53,60 +39,6 @@ func (e *Executor) Setup() error {
return nil return nil
} }
func (e *Executor) getRootNode() (taskfile.Node, error) {
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
}
func (e *Executor) readTaskfile(node taskfile.Node) error {
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
defer cf()
debugFunc := func(s string) {
e.Logger.VerboseOutf(logger.Magenta, s)
}
promptFunc := func(s string) error {
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
}
reader := taskfile.NewReader(
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
taskfile.WithTrustedHosts(e.TrustedHosts),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithReaderCACert(e.CACert),
taskfile.WithReaderCert(e.Cert),
taskfile.WithReaderCertKey(e.CertKey),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
}
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
return err
}
return nil
}
func (e *Executor) setupFuzzyModel() { func (e *Executor) setupFuzzyModel() {
if e.Taskfile == nil { if e.Taskfile == nil {
return return
@@ -128,52 +60,6 @@ func (e *Executor) setupFuzzyModel() {
e.fuzzyModel = model e.fuzzyModel = model
} }
func (e *Executor) setupTempDir() error {
if e.TempDir != (TempDir{}) {
return nil
}
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return err
}
projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir)
e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, tempDir),
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
}
}
// 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 = remoteCacheDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)
}
}
return nil
}
func (e *Executor) setupStdFiles() { func (e *Executor) setupStdFiles() {
if e.Stdin == nil { if e.Stdin == nil {
e.Stdin = os.Stdin e.Stdin = os.Stdin
@@ -219,7 +105,7 @@ func (e *Executor) setupCompiler() error {
e.Compiler = &Compiler{ e.Compiler = &Compiler{
Dir: e.Dir, Dir: e.Dir,
Entrypoint: e.Entrypoint, Entrypoint: e.Taskfile.Location,
UserWorkingDir: e.UserWorkingDir, UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env, TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars, TaskfileVars: e.Taskfile.Vars,

View File

@@ -3,9 +3,11 @@ package task_test
import ( import (
"bytes" "bytes"
"cmp" "cmp"
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"testing" "testing"
"github.com/sebdah/goldie/v2" "github.com/sebdah/goldie/v2"
@@ -14,6 +16,7 @@ import (
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -33,7 +36,12 @@ type (
task string task string
vars map[string]any vars map[string]any
input string input string
nodeDir string
nodeEntrypoint string
nodeInsecure bool
readerOpts []taskfile.ReaderOption
executorOpts []task.ExecutorOption executorOpts []task.ExecutorOption
wantReaderError bool
wantSetupError bool wantSetupError bool
wantRunError bool wantRunError bool
wantStatusError bool wantStatusError bool
@@ -46,8 +54,9 @@ type (
func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) { func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
t.Helper() t.Helper()
tt := &ExecutorTest{ tt := &ExecutorTest{
task: "default", task: "default",
vars: map[string]any{}, vars: map[string]any{},
nodeDir: ".",
TaskTest: TaskTest{ TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{}, experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{}, fixtureTemplateData: map[string]any{},
@@ -143,12 +152,53 @@ func (tt *ExecutorTest) run(t *testing.T) {
t.Helper() t.Helper()
f := func(t *testing.T) { f := func(t *testing.T) {
t.Helper() t.Helper()
var buffer SyncBuffer var buf bytes.Buffer
ctx := context.Background()
opts := append( // Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
tt.nodeEntrypoint,
tt.nodeDir,
tt.nodeInsecure,
)
require.NoError(t, err)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")),
)
// Set up a temporary directory for the taskfile reader and task executor
tempDir, err := task.NewTempDir(node.Dir())
require.NoError(t, err)
tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote))
// Set up the taskfile reader
reader := taskfile.NewReader(tt.readerOpts...)
graph, err := reader.Read(ctx, node)
if tt.wantReaderError {
require.Error(t, err)
tt.writeFixtureErrReader(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
executorOpts := slices.Concat(
// Apply the node directory and temp directory to the executor options
// by default, but allow them to by overridden by the test options
[]task.ExecutorOption{
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
},
// Apply the executor options from the test
tt.executorOpts, tt.executorOpts,
task.WithStdout(&buffer), // Force the input/output streams to be set to the test buffer
task.WithStderr(&buffer), []task.ExecutorOption{
task.WithStdout(&buf),
task.WithStderr(&buf),
},
) )
// If the test has input, create a reader for it and add it to the // If the test has input, create a reader for it and add it to the
@@ -156,23 +206,15 @@ func (tt *ExecutorTest) run(t *testing.T) {
if tt.input != "" { if tt.input != "" {
var reader bytes.Buffer var reader bytes.Buffer
reader.WriteString(tt.input) reader.WriteString(tt.input)
opts = append(opts, task.WithStdin(&reader)) executorOpts = append(executorOpts, task.WithStdin(&reader))
} }
// Set up the task executor // Set up the task executor
e := task.NewExecutor(opts...) executor, err := task.NewExecutor(graph, executorOpts...)
if tt.wantSetupError {
// 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
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err) require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buffer.buf) tt.writeFixtureBuffer(t, g, buf)
return return
} else { } else {
require.NoError(t, err) require.NoError(t, err)
@@ -189,11 +231,10 @@ func (tt *ExecutorTest) run(t *testing.T) {
} }
// Run the task and check for errors // Run the task and check for errors
ctx := t.Context() if err := executor.Run(ctx, call); tt.wantRunError {
if err := e.Run(ctx, call); tt.wantRunError {
require.Error(t, err) require.Error(t, err)
tt.writeFixtureErrRun(t, g, err) tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buffer.buf) tt.writeFixtureBuffer(t, g, buf)
return return
} else { } else {
require.NoError(t, err) require.NoError(t, err)
@@ -201,12 +242,12 @@ func (tt *ExecutorTest) run(t *testing.T) {
// If the status flag is set, run the status check // If the status flag is set, run the status check
if tt.wantStatusError { if tt.wantStatusError {
if err := e.Status(ctx, call); err != nil { if err := executor.Status(ctx, call); err != nil {
tt.writeFixtureStatus(t, g, err.Error()) tt.writeFixtureStatus(t, g, err.Error())
} }
} }
tt.writeFixtureBuffer(t, g, buffer.buf) tt.writeFixtureBuffer(t, g, buf)
} }
// Run the test (with a name if it has one) // Run the test (with a name if it has one)
@@ -220,19 +261,16 @@ func (tt *ExecutorTest) run(t *testing.T) {
func TestEmptyTask(t *testing.T) { func TestEmptyTask(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithExecutorOptions( WithNodeDir("testdata/empty_task"),
task.WithDir("testdata/empty_task"), WithExecutorOptions(),
),
) )
} }
func TestEmptyTaskfile(t *testing.T) { func TestEmptyTaskfile(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithExecutorOptions( WithNodeDir("testdata/empty_taskfile"),
task.WithDir("testdata/empty_taskfile"), WithReaderError(),
),
WithSetupError(),
WithFixtureTemplating(), WithFixtureTemplating(),
) )
} }
@@ -241,15 +279,15 @@ func TestEnv(t *testing.T) {
t.Setenv("QUX", "from_os") t.Setenv("QUX", "from_os")
NewExecutorTest(t, NewExecutorTest(t,
WithName("env precedence disabled"), WithName("env precedence disabled"),
WithNodeDir("testdata/env"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true), task.WithSilent(true),
), ),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("env precedence enabled"), WithName("env precedence enabled"),
WithNodeDir("testdata/env"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true), task.WithSilent(true),
), ),
WithExperiment(&experiments.EnvPrecedence, 1), WithExperiment(&experiments.EnvPrecedence, 1),
@@ -259,53 +297,30 @@ func TestEnv(t *testing.T) {
func TestVars(t *testing.T) { func TestVars(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithNodeDir("testdata/vars"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true), 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) { func TestRequires(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithName("required var missing"), WithName("required var missing"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("missing-var"), WithTask("missing-var"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("required var ok"), WithName("required var ok"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("missing-var"), WithTask("missing-var"),
WithVar("FOO", "bar"), WithVar("FOO", "bar"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("fails validation"), WithName("fails validation"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("validation-var"), WithTask("validation-var"),
WithVar("ENV", "dev"), WithVar("ENV", "dev"),
WithVar("FOO", "bar"), WithVar("FOO", "bar"),
@@ -313,83 +328,37 @@ func TestRequires(t *testing.T) {
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("passes validation"), WithName("passes validation"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("validation-var"), WithTask("validation-var"),
WithVar("FOO", "one"), WithVar("FOO", "one"),
WithVar("ENV", "dev"), WithVar("ENV", "dev"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("required var missing + fails validation"), WithName("required var missing + fails validation"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("validation-var"), WithTask("validation-var"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("required var missing + fails validation"), WithName("required var missing + fails validation"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("validation-var-dynamic"), WithTask("validation-var-dynamic"),
WithVar("FOO", "one"), WithVar("FOO", "one"),
WithVar("ENV", "dev"), WithVar("ENV", "dev"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("require before compile"), WithName("require before compile"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("require-before-compile"), WithTask("require-before-compile"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("var defined in task"), WithName("var defined in task"),
WithExecutorOptions( WithNodeDir("testdata/requires"),
task.WithDir("testdata/requires"),
),
WithTask("var-defined-in-task"), 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
func TestSpecialVars(t *testing.T) { func TestSpecialVars(t *testing.T) {
t.Parallel() t.Parallel()
@@ -400,7 +369,6 @@ func TestSpecialVars(t *testing.T) {
// Root // Root
"print-task", "print-task",
"print-root-dir", "print-root-dir",
"print-root-taskfile",
"print-taskfile", "print-taskfile",
"print-taskfile-dir", "print-taskfile-dir",
"print-task-dir", "print-task-dir",
@@ -411,12 +379,13 @@ func TestSpecialVars(t *testing.T) {
"included:print-taskfile-dir", "included:print-taskfile-dir",
} }
for _, dir := range []string{dir, subdir} { for _, executorDir := range []string{dir, subdir} {
for _, test := range tests { for _, test := range tests {
name := fmt.Sprintf("%s-%s", executorDir, test)
NewExecutorTest(t, NewExecutorTest(t,
WithName(fmt.Sprintf("%s-%s", dir, test)), WithName(name),
WithNodeDir(executorDir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
task.WithVersionCheck(true), task.WithVersionCheck(true),
), ),
@@ -430,8 +399,8 @@ func TestSpecialVars(t *testing.T) {
func TestConcurrency(t *testing.T) { func TestConcurrency(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithNodeDir("testdata/concurrency"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/concurrency"),
task.WithConcurrency(1), task.WithConcurrency(1),
), ),
WithPostProcessFn(PPSortedLines), WithPostProcessFn(PPSortedLines),
@@ -441,8 +410,8 @@ func TestConcurrency(t *testing.T) {
func TestParams(t *testing.T) { func TestParams(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithNodeDir("testdata/params"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/params"),
task.WithSilent(true), task.WithSilent(true),
), ),
WithPostProcessFn(PPSortedLines), WithPostProcessFn(PPSortedLines),
@@ -452,15 +421,14 @@ func TestParams(t *testing.T) {
func TestDeps(t *testing.T) { func TestDeps(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithNodeDir("testdata/deps"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/deps"),
task.WithSilent(true), task.WithSilent(true),
), ),
WithPostProcessFn(PPSortedLines), WithPostProcessFn(PPSortedLines),
) )
} }
// TODO: mock fs
func TestStatus(t *testing.T) { func TestStatus(t *testing.T) {
t.Parallel() t.Parallel()
@@ -483,8 +451,8 @@ func TestStatus(t *testing.T) {
// gen-foo creates foo.txt, and will always fail it's status check. // gen-foo creates foo.txt, and will always fail it's status check.
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-foo 1 silent"), WithName("run gen-foo 1 silent"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
), ),
WithTask("gen-foo"), WithTask("gen-foo"),
@@ -495,8 +463,8 @@ func TestStatus(t *testing.T) {
// only exists after the first run. // only exists after the first run.
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-bar 1 silent"), WithName("run gen-bar 1 silent"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
), ),
WithTask("gen-bar"), WithTask("gen-bar"),
@@ -505,8 +473,8 @@ func TestStatus(t *testing.T) {
// if e.Verbose is set to true. // if e.Verbose is set to true.
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-baz silent"), WithName("run gen-baz silent"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
), ),
WithTask("gen-silent-baz"), WithTask("gen-silent-baz"),
@@ -521,8 +489,8 @@ func TestStatus(t *testing.T) {
// Run gen-bar a second time to produce a checksum file that matches bar.txt // Run gen-bar a second time to produce a checksum file that matches bar.txt
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-bar 2 silent"), WithName("run gen-bar 2 silent"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
), ),
WithTask("gen-bar"), WithTask("gen-bar"),
@@ -530,8 +498,8 @@ func TestStatus(t *testing.T) {
// Run gen-bar a third time, to make sure we've triggered the status check. // Run gen-bar a third time, to make sure we've triggered the status check.
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-bar 3 silent"), WithName("run gen-bar 3 silent"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
), ),
WithTask("gen-bar"), WithTask("gen-bar"),
@@ -543,8 +511,8 @@ func TestStatus(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-bar 4 silent"), WithName("run gen-bar 4 silent"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true), task.WithSilent(true),
), ),
WithTask("gen-bar"), WithTask("gen-bar"),
@@ -552,56 +520,44 @@ func TestStatus(t *testing.T) {
// all: not up-to-date // all: not up-to-date
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-foo 2"), WithName("run gen-foo 2"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("gen-foo"), WithTask("gen-foo"),
) )
// status: not up-to-date // status: not up-to-date
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-foo 3"), WithName("run gen-foo 3"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("gen-foo"), WithTask("gen-foo"),
) )
// sources: not up-to-date // sources: not up-to-date
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-bar 5"), WithName("run gen-bar 5"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("gen-bar"), WithTask("gen-bar"),
) )
// all: up-to-date // all: up-to-date
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-bar 6"), WithName("run gen-bar 6"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("gen-bar"), WithTask("gen-bar"),
) )
// sources: not up-to-date, no output produced. // sources: not up-to-date, no output produced.
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-baz 2"), WithName("run gen-baz 2"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("gen-silent-baz"), WithTask("gen-silent-baz"),
) )
// up-to-date, no output produced // up-to-date, no output produced
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-baz 3"), WithName("run gen-baz 3"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("gen-silent-baz"), WithTask("gen-silent-baz"),
) )
// up-to-date, output produced due to Verbose mode. // up-to-date, output produced due to Verbose mode.
NewExecutorTest(t, NewExecutorTest(t,
WithName("run gen-baz 4 verbose"), WithName("run gen-baz 4 verbose"),
WithNodeDir(dir),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(dir),
task.WithVerbose(true), task.WithVerbose(true),
), ),
WithTask("gen-silent-baz"), WithTask("gen-silent-baz"),
@@ -614,32 +570,24 @@ func TestPrecondition(t *testing.T) {
const dir = "testdata/precondition" const dir = "testdata/precondition"
NewExecutorTest(t, NewExecutorTest(t,
WithName("a precondition has been met"), WithName("a precondition has been met"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("foo"), WithTask("foo"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("a precondition was not met"), WithName("a precondition was not met"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("impossible"), WithTask("impossible"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("precondition in dependency fails the task"), WithName("precondition in dependency fails the task"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("depends_on_impossible"), WithTask("depends_on_impossible"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("precondition in cmd fails the task"), WithName("precondition in cmd fails the task"),
WithExecutorOptions( WithNodeDir(dir),
task.WithDir(dir),
),
WithTask("executes_failing_task_as_cmd"), WithTask("executes_failing_task_as_cmd"),
WithRunError(), WithRunError(),
) )
@@ -650,70 +598,40 @@ func TestAlias(t *testing.T) {
NewExecutorTest(t, NewExecutorTest(t,
WithName("alias"), WithName("alias"),
WithExecutorOptions( WithNodeDir("testdata/alias"),
task.WithDir("testdata/alias"),
),
WithTask("f"), WithTask("f"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("duplicate alias"), WithName("duplicate alias"),
WithExecutorOptions( WithNodeDir("testdata/alias"),
task.WithDir("testdata/alias"),
),
WithTask("x"), WithTask("x"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("alias summary"), WithName("alias summary"),
WithNodeDir("testdata/alias"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/alias"),
task.WithSummary(true), task.WithSummary(true),
), ),
WithTask("f"), WithTask("f"),
) )
} }
func TestSummaryWithVarsAndRequires(t *testing.T) {
t.Parallel()
// Test basic case from prompt.md - vars and requires
NewExecutorTest(t,
WithName("vars-and-requires"),
WithExecutorOptions(
task.WithDir("testdata/summary-vars-requires"),
task.WithSummary(true),
),
WithTask("mytask"),
)
// Test with shell variables
NewExecutorTest(t,
WithName("shell-vars"),
WithExecutorOptions(
task.WithDir("testdata/summary-vars-requires"),
task.WithSummary(true),
),
WithTask("with-sh-var"),
)
}
func TestLabel(t *testing.T) { func TestLabel(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithName("up to date"), WithName("up to date"),
WithExecutorOptions( WithNodeDir("testdata/label_uptodate"),
task.WithDir("testdata/label_uptodate"),
),
WithTask("foo"), WithTask("foo"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("summary"), WithName("summary"),
WithNodeDir("testdata/label_summary"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/label_summary"),
task.WithSummary(true), task.WithSummary(true),
), ),
WithTask("foo"), WithTask("foo"),
@@ -721,56 +639,20 @@ func TestLabel(t *testing.T) {
NewExecutorTest(t, NewExecutorTest(t,
WithName("status"), WithName("status"),
WithExecutorOptions( WithNodeDir("testdata/label_status"),
task.WithDir("testdata/label_status"),
),
WithTask("foo"), WithTask("foo"),
WithStatusError(), WithStatusError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("var"), WithName("var"),
WithExecutorOptions( WithNodeDir("testdata/label_var"),
task.WithDir("testdata/label_var"),
),
WithTask("foo"), WithTask("foo"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("label in summary"), WithName("label in summary"),
WithExecutorOptions( WithNodeDir("testdata/label_summary"),
task.WithDir("testdata/label_summary"),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("label in error"),
WithExecutorOptions(
task.WithDir("testdata/label_error"),
),
WithTask("foo"),
WithRunError(),
)
}
func TestPrefix(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("up to date"),
WithExecutorOptions(
task.WithDir("testdata/prefix_uptodate"),
task.WithOutputStyle(ast.Output{Name: "prefixed"}),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("up to dat with no output style"),
WithExecutorOptions(
task.WithDir("testdata/prefix_uptodate"),
),
WithTask("foo"), WithTask("foo"),
) )
} }
@@ -797,8 +679,8 @@ func TestPromptInSummary(t *testing.T) {
opts := []ExecutorTestOption{ opts := []ExecutorTestOption{
WithName(test.name), WithName(test.name),
WithNodeDir("testdata/prompt"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true), task.WithAssumeTerm(true),
), ),
WithTask("foo"), WithTask("foo"),
@@ -816,8 +698,8 @@ func TestPromptWithIndirectTask(t *testing.T) {
t.Parallel() t.Parallel()
NewExecutorTest(t, NewExecutorTest(t,
WithNodeDir("testdata/prompt"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true), task.WithAssumeTerm(true),
), ),
WithTask("bar"), WithTask("bar"),
@@ -830,8 +712,8 @@ func TestPromptAssumeYes(t *testing.T) {
NewExecutorTest(t, NewExecutorTest(t,
WithName("--yes flag should skip prompt"), WithName("--yes flag should skip prompt"),
WithNodeDir("testdata/prompt"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true), task.WithAssumeTerm(true),
task.WithAssumeYes(true), task.WithAssumeYes(true),
), ),
@@ -841,8 +723,8 @@ func TestPromptAssumeYes(t *testing.T) {
NewExecutorTest(t, NewExecutorTest(t,
WithName("task should raise errors.TaskCancelledError"), WithName("task should raise errors.TaskCancelledError"),
WithNodeDir("testdata/prompt"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true), task.WithAssumeTerm(true),
), ),
WithTask("foo"), WithTask("foo"),
@@ -879,8 +761,8 @@ func TestForCmds(t *testing.T) {
for _, test := range tests { for _, test := range tests {
opts := []ExecutorTestOption{ opts := []ExecutorTestOption{
WithName(test.name), WithName(test.name),
WithNodeDir("testdata/for/cmds"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/for/cmds"),
task.WithSilent(true), task.WithSilent(true),
task.WithForce(true), task.WithForce(true),
), ),
@@ -922,8 +804,8 @@ func TestForDeps(t *testing.T) {
for _, test := range tests { for _, test := range tests {
opts := []ExecutorTestOption{ opts := []ExecutorTestOption{
WithName(test.name), WithName(test.name),
WithNodeDir("testdata/for/deps"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/for/deps"),
task.WithSilent(true), task.WithSilent(true),
task.WithForce(true), task.WithForce(true),
// Force output of each dep to be grouped together to prevent interleaving // Force output of each dep to be grouped together to prevent interleaving
@@ -968,8 +850,8 @@ func TestReference(t *testing.T) {
for _, test := range tests { for _, test := range tests {
NewExecutorTest(t, NewExecutorTest(t,
WithName(test.name), WithName(test.name),
WithNodeDir("testdata/var_references"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/var_references"),
task.WithSilent(true), task.WithSilent(true),
task.WithForce(true), task.WithForce(true),
), ),
@@ -1036,8 +918,8 @@ func TestVarInheritance(t *testing.T) {
for _, test := range tests { for _, test := range tests {
NewExecutorTest(t, NewExecutorTest(t,
WithName(test.name), WithName(test.name),
WithNodeDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
WithExecutorOptions( WithExecutorOptions(
task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
task.WithSilent(true), task.WithSilent(true),
task.WithForce(true), task.WithForce(true),
), ),
@@ -1051,26 +933,20 @@ func TestFuzzyModel(t *testing.T) {
NewExecutorTest(t, NewExecutorTest(t,
WithName("fuzzy"), WithName("fuzzy"),
WithExecutorOptions( WithNodeDir("testdata/fuzzy"),
task.WithDir("testdata/fuzzy"),
),
WithTask("instal"), WithTask("instal"),
WithRunError(), WithRunError(),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("not-fuzzy"), WithName("not-fuzzy"),
WithExecutorOptions( WithNodeDir("testdata/fuzzy"),
task.WithDir("testdata/fuzzy"),
),
WithTask("install"), WithTask("install"),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("intern"), WithName("intern"),
WithExecutorOptions( WithNodeDir("testdata/fuzzy"),
task.WithDir("testdata/fuzzy"),
),
WithTask("intern"), WithTask("intern"),
WithRunError(), WithRunError(),
) )
@@ -1081,141 +957,64 @@ func TestIncludeChecksum(t *testing.T) {
NewExecutorTest(t, NewExecutorTest(t,
WithName("correct"), WithName("correct"),
WithExecutorOptions( WithNodeDir("testdata/includes_checksum/correct"),
task.WithDir("testdata/includes_checksum/correct"),
),
) )
NewExecutorTest(t, NewExecutorTest(t,
WithName("incorrect"), WithName("incorrect"),
WithExecutorOptions( WithNodeDir("testdata/includes_checksum/incorrect"),
task.WithDir("testdata/includes_checksum/incorrect"), WithReaderError(),
),
WithSetupError(),
WithFixtureTemplating(), WithFixtureTemplating(),
) )
} }
func TestIncludeSilent(t *testing.T) { func TestWildcard(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("include-taskfile-silent"),
WithExecutorOptions(
task.WithDir("testdata/includes_silent"),
),
WithTask("default"),
)
}
func TestFailfast(t *testing.T) {
t.Parallel()
t.Run("Default", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Option", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true),
task.WithFailfast(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Task", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("task"),
WithExecutorOptions(
task.WithDir("testdata/failfast/task"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
}
func TestIf(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
name string name string
task string call string
vars map[string]any wantErr bool
verbose bool
}{ }{
// Basic command-level if {
{name: "cmd-if-true", task: "cmd-if-true"}, name: "basic wildcard",
{name: "cmd-if-false", task: "cmd-if-false"}, call: "wildcard-foo",
},
// Task-level if {
{name: "task-if-true", task: "task-if-true"}, name: "double wildcard",
{name: "task-if-false", task: "task-if-false", verbose: true}, call: "foo-wildcard-bar",
},
// Task call with if {
{name: "task-call-if-true", task: "task-call-if-true"}, name: "store wildcard",
{name: "task-call-if-false", task: "task-call-if-false", verbose: true}, call: "start-foo",
},
// Go template conditions {
{name: "template-eq-true", task: "template-eq-true"}, name: "matches exactly",
{name: "template-eq-false", task: "template-eq-false", verbose: true}, call: "matches-exactly-*",
{name: "template-ne", task: "template-ne"}, },
{name: "template-bool-true", task: "template-bool-true"}, {
{name: "template-bool-false", task: "template-bool-false"}, name: "no matches",
{name: "template-direct-true", task: "template-direct-true"}, call: "no-match",
{name: "template-direct-false", task: "template-direct-false"}, wantErr: true,
{name: "template-and", task: "template-and"}, },
{name: "template-or", task: "template-or"}, {
name: "multiple matches",
// CLI variable override call: "wildcard-foo-bar",
{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 { for _, test := range tests {
opts := []ExecutorTestOption{ opts := []ExecutorTestOption{
WithName(test.name), WithName(test.name),
WithNodeDir("testdata/wildcards"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/if"),
task.WithSilent(true), task.WithSilent(true),
task.WithVerbose(test.verbose), task.WithForce(true),
), ),
WithTask(test.task), WithTask(test.call),
} }
if test.vars != nil { if test.wantErr {
for k, v := range test.vars { opts = append(opts, WithRunError())
opts = append(opts, WithVar(k, v))
}
} }
NewExecutorTest(t, opts...) NewExecutorTest(t, opts...)
} }

View File

@@ -9,7 +9,6 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/go-task/task/v3/taskrc" "github.com/go-task/task/v3/taskrc"
"github.com/go-task/task/v3/taskrc/ast"
) )
const envPrefix = "TASK_X_" const envPrefix = "TASK_X_"
@@ -32,13 +31,16 @@ var (
var xList []Experiment var xList []Experiment
func Parse(dir string) { func Parse(dir string) {
config, _ := taskrc.GetConfig(dir)
ParseWithConfig(dir, config)
}
func ParseWithConfig(dir string, config *ast.TaskRC) {
// Read any .env files // Read any .env files
readDotEnv(dir) readDotEnv(dir)
// Create a node for the Task config reader
node, _ := taskrc.NewNode("", dir)
// Read the Task config file
reader := taskrc.NewReader()
config, _ := reader.Read(node)
// Initialize the experiments // Initialize the experiments
GentleForce = New("GENTLE_FORCE", config, 1) GentleForce = New("GENTLE_FORCE", config, 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1) RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)

View File

@@ -2,7 +2,9 @@ package task_test
import ( import (
"bytes" "bytes"
"context"
"path/filepath" "path/filepath"
"slices"
"testing" "testing"
"github.com/sebdah/goldie/v2" "github.com/sebdah/goldie/v2"
@@ -10,6 +12,7 @@ import (
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -26,12 +29,17 @@ type (
// running `task gen:fixtures`. // running `task gen:fixtures`.
FormatterTest struct { FormatterTest struct {
TaskTest TaskTest
task string task string
vars map[string]any vars map[string]any
executorOpts []task.ExecutorOption nodeDir string
listOptions task.ListOptions nodeEntrypoint string
wantSetupError bool nodeInsecure bool
wantListError bool readerOpts []taskfile.ReaderOption
executorOpts []task.ExecutorOption
listOptions task.ListOptions
wantReaderError bool
wantSetupError bool
wantListError bool
} }
) )
@@ -41,8 +49,9 @@ type (
func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) { func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
t.Helper() t.Helper()
tt := &FormatterTest{ tt := &FormatterTest{
task: "default", task: "default",
vars: map[string]any{}, vars: map[string]any{},
nodeDir: ".",
TaskTest: TaskTest{ TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{}, experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{}, fixtureTemplateData: map[string]any{},
@@ -114,24 +123,57 @@ func (tt *FormatterTest) run(t *testing.T) {
f := func(t *testing.T) { f := func(t *testing.T) {
t.Helper() t.Helper()
var buf bytes.Buffer var buf bytes.Buffer
ctx := context.Background()
opts := append( // Create a new root node for the given entrypoint
tt.executorOpts, node, err := taskfile.NewRootNode(
task.WithStdout(&buf), tt.nodeEntrypoint,
task.WithStderr(&buf), tt.nodeDir,
tt.nodeInsecure,
) )
require.NoError(t, err)
// Set up the task executor
e := task.NewExecutor(opts...)
// Create a golden fixture file for the output // Create a golden fixture file for the output
g := goldie.New(t, g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")), goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")),
goldie.WithEqualFn(NormalizedEqual),
) )
// Call setup and check for errors // Set up a temporary directory for the taskfile reader and task executor
if err := e.Setup(); tt.wantSetupError { tempDir, err := task.NewTempDir(node.Dir())
require.NoError(t, err)
tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote))
// Set up the taskfile reader
reader := taskfile.NewReader(tt.readerOpts...)
graph, err := reader.Read(ctx, node)
if tt.wantReaderError {
require.Error(t, err)
tt.writeFixtureErrReader(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
executorOpts := slices.Concat(
// Apply the node directory and temp directory to the executor options
// by default, but allow them to by overridden by the test options
[]task.ExecutorOption{
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
},
// Apply the executor options from the test
tt.executorOpts,
// Force the input/output streams to be set to the test buffer
[]task.ExecutorOption{
task.WithStdout(&buf),
task.WithStderr(&buf),
},
)
// Set up the task executor
executor, err := task.NewExecutor(graph, executorOpts...)
if tt.wantSetupError {
require.Error(t, err) require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf) tt.writeFixtureBuffer(t, g, buf)
@@ -147,7 +189,7 @@ func (tt *FormatterTest) run(t *testing.T) {
} }
// Run the formatter and check for errors // Run the formatter and check for errors
if _, err := e.ListTasks(tt.listOptions); tt.wantListError { if _, err := executor.ListTasks(tt.listOptions); tt.wantListError {
require.Error(t, err) require.Error(t, err)
tt.writeFixtureErrList(t, g, err) tt.writeFixtureErrList(t, g, err)
tt.writeFixtureBuffer(t, g, buf) tt.writeFixtureBuffer(t, g, buf)
@@ -171,9 +213,7 @@ func TestNoLabelInList(t *testing.T) {
t.Parallel() t.Parallel()
NewFormatterTest(t, NewFormatterTest(t,
WithExecutorOptions( WithNodeDir("testdata/label_list"),
task.WithDir("testdata/label_list"),
),
WithListOptions(task.ListOptions{ WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true, ListOnlyTasksWithDescriptions: true,
}), }),
@@ -185,9 +225,7 @@ func TestListAllShowsNoDesc(t *testing.T) {
t.Parallel() t.Parallel()
NewFormatterTest(t, NewFormatterTest(t,
WithExecutorOptions( WithNodeDir("testdata/list_mixed_desc"),
task.WithDir("testdata/list_mixed_desc"),
),
WithListOptions(task.ListOptions{ WithListOptions(task.ListOptions{
ListAllTasks: true, ListAllTasks: true,
}), }),
@@ -199,9 +237,7 @@ func TestListCanListDescOnly(t *testing.T) {
t.Parallel() t.Parallel()
NewFormatterTest(t, NewFormatterTest(t,
WithExecutorOptions( WithNodeDir("testdata/list_mixed_desc"),
task.WithDir("testdata/list_mixed_desc"),
),
WithListOptions(task.ListOptions{ WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true, ListOnlyTasksWithDescriptions: true,
}), }),
@@ -212,9 +248,7 @@ func TestListDescInterpolation(t *testing.T) {
t.Parallel() t.Parallel()
NewFormatterTest(t, NewFormatterTest(t,
WithExecutorOptions( WithNodeDir("testdata/list_desc_interpolation"),
task.WithDir("testdata/list_desc_interpolation"),
),
WithListOptions(task.ListOptions{ WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true, ListOnlyTasksWithDescriptions: true,
}), }),
@@ -225,9 +259,7 @@ func TestJsonListFormat(t *testing.T) {
t.Parallel() t.Parallel()
NewFormatterTest(t, NewFormatterTest(t,
WithExecutorOptions( WithNodeDir("testdata/json_list_format"),
task.WithDir("testdata/json_list_format"),
),
WithListOptions(task.ListOptions{ WithListOptions(task.ListOptions{
FormatTaskListAsJSON: true, FormatTaskListAsJSON: true,
}), }),

156
go.mod
View File

@@ -1,137 +1,61 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.25.10 go 1.23.0
require ( require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.6
charm.land/lipgloss/v2 v2.0.3
github.com/Ladicle/tabwriter v1.0.0 github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.5.0 github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.26.1 github.com/alecthomas/chroma/v2 v2.20.0
github.com/chainguard-dev/git-urls v1.0.2 github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0 github.com/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v3 v3.1.0 github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.19.0 github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.2
github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.2.0 github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.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/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/puzpuzpuz/xsync/v4 v4.5.0 github.com/otiai10/copy v1.14.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0 github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.8.0 github.com/sebdah/goldie/v2 v2.7.1
github.com/spf13/pflag v1.0.10 github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.1.0 github.com/zeebo/xxh3 v1.0.2
go.yaml.in/yaml/v3 v3.0.4 golang.org/x/sync v0.16.0
golang.org/x/sync v0.20.0 golang.org/x/term v0.33.0
golang.org/x/term v0.43.0 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 mvdan.cc/sh/v3 v3.12.0
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117
) )
require ( require (
cel.dev/expr v0.25.1 // indirect dario.cat/mergo v1.0.0 // indirect
cloud.google.com/go v0.123.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
cloud.google.com/go/auth v0.18.2 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect github.com/cloudflare/circl v1.6.1 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
cloud.google.com/go/iam v1.5.3 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect github.com/emirpasic/gods v1.18.1 // indirect
cloud.google.com/go/storage v1.61.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect github.com/mattn/go-colorable v0.1.13 // 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.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/dlclark/regexp2/v2 v2.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
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.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/otiai10/mint v1.6.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // 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/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect golang.org/x/crypto v0.37.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect golang.org/x/net v0.39.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sys v0.34.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // 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.51.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
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
) )

368
go.sum
View File

@@ -1,190 +1,83 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
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.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
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 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78= github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y= github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
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 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
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 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
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/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
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 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 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 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
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= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -192,128 +85,93 @@ 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 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/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 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/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.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 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
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/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 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo= mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117 h1:BfzdGSjcnJBb8sPNLudpzTml8zFUxS1N0N/v9IIS6tQ=
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=

65
help.go
View File

@@ -24,17 +24,15 @@ type ListOptions struct {
ListAllTasks bool ListAllTasks bool
FormatTaskListAsJSON bool FormatTaskListAsJSON bool
NoStatus bool NoStatus bool
Nested bool
} }
// NewListOptions creates a new ListOptions instance // NewListOptions creates a new ListOptions instance
func NewListOptions(list, listAll, listAsJson, noStatus, nested bool) ListOptions { func NewListOptions(list, listAll, listAsJson, noStatus bool) ListOptions {
return ListOptions{ return ListOptions{
ListOnlyTasksWithDescriptions: list, ListOnlyTasksWithDescriptions: list,
ListAllTasks: listAll, ListAllTasks: listAll,
FormatTaskListAsJSON: listAsJson, FormatTaskListAsJSON: listAsJson,
NoStatus: noStatus, NoStatus: noStatus,
Nested: nested,
} }
} }
@@ -65,7 +63,7 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
return false, err return false, err
} }
if o.FormatTaskListAsJSON { if o.FormatTaskListAsJSON {
output, err := e.ToEditorOutput(tasks, o.NoStatus, o.Nested) output, err := e.ToEditorOutput(tasks, o.NoStatus)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -137,17 +135,33 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
return nil return nil
} }
func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool) (*editors.Namespace, error) { func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Taskfile, error) {
o := &editors.Taskfile{
Tasks: make([]editors.Task, len(tasks)),
Location: e.Taskfile.Location,
}
var g errgroup.Group var g errgroup.Group
editorTasks := make([]editors.Task, len(tasks))
// Look over each task in parallel and turn it into an editor task
for i := range tasks { for i := range tasks {
aliases := []string{}
if len(tasks[i].Aliases) > 0 {
aliases = tasks[i].Aliases
}
g.Go(func() error { g.Go(func() error {
editorTask := editors.NewTask(tasks[i]) o.Tasks[i] = editors.Task{
Name: tasks[i].Name(),
Task: tasks[i].Task,
Desc: tasks[i].Desc,
Summary: tasks[i].Summary,
Aliases: aliases,
UpToDate: false,
Location: &editors.Location{
Line: tasks[i].Location.Line,
Column: tasks[i].Location.Column,
Taskfile: tasks[i].Location.Taskfile,
},
}
if noStatus { if noStatus {
editorTasks[i] = editorTask
return nil return nil
} }
@@ -166,35 +180,10 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool)
return err return err
} }
editorTask.UpToDate = &upToDate o.Tasks[i].UpToDate = upToDate
editorTasks[i] = editorTask
return nil return nil
}) })
} }
if err := g.Wait(); err != nil { return o, g.Wait()
return nil, err
}
// Create the root namespace
var tasksLen int
if !nested {
tasksLen = len(editorTasks)
}
rootNamespace := &editors.Namespace{
Tasks: make([]editors.Task, tasksLen),
Location: e.Taskfile.Location,
}
// Recursively add namespaces to the root namespace or if nesting is
// disabled add them all to the root namespace
for i, task := range editorTasks {
taskNamespacePath := strings.Split(task.Task, ast.NamespaceSeparator)
if nested {
rootNamespace.AddNamespace(taskNamespacePath, task)
} else {
rootNamespace.Tasks[i] = task
}
}
return rootNamespace, g.Wait()
} }

25
init.go
View File

@@ -6,10 +6,9 @@ import (
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
) )
const defaultFilename = "Taskfile.yml" const defaultTaskFilename = "Taskfile.yml"
//go:embed taskfile/templates/default.yml //go:embed taskfile/templates/default.yml
var DefaultTaskfile string var DefaultTaskfile string
@@ -21,30 +20,22 @@ var DefaultTaskfile string
// //
// The final file path is always returned and may be different from the input path. // The final file path is always returned and may be different from the input path.
func InitTaskfile(path string) (string, error) { func InitTaskfile(path string) (string, error) {
info, err := os.Stat(path) fi, err := os.Stat(path)
if err == nil && !info.IsDir() { if err == nil && !fi.IsDir() {
return path, errors.TaskfileAlreadyExistsError{} return path, errors.TaskfileAlreadyExistsError{}
} }
if info != nil && info.IsDir() { if fi != nil && fi.IsDir() {
// path was a directory, check if there is a Taskfile already path = filepathext.SmartJoin(path, defaultTaskFilename)
if hasDefaultTaskfile(path) { // path was a directory, so check if Taskfile.yml exists in it
if _, err := os.Stat(path); err == nil {
return path, errors.TaskfileAlreadyExistsError{} return path, errors.TaskfileAlreadyExistsError{}
} }
path = filepathext.SmartJoin(path, defaultFilename)
} }
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil { if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
return path, err return path, err
} }
return path, nil return path, nil
} }
func hasDefaultTaskfile(dir string) bool {
for _, name := range taskfile.DefaultTaskfiles {
if _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil {
return true
}
}
return false
}

View File

@@ -10,15 +10,6 @@ type Copier[T any] interface {
DeepCopy() T 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 { func Slice[T any](orig []T) []T {
if orig == nil { if orig == nil {
return nil return nil
@@ -81,7 +72,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
traverseFunc = func(copy, v reflect.Value) error { traverseFunc = func(copy, v reflect.Value) error {
switch v.Kind() { switch v.Kind() {
case reflect.Pointer: case reflect.Ptr:
// Unwrap the pointer // Unwrap the pointer
originalValue := v.Elem() originalValue := v.Elem()
// If the pointer is nil, do nothing // If the pointer is nil, do nothing

View File

@@ -1,15 +1,10 @@
package editors package editors
import (
"github.com/go-task/task/v3/taskfile/ast"
)
type ( type (
// Namespace wraps task list output for use in editor integrations (e.g. VSCode, etc) // Taskfile wraps task list output for use in editor integrations (e.g. VSCode, etc)
Namespace struct { Taskfile struct {
Tasks []Task `json:"tasks"` Tasks []Task `json:"tasks"`
Namespaces map[string]*Namespace `json:"namespaces,omitempty"` Location string `json:"location"`
Location string `json:"location,omitempty"`
} }
// Task describes a single task // Task describes a single task
Task struct { Task struct {
@@ -18,7 +13,7 @@ type (
Desc string `json:"desc"` Desc string `json:"desc"`
Summary string `json:"summary"` Summary string `json:"summary"`
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
UpToDate *bool `json:"up_to_date,omitempty"` UpToDate bool `json:"up_to_date"`
Location *Location `json:"location"` Location *Location `json:"location"`
} }
// Location describes a task's location in a taskfile // Location describes a task's location in a taskfile
@@ -28,59 +23,3 @@ type (
Taskfile string `json:"taskfile"` Taskfile string `json:"taskfile"`
} }
) )
func NewTask(task *ast.Task) Task {
aliases := []string{}
if len(task.Aliases) > 0 {
aliases = task.Aliases
}
return Task{
Name: task.Name(),
Task: task.Task,
Desc: task.Desc,
Summary: task.Summary,
Aliases: aliases,
Location: &Location{
Line: task.Location.Line,
Column: task.Location.Column,
Taskfile: task.Location.Taskfile,
},
}
}
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
if len(namespacePath) == 0 {
return
}
// If there are no child namespaces, then we have found a task and we can
// simply add it to the current namespace
if len(namespacePath) == 1 {
parent.Tasks = append(parent.Tasks, task)
return
}
// Get the key of the current namespace in the path
namespaceKey := namespacePath[0]
// Add the namespace to the parent namespaces map using the namespace key
if parent.Namespaces == nil {
parent.Namespaces = make(map[string]*Namespace, 0)
}
// Search for the current namespace in the parent namespaces map
// If it doesn't exist, create it
namespace, ok := parent.Namespaces[namespaceKey]
if !ok {
namespace = &Namespace{}
parent.Namespaces[namespaceKey] = namespace
}
// Remove the current namespace key from the namespace path.
childNamespacePath := namespacePath[1:]
// If there are no child namespaces in the task name, then we have found the
// namespace of the task and we can add it to the current namespace.
// Otherwise, we need to go deeper
namespace.AddNamespace(childNamespacePath, task)
}

62
internal/env/env.go vendored
View File

@@ -3,9 +3,7 @@ package env
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"time"
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
@@ -63,63 +61,3 @@ func isTypeAllowed(v any) bool {
func GetTaskEnv(key string) string { func GetTaskEnv(key string) string {
return os.Getenv(taskVarPrefix + key) 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
}

View File

@@ -1,20 +0,0 @@
package execext
import (
"runtime"
"strconv"
"github.com/go-task/task/v3/internal/env"
)
var useGoCoreUtils bool
func init() {
// If TASK_CORE_UTILS is set to either true or false, respect that.
// By default, enable on Windows only.
if v, err := strconv.ParseBool(env.GetTaskEnv("CORE_UTILS")); err == nil {
useGoCoreUtils = v
} else {
useGoCoreUtils = runtime.GOOS == "windows"
}
}

View File

@@ -7,8 +7,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"mvdan.cc/sh/moreinterp/coreutils"
"mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
@@ -59,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
r, err := interp.New( r, err := interp.New(
interp.Params(params...), interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)), interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandlers(execHandlers()...), interp.ExecHandlers(execHandler),
interp.OpenHandler(openHandler), interp.OpenHandler(openHandler),
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr), interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
dirOption(opts.Dir), dirOption(opts.Dir),
@@ -127,11 +127,12 @@ func ExpandFields(s string) ([]string, error) {
s = escape(s) s = escape(s)
p := syntax.NewParser() p := syntax.NewParser()
var words []*syntax.Word var words []*syntax.Word
for w, err := range p.WordsSeq(strings.NewReader(s)) { err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
if err != nil {
return nil, err
}
words = append(words, w) words = append(words, w)
return true
})
if err != nil {
return nil, err
} }
cfg := &expand.Config{ cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv), Env: expand.FuncEnviron(os.Getenv),
@@ -142,11 +143,8 @@ func ExpandFields(s string) ([]string, error) {
return expand.Fields(cfg, words...) return expand.Fields(cfg, words...)
} }
func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc) { func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
if useGoCoreUtils { return interp.DefaultExecHandler(15 * time.Second)
handlers = append(handlers, coreutils.ExecHandler)
}
return handlers
} }
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {

View File

@@ -1,102 +0,0 @@
package fingerprint
import (
"bufio"
"os"
"path/filepath"
"strings"
"github.com/go-task/task/v3/internal/gitignore"
)
type gitignoreRule struct {
dir string
matcher gitignore.Matcher
}
// loadGitignoreRules walks up from dir collecting .gitignore files.
// Stops at the first .git (file or directory) found.
// Returns nil if no .git is found (not in a git repo).
func loadGitignoreRules(dir string) []gitignoreRule {
dir, _ = filepath.Abs(dir)
var rules []gitignoreRule
foundGit := false
current := dir
for {
lines := readGitignoreLines(filepath.Join(current, ".gitignore"))
if len(lines) > 0 {
patterns := make([]gitignore.Pattern, 0, len(lines))
for _, line := range lines {
patterns = append(patterns, gitignore.ParsePattern(line, nil))
}
rules = append(rules, gitignoreRule{
dir: current,
matcher: gitignore.NewMatcher(patterns),
})
}
if _, err := os.Stat(filepath.Join(current, ".git")); err == nil {
foundGit = true
break
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
if !foundGit {
return nil
}
return rules
}
func readGitignoreLines(path string) []string {
f, err := os.Open(path)
if err != nil {
return nil
}
defer f.Close()
var lines []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimRight(scanner.Text(), "\r")
if line != "" && !strings.HasPrefix(line, "#") {
lines = append(lines, line)
}
}
if err := scanner.Err(); err != nil {
return nil
}
return lines
}
// filterGitignored removes entries from the file map that match gitignore rules.
func filterGitignored(files map[string]bool, dir string) map[string]bool {
rules := loadGitignoreRules(dir)
if len(rules) == 0 {
return files
}
for path := range files {
for _, rule := range rules {
relPath, err := filepath.Rel(rule.dir, path)
if err != nil || strings.HasPrefix(relPath, "..") {
continue
}
// Sources are files, not directories; pass isDir=false. Per the
// gitignore spec this still matches files under an ignored dir
// (e.g. "build/" matches build/out.txt).
if rule.matcher.Match(strings.Split(filepath.ToSlash(relPath), "/"), false) {
files[path] = false
break
}
}
}
return files
}

View File

@@ -1,112 +0,0 @@
package fingerprint
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/taskfile/ast"
)
func initGitRepo(t *testing.T, dir string) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
}
func TestGlobsWithGitignore(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("ignored"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "also-included.txt"), []byte("also included"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
filesWithout, err := Globs(dir, globs, false)
require.NoError(t, err)
filesWith, err := Globs(dir, globs, true)
require.NoError(t, err)
hasLog := false
for _, f := range filesWithout {
if filepath.Base(f) == "ignored.log" {
hasLog = true
break
}
}
assert.True(t, hasLog, "ignored.log should be present without gitignore filter")
hasLog = false
for _, f := range filesWith {
if filepath.Base(f) == "ignored.log" {
hasLog = true
break
}
}
assert.False(t, hasLog, "ignored.log should be excluded with gitignore filter")
txtCount := 0
for _, f := range filesWith {
if filepath.Ext(f) == ".txt" {
txtCount++
}
}
assert.Equal(t, 2, txtCount, "both .txt files should remain")
}
func TestGlobsWithGitignoreNested(t *testing.T) {
t.Parallel()
dir := t.TempDir()
initGitRepo(t, dir)
subDir := filepath.Join(dir, "sub")
require.NoError(t, os.MkdirAll(subDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "build.out"), []byte("build"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(subDir, globs, true)
require.NoError(t, err)
for _, f := range files {
assert.NotEqual(t, "build.out", filepath.Base(f), "build.out should be excluded by nested .gitignore")
}
}
func TestGlobsWithGitignoreNoRepo(t *testing.T) {
t.Parallel()
// Cannot use t.TempDir() here because it creates a dir inside the
// go-task repo which has a .git parent, defeating the "no repo" test.
dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") //nolint:usetesting
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(dir) })
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644))
globs := []*ast.Glob{
{Glob: "./*"},
}
files, err := Globs(dir, globs, true)
require.NoError(t, err)
assert.Len(t, files, 1)
}

View File

@@ -2,7 +2,6 @@ package fingerprint
import ( import (
"os" "os"
"path/filepath"
"sort" "sort"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
@@ -10,7 +9,7 @@ import (
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) { func Globs(dir string, globs []*ast.Glob) ([]string, error) {
resultMap := make(map[string]bool) resultMap := make(map[string]bool)
for _, g := range globs { for _, g := range globs {
matches, err := glob(dir, g.Glob) matches, err := glob(dir, g.Glob)
@@ -21,11 +20,6 @@ func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) {
resultMap[match] = !g.Negate resultMap[match] = !g.Negate
} }
} }
if useGitignore {
resultMap = filterGitignored(resultMap, dir)
}
return collectKeys(resultMap), nil return collectKeys(resultMap), nil
} }
@@ -56,8 +50,7 @@ func collectKeys(m map[string]bool) []string {
keys := make([]string, 0, len(m)) keys := make([]string, 0, len(m))
for k, v := range m { for k, v := range m {
if v { if v {
// Normalize path separators for consistent sorting across platforms keys = append(keys, k)
keys = append(keys, filepath.ToSlash(k))
} }
} }
sort.Strings(keys) sort.Strings(keys)

View File

@@ -53,7 +53,6 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
if len(t.Generates) > 0 { if len(t.Generates) > 0 {
// For each specified 'generates' field, check whether the files actually exist // For each specified 'generates' field, check whether the files actually exist
for _, g := range t.Generates { for _, g := range t.Generates {
// Exclusion patterns don't represent output files; skip them.
if g.Negate { if g.Negate {
continue continue
} }
@@ -89,7 +88,7 @@ func (*ChecksumChecker) Kind() string {
} }
func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) { func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) {
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore()) sources, err := Globs(t.Dir, t.Sources)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

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

View File

@@ -1,6 +1,7 @@
package fingerprint package fingerprint
import ( import (
"context"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -163,7 +164,7 @@ func TestIsTaskUpToDate(t *testing.T) {
} }
result, err := IsTaskUpToDate( result, err := IsTaskUpToDate(
t.Context(), context.Background(),
tt.task, tt.task,
WithStatusChecker(mockStatusChecker), WithStatusChecker(mockStatusChecker),
WithSourcesChecker(mockSourcesChecker), WithSourcesChecker(mockSourcesChecker),

View File

@@ -8,7 +8,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/fatih/color"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
@@ -16,9 +15,8 @@ import (
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/task/v3/taskrc"
taskrcast "github.com/go-task/task/v3/taskrc/ast"
) )
const usage = `Usage: task [flags...] [task...] const usage = `Usage: task [flags...] [task...]
@@ -54,14 +52,12 @@ var (
TaskSort string TaskSort string
Status bool Status bool
NoStatus bool NoStatus bool
Nested bool
Insecure bool Insecure bool
Force bool Force bool
ForceAll bool ForceAll bool
Watch bool Watch bool
Verbose bool Verbose bool
Silent bool Silent bool
DisableFuzzy bool
AssumeYes bool AssumeYes bool
Dry bool Dry bool
Summary bool Summary bool
@@ -73,20 +69,13 @@ var (
Output ast.Output Output ast.Output
Color bool Color bool
Interval time.Duration Interval time.Duration
Failfast bool
Global bool Global bool
Experiments bool Experiments bool
Download bool Download bool
Offline bool Offline bool
TrustedHosts []string
ClearCache bool ClearCache bool
Timeout time.Duration Timeout time.Duration
CacheExpiryDuration time.Duration CacheExpiryDuration time.Duration
RemoteCacheDir string
CACert string
Cert string
CertKey string
Interactive bool
) )
func init() { func init() {
@@ -107,9 +96,7 @@ func init() {
// Parse the experiments // Parse the experiments
dir = cmp.Or(dir, filepath.Dir(entrypoint)) dir = cmp.Or(dir, filepath.Dir(entrypoint))
experiments.Parse(dir)
config, _ := taskrc.GetConfig(dir)
experiments.ParseWithConfig(dir, config)
// Parse the rest of the flags // Parse the rest of the flags
log.SetFlags(0) log.SetFlags(0)
@@ -118,7 +105,10 @@ func init() {
log.Print(usage) log.Print(usage)
pflag.PrintDefaults() pflag.PrintDefaults()
} }
offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
if err != nil {
offline = false
}
pflag.BoolVar(&Version, "version", false, "Show Task version.") pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
@@ -129,16 +119,13 @@ func init() {
pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].") pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].")
pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.") 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(&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", 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(&Watch, "watch", "w", false, "Enables watch of the given task.")
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, "VERBOSE", func() *bool { return config.Verbose }, false), "Enables verbose mode.") pflag.BoolVarP(&Verbose, "verbose", "v", false, "Enables verbose mode.")
pflag.BoolVarP(&Silent, "silent", "s", getConfig(config, "SILENT", func() *bool { return config.Silent }, false), "Disables echoing.") pflag.BoolVarP(&Silent, "silent", "s", 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", false, "Assume \"yes\" as answer to all prompts.")
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(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
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.BoolVarP(&Dry, "dry", "n", 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.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.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(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.")
@@ -147,10 +134,9 @@ func init() {
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.") 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.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.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", getConfig(config, "COLOR", func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.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, "CONCURRENCY", func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.") pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, "FAILFAST", func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") pflag.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.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -165,40 +151,13 @@ func init() {
// Remote Taskfiles experiment will adds the "download" and "offline" flags // Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled() { if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
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.BoolVar(&Offline, "offline", offline, "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", time.Second*10, "Timeout for downloading remote Taskfiles.")
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.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.") pflag.DurationVar(&CacheExpiryDuration, "expiry", 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() 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 { func Validate() error {
@@ -238,21 +197,12 @@ func Validate() error {
return errors.New("task: --no-status only applies to --json with --list or --list-all") return errors.New("task: --no-status only applies to --json with --list or --list-all")
} }
if Nested && !ListJson {
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 return nil
} }
// WithFlags is a special internal functional option that is used to pass flags // WithFlags is a special internal functional option that is used to pass flags
// from the CLI into any constructor that accepts functional options. // from the CLI into any constructor that accepts functional options.
func WithFlags() task.ExecutorOption { func WithFlags() *flagsOption {
return &flagsOption{} return &flagsOption{}
} }
@@ -279,25 +229,12 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
e.Options( e.Options(
task.WithDir(dir), task.WithDir(dir),
task.WithEntrypoint(Entrypoint),
task.WithForce(Force), task.WithForce(Force),
task.WithForceAll(ForceAll), task.WithForceAll(ForceAll),
task.WithInsecure(Insecure),
task.WithDownload(Download),
task.WithOffline(Offline),
task.WithTrustedHosts(TrustedHosts),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithRemoteCacheDir(RemoteCacheDir),
task.WithCACert(CACert),
task.WithCert(Cert),
task.WithCertKey(CertKey),
task.WithWatch(Watch), task.WithWatch(Watch),
task.WithVerbose(Verbose), task.WithVerbose(Verbose),
task.WithSilent(Silent), task.WithSilent(Silent),
task.WithDisableFuzzy(DisableFuzzy),
task.WithAssumeYes(AssumeYes), task.WithAssumeYes(AssumeYes),
task.WithInteractive(Interactive),
task.WithDry(Dry || Status), task.WithDry(Dry || Status),
task.WithSummary(Summary), task.WithSummary(Summary),
task.WithParallel(Parallel), task.WithParallel(Parallel),
@@ -307,49 +244,14 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithOutputStyle(Output), task.WithOutputStyle(Output),
task.WithTaskSorter(sorter), task.WithTaskSorter(sorter),
task.WithVersionCheck(true), task.WithVersionCheck(true),
task.WithFailfast(Failfast),
) )
} }
// getConfig extracts a config value with priority: env var > taskrc config > fallback func (o *flagsOption) ApplyToReader(r *taskfile.Reader) {
func getConfig[T any](config *taskrcast.TaskRC, envKey string, fieldFunc func() *T, fallback T) T { r.Options(
if envKey != "" { taskfile.WithInsecure(Insecure),
if val, ok := getEnvAs[T](envKey); ok { taskfile.WithDownload(Download),
return val taskfile.WithOffline(Offline),
} taskfile.WithCacheExpiryDuration(CacheExpiryDuration),
} )
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
} }

View File

@@ -37,87 +37,51 @@ func DefaultDir(entrypoint, dir string) string {
return "" return ""
} }
// ResolveDir returns an absolute path to the directory that the task should be // Search will look for files with the given possible filenames using the given
// run in. If the entrypoint and dir are BOTH set, then the Taskfile will not // entrypoint and directory. If the entrypoint is set, it will check if the
// sit inside the directory specified by dir and we should ensure that the dir
// is absolute. Otherwise, the dir will always be the parent directory of the
// resolved entrypoint, so we should return that parent directory.
func ResolveDir(entrypoint, resolvedEntrypoint, dir string) (string, error) {
if entrypoint != "" && dir != "" {
return filepath.Abs(dir)
}
return filepath.Dir(resolvedEntrypoint), nil
}
// Search looks for files with the given possible filenames using the given
// entrypoint and directory. If the entrypoint is set, it checks if the
// entrypoint matches a file or if it matches a directory containing one of the // entrypoint matches a file or if it matches a directory containing one of the
// possible filenames. Otherwise, it walks up the file tree starting at the // possible filenames. Otherwise, it will walk up the file tree starting at the
// given directory and performs a search in each directory for the possible // given directory and perform a search in each directory for the possible
// filenames until it finds a match or reaches the root directory. If the // filenames until it finds a match or reaches the root directory. If the
// entrypoint and directory are both empty, it defaults the directory to the // entrypoint and directory are both empty, it will default the directory to the
// current working directory and performs a recursive search starting there. If // current working directory and perform a recursive search starting there. If a
// a match is found, the absolute path to the file is returned with its // match is found, the absolute path to the file will be returned with its
// directory. If no match is found, an error is returned. // directory. If no match is found, an error will be returned.
func Search(entrypoint, dir string, possibleFilenames []string) (string, error) { func Search(entrypoint, dir string, possibleFilenames []string) (string, string, error) {
var err error var err error
if entrypoint != "" { if entrypoint != "" {
entrypoint, err = SearchPath(entrypoint, possibleFilenames) entrypoint, err = SearchPath(entrypoint, possibleFilenames)
if err != nil { if err != nil {
return "", err return "", "", err
} }
return entrypoint, nil if dir == "" {
dir = filepath.Dir(entrypoint)
} else {
dir, err = filepath.Abs(dir)
if err != nil {
return "", "", err
}
}
return entrypoint, dir, nil
} }
if dir == "" { if dir == "" {
dir, err = os.Getwd() dir, err = os.Getwd()
if err != nil { if err != nil {
return "", err return "", "", err
} }
} }
entrypoint, err = SearchPathRecursively(dir, possibleFilenames) entrypoint, err = SearchPathRecursively(dir, possibleFilenames)
if err != nil { if err != nil {
return "", err return "", "", err
} }
return entrypoint, nil dir = filepath.Dir(entrypoint)
return entrypoint, dir, nil
} }
// SearchAll looks for files with the given possible filenames using the given // Search will check if a file at the given path exists or not. If it does, it
// entrypoint and directory. If the entrypoint is set, it checks if the // will return the path to it. If it does not, it will search for any files at
// entrypoint matches a file or if it matches a directory containing one of the // the given path with any of the given possible names. If any of these match a
// possible filenames and add it to a list of matches. It then walks up the file // file, the first matching path will be returned. If no files are found, an
// tree starting at the given directory and performs a search in each directory
// for the possible filenames until it finds a match or reaches the root
// directory. If the entrypoint and directory are both empty, it defaults the
// directory to the current working directory and performs a recursive search
// starting there. If matches are found, the absolute path to each file is added
// to the list and returned.
func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, error) {
var err error
var entrypoints []string
if entrypoint != "" {
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
if err != nil {
return nil, err
}
entrypoints = append(entrypoints, entrypoint)
}
if dir == "" {
dir, err = os.Getwd()
if err != nil {
return nil, err
}
}
paths, err := SearchNPathRecursively(dir, possibleFilenames, -1)
// 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,
// it will return the path to it. If it does not, it will search for any files
// at the given path with any of the given possible 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. // error will be returned.
func SearchPath(path string, possibleFilenames []string) (string, error) { func SearchPath(path string, possibleFilenames []string) (string, error) {
// Get file info about the path // Get file info about the path
@@ -147,62 +111,36 @@ func SearchPath(path string, possibleFilenames []string) (string, error) {
return "", os.ErrNotExist return "", os.ErrNotExist
} }
// SearchPathRecursively walks up the directory tree starting at the given // SearchRecursively will check if a file at the given path exists by calling
// path, calling the Search function in each directory until it finds a matching // the exists function. If a file is not found, it will walk up the directory
// file or reaches the root directory. On supported operating systems, it will // tree calling the Search function until it finds a file or reaches the root
// also check if the user ID of the directory changes and abort if it does. // directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does.
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) { func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
paths, err := SearchNPathRecursively(path, possibleFilenames, 1)
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
}
}
}
// SearchNPathRecursively walks up the directory tree starting at the given
// path, calling the Search function in each directory and adding each matching
// file that it finds to a list until it reaches the root directory or the
// length of the list exceeds n. On supported operating systems, it will also
// check if the user ID of the directory changes and abort if it does.
func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]string, error) {
var paths []string
owner, err := sysinfo.Owner(path) owner, err := sysinfo.Owner(path)
if err != nil { if err != nil {
return nil, err return "", err
} }
for {
for n == -1 || len(paths) < n {
fpath, err := SearchPath(path, possibleFilenames) fpath, err := SearchPath(path, possibleFilenames)
if err == nil { if err == nil {
paths = append(paths, fpath) return fpath, nil
} }
// Get the parent path/user id // Get the parent path/user id
parentPath := filepath.Dir(path) parentPath := filepath.Dir(path)
parentOwner, err := sysinfo.Owner(parentPath) parentOwner, err := sysinfo.Owner(parentPath)
if err != nil { if err != nil {
return nil, err return "", err
} }
// If the user id of the directory changes indicate a permission error, otherwise // Error if we reached the root directory and still haven't found a file
// the calling code will infer an error condition based on the accumulated // OR if the user id of the directory changes
// contents of paths. if path == parentPath || (parentOwner != owner) {
if path == parentPath { return "", os.ErrNotExist
return paths, nil
} else if parentOwner != owner {
return paths, os.ErrPermission
} }
owner = parentOwner owner = parentOwner
path = parentPath path = parentPath
} }
return paths, nil
} }

View File

@@ -71,30 +71,35 @@ func TestSearch(t *testing.T) {
dir string dir string
possibleFilenames []string possibleFilenames []string
expectedEntrypoint string expectedEntrypoint string
expectedDir string
}{ }{
{ {
name: "find foo.txt using relative entrypoint", name: "find foo.txt using relative entrypoint",
entrypoint: "./testdata/foo.txt", entrypoint: "./testdata/foo.txt",
possibleFilenames: []string{"foo.txt"}, possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
}, },
{ {
name: "find foo.txt using absolute entrypoint", name: "find foo.txt using absolute entrypoint",
entrypoint: filepath.Join(wd, "testdata", "foo.txt"), entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
possibleFilenames: []string{"foo.txt"}, possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
}, },
{ {
name: "find foo.txt using relative dir", name: "find foo.txt using relative dir",
dir: "./testdata", dir: "./testdata",
possibleFilenames: []string{"foo.txt"}, possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
}, },
{ {
name: "find foo.txt using absolute dir", name: "find foo.txt using absolute dir",
dir: filepath.Join(wd, "testdata"), dir: filepath.Join(wd, "testdata"),
possibleFilenames: []string{"foo.txt"}, possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
}, },
{ {
name: "find foo.txt using relative dir and relative entrypoint", name: "find foo.txt using relative dir and relative entrypoint",
@@ -102,6 +107,7 @@ func TestSearch(t *testing.T) {
dir: "./testdata/some/other/dir", dir: "./testdata/some/other/dir",
possibleFilenames: []string{"foo.txt"}, possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
}, },
{ {
name: "find fs.go using no entrypoint or dir", name: "find fs.go using no entrypoint or dir",
@@ -109,6 +115,7 @@ func TestSearch(t *testing.T) {
dir: "", dir: "",
possibleFilenames: []string{"fs.go"}, possibleFilenames: []string{"fs.go"},
expectedEntrypoint: filepath.Join(wd, "fs.go"), expectedEntrypoint: filepath.Join(wd, "fs.go"),
expectedDir: wd,
}, },
{ {
name: "find ../../Taskfile.yml using no entrypoint or dir by walking", name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
@@ -116,109 +123,30 @@ func TestSearch(t *testing.T) {
dir: "", dir: "",
possibleFilenames: []string{"Taskfile.yml"}, possibleFilenames: []string{"Taskfile.yml"},
expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"), expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
expectedDir: filepath.Join(wd, "..", ".."),
}, },
{ {
name: "find foo.txt first if listed first in possible filenames", name: "find foo.txt first if listed first in possible filenames",
entrypoint: "./testdata", entrypoint: "./testdata",
possibleFilenames: []string{"foo.txt", "bar.txt"}, possibleFilenames: []string{"foo.txt", "bar.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
}, },
{ {
name: "find bar.txt first if listed first in possible filenames", name: "find bar.txt first if listed first in possible filenames",
entrypoint: "./testdata", entrypoint: "./testdata",
possibleFilenames: []string{"bar.txt", "foo.txt"}, possibleFilenames: []string{"bar.txt", "foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"), expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
expectedDir: filepath.Join(wd, "testdata"),
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
entrypoint, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames) entrypoint, dir, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.expectedEntrypoint, entrypoint) require.Equal(t, tt.expectedEntrypoint, entrypoint)
require.NoError(t, err)
})
}
}
func TestResolveDir(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
entrypoint string
resolvedEntrypoint string
dir string
expectedDir string
}{
{
name: "find foo.txt using relative entrypoint",
entrypoint: "./testdata/foo.txt",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute entrypoint",
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
dir: "./testdata",
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute dir",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
dir: filepath.Join(wd, "testdata"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir and relative entrypoint",
entrypoint: "./testdata/foo.txt",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
dir: "./testdata/some/other/dir",
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
},
{
name: "find fs.go using no entrypoint or dir",
entrypoint: "",
resolvedEntrypoint: filepath.Join(wd, "fs.go"),
dir: "",
expectedDir: wd,
},
{
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
entrypoint: "",
resolvedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
dir: "",
expectedDir: filepath.Join(wd, "..", ".."),
},
{
name: "find foo.txt first if listed first in possible filenames",
entrypoint: "./testdata",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find bar.txt first if listed first in possible filenames",
entrypoint: "./testdata",
resolvedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
dir, err := ResolveDir(tt.entrypoint, tt.resolvedEntrypoint, tt.dir)
require.NoError(t, err)
require.Equal(t, tt.expectedDir, dir) require.Equal(t, tt.expectedDir, dir)
require.NoError(t, err)
}) })
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,5 +20,5 @@ func Name(t *ast.Task) (string, error) {
func Hash(t *ast.Task) (string, error) { func Hash(t *ast.Task) (string, error) {
h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil) h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil)
return fmt.Sprintf("%s:%s:%d", t.Location.Taskfile, t.LocalName(), h), err return fmt.Sprintf("%s:%d", t.Task, h), err
} }

View File

@@ -1,211 +0,0 @@
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())
}

View File

@@ -3,6 +3,7 @@ package logger
import ( import (
"bufio" "bufio"
"io" "io"
"os"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -42,12 +43,6 @@ type (
PrintFunc func(io.Writer, string, ...any) PrintFunc func(io.Writer, string, ...any)
) )
func None() PrintFunc {
c := color.New()
c.DisableColor()
return c.FprintfFunc()
}
func Default() PrintFunc { func Default() PrintFunc {
return color.New(attrsReset...).FprintfFunc() return color.New(attrsReset...).FprintfFunc()
} }
@@ -101,6 +96,10 @@ func BrightRed() PrintFunc {
} }
func envColor(name string, defaultColor color.Attribute) []color.Attribute { func envColor(name string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false
}
// Fetch the environment variable // Fetch the environment variable
override := env.GetTaskEnv(name) override := env.GetTaskEnv(name)
@@ -150,7 +149,7 @@ func (l *Logger) FOutf(w io.Writer, color Color, s string, args ...any) {
s, args = "%s", []any{s} s, args = "%s", []any{s}
} }
if !l.Color { if !l.Color {
color = None color = Default
} }
print := color() print := color()
print(w, s, args...) print(w, s, args...)
@@ -169,7 +168,7 @@ func (l *Logger) Errf(color Color, s string, args ...any) {
s, args = "%s", []any{s} s, args = "%s", []any{s}
} }
if !l.Color { if !l.Color {
color = None color = Default
} }
print := color() print := color()
print(l.Stderr, s, args...) print(l.Stderr, s, args...)

View File

@@ -24,6 +24,7 @@ func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, cache *templater.Cache)
if g.ErrorOnly && err == nil { if g.ErrorOnly && err == nil {
return nil return nil
} }
return gw.close() return gw.close()
} }
} }
@@ -39,22 +40,14 @@ func (gw *groupWriter) Write(p []byte) (int, error) {
} }
func (gw *groupWriter) close() error { func (gw *groupWriter) close() error {
switch { if gw.buff.Len() == 0 {
case gw.buff.Len() == 0: // don't print begin/end messages if there's no buffered entries
return nil return nil
case gw.begin == "" && gw.end == "": }
_, err := io.Copy(gw.writer, &gw.buff) if _, err := io.WriteString(gw.writer, gw.begin); err != nil {
return err
default:
_, err := io.Copy(gw.writer, gw.combinedBuff())
return err return err
} }
} gw.buff.WriteString(gw.end)
_, err := io.Copy(gw.writer, &gw.buff)
func (gw *groupWriter) combinedBuff() io.Reader { return err
var b bytes.Buffer
_, _ = b.WriteString(gw.begin)
_, _ = io.Copy(&b, &gw.buff)
_, _ = b.WriteString(gw.end)
return &b
} }

View File

@@ -1,8 +1,6 @@
package summary package summary
import ( import (
"fmt"
"os"
"strings" "strings"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -31,9 +29,6 @@ func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
func PrintTask(l *logger.Logger, t *ast.Task) { func PrintTask(l *logger.Logger, t *ast.Task) {
printTaskName(l, t) printTaskName(l, t)
printTaskDescribingText(t, l) printTaskDescribingText(t, l)
printTaskVars(l, t)
printTaskEnv(l, t)
printTaskRequires(l, t)
printTaskDependencies(l, t) printTaskDependencies(l, t)
printTaskAliases(l, t) printTaskAliases(l, t)
printTaskCommands(l, t) printTaskCommands(l, t)
@@ -123,172 +118,3 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
} }
} }
} }
func printTaskVars(l *logger.Logger, t *ast.Task) {
if t.Vars == nil || t.Vars.Len() == 0 {
return
}
osEnvVars := getEnvVarNames()
taskfileEnvVars := make(map[string]bool)
if t.Env != nil {
for key := range t.Env.All() {
taskfileEnvVars[key] = true
}
}
hasNonEnvVars := false
for key := range t.Vars.All() {
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
hasNonEnvVars = true
break
}
}
if !hasNonEnvVars {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "vars:\n")
for key, value := range t.Vars.All() {
// Only display variables that are not from OS environment or Taskfile env
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
formattedValue := formatVarValue(value)
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
}
}
}
func printTaskEnv(l *logger.Logger, t *ast.Task) {
if t.Env == nil || t.Env.Len() == 0 {
return
}
envVars := getEnvVarNames()
hasNonEnvVars := false
for key := range t.Env.All() {
if !isEnvVar(key, envVars) {
hasNonEnvVars = true
break
}
}
if !hasNonEnvVars {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "env:\n")
for key, value := range t.Env.All() {
// Only display variables that are not from OS environment
if !isEnvVar(key, envVars) {
formattedValue := formatVarValue(value)
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
}
}
}
// formatVarValue formats a variable value based on its type.
// Handles static values, shell commands (sh:), references (ref:), and maps.
func formatVarValue(v ast.Var) string {
// Shell command - check this first before Value
// because dynamic vars may have both Sh and an empty Value
if v.Sh != nil {
return fmt.Sprintf("sh: %s", *v.Sh)
}
// Reference
if v.Ref != "" {
return fmt.Sprintf("ref: %s", v.Ref)
}
// Static value
if v.Value != nil {
// Check if it's a map or complex type
if m, ok := v.Value.(map[string]any); ok {
return formatMap(m, 4)
}
// Simple string value
return fmt.Sprintf(`"%v"`, v.Value)
}
return `""`
}
// formatMap formats a map value with proper indentation for YAML.
func formatMap(m map[string]any, indent int) string {
if len(m) == 0 {
return "{}"
}
var result strings.Builder
result.WriteString("\n")
spaces := strings.Repeat(" ", indent)
for k, v := range m {
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v)) //nolint:staticcheck
}
return result.String()
}
func printTaskRequires(l *logger.Logger, t *ast.Task) {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "requires:\n")
l.Outf(logger.Default, " vars:\n")
for _, v := range t.Requires.Vars {
if 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.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 {
l.Outf(logger.Yellow, " - %s\n", v.Name)
}
}
}
func getEnvVarNames() map[string]bool {
envMap := make(map[string]bool)
for _, e := range os.Environ() {
parts := strings.SplitN(e, "=", 2)
if len(parts) > 0 {
envMap[parts[0]] = true
}
}
return envMap
}
// isEnvVar checks if a variable is from OS environment or auto-generated by Task.
func isEnvVar(key string, envVars map[string]bool) bool {
// Filter out auto-generated Task variables
if strings.HasPrefix(key, "TASK_") ||
strings.HasPrefix(key, "CLI_") ||
strings.HasPrefix(key, "ROOT_") ||
key == "TASK" ||
key == "TASKFILE" ||
key == "TASKFILE_DIR" ||
key == "USER_WORKING_DIR" ||
key == "ALIAS" ||
key == "MATCH" ||
key == "PATH_LIST_SEPARATOR" ||
key == "FILE_PATH_SEPARATOR" {
return true
}
return envVars[key]
}

View File

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

View File

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

View File

@@ -7,5 +7,5 @@ import (
) )
func IsTerminal() bool { func IsTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) //nolint:gosec return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
} }

View File

@@ -1 +1 @@
3.51.1 3.44.1

View File

@@ -1,12 +0,0 @@
[tools]
# Runtimes
go = "1.25.10"
node = "24"
pnpm = "11.5.0"
# Dev tools
golangci-lint = "2.12.2"
mockery = "3.2.2"
gotestsum = "latest"
goreleaser = "2"
"go:golang.org/x/exp/cmd/gorelease" = "latest"

32
package-lock.json generated Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@go-task/cli",
"version": "3.44.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@go-task/cli",
"version": "3.26.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@go-task/go-npm": "^0.2.0"
}
},
"node_modules/@go-task/go-npm": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@go-task/go-npm/-/go-npm-0.2.0.tgz",
"integrity": "sha512-vQbdtBvesHm8EUFHX8QKg4rbBodmu9VsAXH1ozpbiN5jdTMOYHTCMM31EurAYmY+rNNtxJQ4JGy6t383RPlqbw==",
"bin": {
"go-npm": "bin/index.js"
}
}
},
"dependencies": {
"@go-task/go-npm": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@go-task/go-npm/-/go-npm-0.2.0.tgz",
"integrity": "sha512-vQbdtBvesHm8EUFHX8QKg4rbBodmu9VsAXH1ozpbiN5jdTMOYHTCMM31EurAYmY+rNNtxJQ4JGy6t383RPlqbw=="
}
}
}

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "@go-task/cli",
"version": "3.44.1",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"postinstall": "go-npm install",
"preuninstall": "go-npm uninstall"
},
"goBinary": {
"name": "task",
"path": "./bin",
"url": "https://github.com/go-task/task/releases/download/v{{version}}/task_{{platform}}_{{arch}}{{archive_ext}}"
},
"files": [],
"repository": {
"type": "git",
"url": "https://github.com/go-task/task.git"
},
"keywords": [
"task",
"taskfile",
"build-tool",
"task-runner"
],
"author": "The Task authors",
"license": "MIT",
"bugs": {
"url": "https://github.com/go-task/task/issues"
},
"homepage": "https://taskfile.dev",
"dependencies": {
"@go-task/go-npm": "^0.2.0"
}
}

View File

@@ -4,180 +4,35 @@ import (
"slices" "slices"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/input"
"github.com/go-task/task/v3/internal/term"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func (e *Executor) canPrompt() bool { func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
return e.Interactive && (e.AssumeTerm || term.IsTerminal()) if t.Requires == nil || len(t.Requires.Vars) == 0 {
}
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 return nil
} }
// Collect all missing vars from the dependency tree var missingVars []errors.MissingVar
visited := make(map[string]bool) for _, requiredVar := range t.Requires.Vars {
varsMap := make(map[string]*ast.VarsWithValidation) _, ok := t.Vars.Get(requiredVar.Name)
if !ok {
var collect func(call *Call) error missingVars = append(missingVars, errors.MissingVar{
collect = func(call *Call) error { Name: requiredVar.Name,
compiledTask, err := e.FastCompiledTask(call) AllowedValues: requiredVar.Enum,
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(varsMap) == 0 { if len(missingVars) > 0 {
return nil return &errors.TaskMissingRequiredVarsError{
} TaskName: t.Name(),
MissingVars: missingVars,
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 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 { func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 { if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil return nil
@@ -187,15 +42,15 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
for _, requiredVar := range t.Requires.Vars { for _, requiredVar := range t.Requires.Vars {
varValue, _ := t.Vars.Get(requiredVar.Name) varValue, _ := t.Vars.Get(requiredVar.Name)
enumValues := getEnumValues(requiredVar.Enum)
value, isString := varValue.Value.(string) value, isString := varValue.Value.(string)
if isString && len(enumValues) > 0 && !slices.Contains(enumValues, value) { if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{ notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value, Value: value,
Enum: enumValues, Enum: requiredVar.Enum,
Name: requiredVar.Name, Name: requiredVar.Name,
}) })
} }
} }
if len(notAllowedValuesVars) > 0 { if len(notAllowedValuesVars) > 0 {
@@ -207,10 +62,3 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
return nil return nil
} }
func getEnumValues(e *ast.Enum) []string {
if e == nil {
return nil
}
return e.Value
}

View File

@@ -15,7 +15,7 @@ const maxInterruptSignals = 3
// time to do cleanup work. // time to do cleanup work.
func (e *Executor) InterceptInterruptSignals() { func (e *Executor) InterceptInterruptSignals() {
ch := make(chan os.Signal, maxInterruptSignals) ch := make(chan os.Signal, maxInterruptSignals)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
for i := range maxInterruptSignals { for i := range maxInterruptSignals {

198
task.go
View File

@@ -6,7 +6,6 @@ import (
"os" "os"
"runtime" "runtime"
"slices" "slices"
"strings"
"sync/atomic" "sync/atomic"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -74,21 +73,14 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
return nil 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...) regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
if err != nil { if err != nil {
return err return err
} }
g := &errgroup.Group{} g, ctx := errgroup.WithContext(ctx)
if e.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
for _, c := range regularCalls { for _, c := range regularCalls {
c := c
if e.Parallel { if e.Parallel {
g.Go(func() error { return e.RunTask(ctx, c) }) g.Go(func() error { return e.RunTask(ctx, c) })
} else { } else {
@@ -121,24 +113,11 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
regularCalls = append(regularCalls, c) regularCalls = append(regularCalls, c)
} }
} }
return regularCalls, watchCalls, err return
} }
// RunTask runs a task by its name // RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call *Call) error { 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) t, err := e.FastCompiledTask(call)
if err != nil { if err != nil {
return err return err
@@ -148,12 +127,8 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return nil return nil
} }
// Check required vars early (before template compilation) if we can't prompt. if err := e.areTaskRequiredVarsSet(t); err != nil {
// This gives a clear "missing required variables" error instead of a template error. return err
if !e.canPrompt() {
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
} }
t, err = e.CompiledTask(call) t, err = e.CompiledTask(call)
@@ -161,35 +136,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return err 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 { if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
return err return err
} }
@@ -204,7 +150,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
release := e.acquireConcurrencyLimit() release := e.acquireConcurrencyLimit()
defer release() defer release()
if err = e.startExecution(ctx, t, func(ctx context.Context) error { return e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) 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 return err
@@ -226,6 +172,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
if t.Method != "" { if t.Method != "" {
method = t.Method method = t.Method
} }
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithTempDir(e.TempDir.Fingerprint),
@@ -237,12 +184,8 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
} }
if upToDate && preCondMet { if upToDate && preCondMet {
if e.Verbose || (!call.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) { if e.Verbose || (!call.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
name := t.Name() e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", 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 return nil
} }
@@ -268,7 +211,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
for i := range t.Cmds { for i := range t.Cmds {
if t.Cmds[i].Defer { if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode) defer e.runDeferred(t, call, i, &deferredExitCode)
continue continue
} }
@@ -286,16 +229,16 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
deferredExitCode = uint8(exitCode) deferredExitCode = uint8(exitCode)
} }
return err if call.Indirect {
return err
}
return &errors.TaskRunError{TaskName: t.Task, Err: err}
} }
} }
e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task) e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
return nil return nil
}); err != nil { })
return &errors.TaskRunError{TaskName: t.Name(), Err: err}
}
return nil
} }
func (e *Executor) mkdir(t *ast.Task) error { func (e *Executor) mkdir(t *ast.Task) error {
@@ -316,15 +259,13 @@ func (e *Executor) mkdir(t *ast.Task) error {
} }
func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error { func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
g := &errgroup.Group{} g, ctx := errgroup.WithContext(ctx)
if e.Failfast || t.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
reacquire := e.releaseConcurrencyLimit() reacquire := e.releaseConcurrencyLimit()
defer reacquire() defer reacquire()
for _, d := range t.Deps { for _, d := range t.Deps {
d := d
g.Go(func() error { g.Go(func() error {
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}) err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil { if err != nil {
@@ -337,11 +278,17 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, deferredExitCode *uint8) { func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i] cmd := t.Cmds[i]
vars, _ := e.Compiler.GetVariables(origTask, call)
cache := &templater.Cache{Vars: vars} cache := &templater.Cache{Vars: vars}
extra := map[string]any{} extra := map[string]any{}
@@ -351,7 +298,6 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra) cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra) cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
if err := e.runCommand(ctx, t, call, i); err != nil { if err := e.runCommand(ctx, t, call, i); err != nil {
@@ -362,37 +308,23 @@ 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 { func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {
cmd := t.Cmds[i] 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 { switch {
case cmd.Task != "": case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit() reacquire := e.releaseConcurrencyLimit()
defer reacquire() defer reacquire()
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true}) err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
var exitCode interp.ExitStatus if err != nil {
if errors.As(err, &exitCode) && cmd.IgnoreError { return err
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
return nil
} }
return err return nil
case cmd.Cmd != "": case cmd.Cmd != "":
if !shouldRunOnCurrentPlatform(cmd.Platforms) { if !shouldRunOnCurrentPlatform(cmd.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd) e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
return nil return nil
} }
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) { if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd) e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
} }
@@ -441,7 +373,7 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
return err return err
} }
if h == "" || t.Watch { if h == "" {
return execute(ctx) return execute(ctx)
} }
@@ -469,40 +401,19 @@ 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 // 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 one of aliases, or if it matches // matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each // a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched. // containing a task and a list of wildcards that were matched.
// If multiple tasks match due to aliases, a TaskNameConflictError is returned. func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
if call == nil { if call == nil {
return nil, nil return nil
} }
var matchingTasks []*MatchingTask var matchingTasks []*MatchingTask
// If there is a direct match, return it // If there is a direct match, return it
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok { if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil}) matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks, nil return matchingTasks
} }
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 // Attempt a wildcard match
for _, value := range e.Taskfile.Tasks.All(nil) { for _, value := range e.Taskfile.Tasks.All(nil) {
if match, wildcards := value.WildcardMatch(call.Task); match { if match, wildcards := value.WildcardMatch(call.Task); match {
@@ -512,7 +423,7 @@ func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
}) })
} }
} }
return matchingTasks, nil return matchingTasks
} }
// GetTask will return the task with the name matching the given call from the taskfile. // GetTask will return the task with the name matching the given call from the taskfile.
@@ -520,11 +431,7 @@ func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
// If multiple tasks contain the same alias or no matches are found an error is returned. // 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) { func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// Search for a matching task // Search for a matching task
matchingTasks, err := e.FindMatchingTasks(call) matchingTasks := e.FindMatchingTasks(call)
if err != nil {
return nil, err
}
if len(matchingTasks) > 0 { if len(matchingTasks) > 0 {
if call.Vars == nil { if call.Vars == nil {
call.Vars = ast.NewVars() call.Vars = ast.NewVars()
@@ -533,18 +440,35 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
return matchingTasks[0].Task, nil 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 we found no tasks
didYouMean := "" if len(aliasedTasks) == 0 {
if !e.DisableFuzzy { didYouMean := ""
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
if e.fuzzyModel != nil { if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task) didYouMean = e.fuzzyModel.SpellCheck(call.Task)
} }
return nil, &errors.TaskNotFoundError{
TaskName: call.Task,
DidYouMean: didYouMean,
}
} }
return nil, &errors.TaskNotFoundError{
TaskName: call.Task, return matchingTask, nil
DidYouMean: didYouMean,
}
} }
type FilterFunc func(task *ast.Task) bool type FilterFunc func(task *ast.Task) bool
@@ -576,7 +500,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks // Compile the list of tasks
for i := range tasks { for i := range tasks {
g.Go(func() error { g.Go(func() error {
compiledTask, err := e.CompiledTaskForTaskList(&Call{Task: tasks[i].Task}) compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task})
if err != nil { if err != nil {
return err return err
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
@@ -12,7 +12,6 @@ type Cmd struct {
Cmd string Cmd string
Task string Task string
For *For For *For
If string
Silent bool Silent bool
Set []string Set []string
Shopt []string Shopt []string
@@ -30,7 +29,6 @@ func (c *Cmd) DeepCopy() *Cmd {
Cmd: c.Cmd, Cmd: c.Cmd,
Task: c.Task, Task: c.Task,
For: c.For.DeepCopy(), For: c.For.DeepCopy(),
If: c.If,
Silent: c.Silent, Silent: c.Silent,
Set: deepcopy.Slice(c.Set), Set: deepcopy.Slice(c.Set),
Shopt: deepcopy.Slice(c.Shopt), Shopt: deepcopy.Slice(c.Shopt),
@@ -57,7 +55,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
Cmd string Cmd string
Task string Task string
For *For For *For
If string
Silent bool Silent bool
Set []string Set []string
Shopt []string Shopt []string
@@ -95,9 +92,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
c.Task = cmdStruct.Task c.Task = cmdStruct.Task
c.Vars = cmdStruct.Vars c.Vars = cmdStruct.Vars
c.For = cmdStruct.For c.For = cmdStruct.For
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
return nil return nil
} }
@@ -105,7 +100,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
if cmdStruct.Cmd != "" { if cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd c.Cmd = cmdStruct.Cmd
c.For = cmdStruct.For c.For = cmdStruct.For
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt c.Shopt = cmdStruct.Shopt

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )

View File

@@ -5,7 +5,7 @@ import (
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v3"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -4,7 +4,7 @@ import (
"iter" "iter"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v3"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/goext" "github.com/go-task/task/v3/internal/goext"

View File

@@ -3,7 +3,7 @@ package ast
import ( import (
"fmt" "fmt"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )

View File

@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
@@ -22,56 +22,9 @@ 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 { type VarsWithValidation struct {
Name string Name string
Enum *Enum Enum []string
} }
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation { func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
@@ -80,7 +33,7 @@ func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
} }
return &VarsWithValidation{ return &VarsWithValidation{
Name: v.Name, Name: v.Name,
Enum: v.Enum.DeepCopy(), Enum: v.Enum,
} }
} }
@@ -100,7 +53,7 @@ func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode: case yaml.MappingNode:
var vv struct { var vv struct {
Name string Name string
Enum *Enum Enum []string
} }
if err := node.Decode(&vv); err != nil { if err := node.Decode(&vv); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)

View File

@@ -5,7 +5,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
@@ -13,7 +13,7 @@ import (
// Task represents a task // Task represents a task
type Task struct { type Task struct {
Task string `hash:"ignore"` Task string
Cmds []*Cmd Cmds []*Cmd
Deps []*Dep Deps []*Dep
Label string Label string
@@ -32,81 +32,58 @@ type Task struct {
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
Silent *bool Silent bool
Interactive bool Interactive bool
Internal bool Internal bool
Method string Method string
Prefix string `hash:"ignore"` Prefix string
IgnoreError bool IgnoreError bool
UseGitignore *bool
Run string Run string
Platforms []*Platform Platforms []*Platform
If string
Watch bool Watch bool
Location *Location Location *Location
Failfast bool
// Populated during merging // Populated during merging
Namespace string `hash:"ignore"` Namespace string
IncludeVars *Vars IncludeVars *Vars
IncludedTaskfileVars *Vars IncludedTaskfileVars *Vars
FullName string `hash:"ignore"`
} }
func (t *Task) Name() string { func (t *Task) Name() string {
if t.Label != "" { if t.Label != "" {
return t.Label return t.Label
} }
if t.FullName != "" {
return t.FullName
}
return t.Task return t.Task
} }
func (t *Task) LocalName() string { func (t *Task) LocalName() string {
name := t.FullName name := t.Task
name = strings.TrimPrefix(name, t.Namespace) name = strings.TrimPrefix(name, t.Namespace)
name = strings.TrimPrefix(name, ":") name = strings.TrimPrefix(name, ":")
return name 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
}
// ShouldUseGitignore returns true if gitignore filtering is enabled for the task.
// Returns false if UseGitignore is nil or set to false.
func (t *Task) ShouldUseGitignore() bool {
return t.UseGitignore != nil && *t.UseGitignore
}
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values. // 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) { func (t *Task) WildcardMatch(name string) (bool, []string) {
names := append([]string{t.Task}, t.Aliases...) // 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, "*")
for _, taskName := range names { // If there are no wildcards, return false
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)")) if len(wildcards) == 0 {
regex := regexp.MustCompile(regexStr) return false, nil
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
} }
return false, nil // 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
} }
func (t *Task) UnmarshalYAML(node *yaml.Node) error { func (t *Task) UnmarshalYAML(node *yaml.Node) error {
@@ -151,19 +128,16 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
Silent *bool `yaml:"silent,omitempty"` Silent bool
Interactive bool Interactive bool
Internal bool Internal bool
Method string Method string
Prefix string Prefix string
IgnoreError bool `yaml:"ignore_error"` IgnoreError bool `yaml:"ignore_error"`
UseGitignore *bool `yaml:"use_gitignore,omitempty"`
Run string Run string
Platforms []*Platform Platforms []*Platform
If string
Requires *Requires Requires *Requires
Watch bool Watch bool
Failfast bool
} }
if err := node.Decode(&task); err != nil { if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)
@@ -192,19 +166,16 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Vars = task.Vars t.Vars = task.Vars
t.Env = task.Env t.Env = task.Env
t.Dotenv = task.Dotenv t.Dotenv = task.Dotenv
t.Silent = deepcopy.Scalar(task.Silent) t.Silent = task.Silent
t.Interactive = task.Interactive t.Interactive = task.Interactive
t.Internal = task.Internal t.Internal = task.Internal
t.Method = task.Method t.Method = task.Method
t.Prefix = task.Prefix t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError t.IgnoreError = task.IgnoreError
t.UseGitignore = deepcopy.Scalar(task.UseGitignore)
t.Run = task.Run t.Run = task.Run
t.Platforms = task.Platforms t.Platforms = task.Platforms
t.If = task.If
t.Requires = task.Requires t.Requires = task.Requires
t.Watch = task.Watch t.Watch = task.Watch
t.Failfast = task.Failfast
return nil return nil
} }
@@ -236,24 +207,19 @@ func (t *Task) DeepCopy() *Task {
Vars: t.Vars.DeepCopy(), Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(), Env: t.Env.DeepCopy(),
Dotenv: deepcopy.Slice(t.Dotenv), Dotenv: deepcopy.Slice(t.Dotenv),
Silent: deepcopy.Scalar(t.Silent), Silent: t.Silent,
Interactive: t.Interactive, Interactive: t.Interactive,
Internal: t.Internal, Internal: t.Internal,
Method: t.Method, Method: t.Method,
Prefix: t.Prefix, Prefix: t.Prefix,
IgnoreError: t.IgnoreError, IgnoreError: t.IgnoreError,
UseGitignore: deepcopy.Scalar(t.UseGitignore),
Run: t.Run, Run: t.Run,
IncludeVars: t.IncludeVars.DeepCopy(), IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(), IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
Platforms: deepcopy.Slice(t.Platforms), Platforms: deepcopy.Slice(t.Platforms),
If: t.If,
Location: t.Location.DeepCopy(), Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(), Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace, Namespace: t.Namespace,
FullName: t.FullName,
Watch: t.Watch,
Failfast: t.Failfast,
} }
return c return c
} }

View File

@@ -5,7 +5,7 @@ import (
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )
@@ -20,21 +20,20 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c
// Taskfile is the abstract syntax tree for a Taskfile // Taskfile is the abstract syntax tree for a Taskfile
type Taskfile struct { type Taskfile struct {
Location string Location string
Version *semver.Version Version *semver.Version
Output Output Output Output
Method string Method string
Includes *Includes Includes *Includes
Set []string Set []string
Shopt []string Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks *Tasks Tasks *Tasks
Silent bool Silent bool
Dotenv []string Dotenv []string
Run string Run string
Interval time.Duration Interval time.Duration
UseGitignore bool `yaml:"use_gitignore"`
} }
// Merge merges the second Taskfile into the first // Merge merges the second Taskfile into the first
@@ -60,14 +59,6 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Tasks == nil { if t1.Tasks == nil {
t1.Tasks = NewTasks() 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.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include) t1.Env.Merge(t2.Env, include)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars) return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
@@ -77,20 +68,19 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
var taskfile struct { var taskfile struct {
Version *semver.Version Version *semver.Version
Output Output Output Output
Method string Method string
Includes *Includes Includes *Includes
Set []string Set []string
Shopt []string Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks *Tasks Tasks *Tasks
Silent bool Silent bool
Dotenv []string Dotenv []string
Run string Run string
Interval time.Duration Interval time.Duration
UseGitignore bool `yaml:"use_gitignore"`
} }
if err := node.Decode(&taskfile); err != nil { if err := node.Decode(&taskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)
@@ -108,7 +98,6 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run tf.Run = taskfile.Run
tf.Interval = taskfile.Interval tf.Interval = taskfile.Interval
tf.UseGitignore = taskfile.UseGitignore
if tf.Includes == nil { if tf.Includes == nil {
tf.Includes = NewIncludes() tf.Includes = NewIncludes()
} }

View File

@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )

View File

@@ -8,7 +8,7 @@ import (
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v3"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
@@ -244,8 +244,8 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
} }
func taskNameWithNamespace(taskName string, namespace string) string { func taskNameWithNamespace(taskName string, namespace string) string {
if after, ok := strings.CutPrefix(taskName, NamespaceSeparator); ok { if strings.HasPrefix(taskName, NamespaceSeparator) {
return after return strings.TrimPrefix(taskName, NamespaceSeparator)
} }
return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName) return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName)
} }

View File

@@ -1,7 +1,7 @@
package ast package ast
import ( import (
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )
@@ -18,10 +18,7 @@ type Var struct {
func (v *Var) UnmarshalYAML(node *yaml.Node) error { func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
key := "<none>" key := node.Content[0].Value
if len(node.Content) > 0 {
key = node.Content[0].Value
}
switch key { switch key {
case "sh", "ref", "map": case "sh", "ref", "map":
var m struct { var m struct {

View File

@@ -5,7 +5,7 @@ import (
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "github.com/elliotchance/orderedmap/v3"
"go.yaml.in/yaml/v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
@@ -113,12 +113,12 @@ func (vars *Vars) ToCacheMap() (m map[string]any) {
m[k] = v.Value m[k] = v.Value
} }
} }
return m return
} }
// Merge loops over other and merges it values with the variables in vars. If // 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 // the include parameter is not nil and its it is an advanced import, the
// directory is set to the value of the include parameter. // directory is set set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) { func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil { if vars == nil || vars.om == nil || other == nil {
return return
@@ -133,35 +133,6 @@ 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 { func (vs *Vars) DeepCopy() *Vars {
if vs == nil { if vs == nil {
return nil return nil

View File

@@ -3,7 +3,6 @@ package taskfile
import ( import (
"context" "context"
"strings" "strings"
"time"
giturls "github.com/chainguard-dev/git-urls" giturls "github.com/chainguard-dev/git-urls"
@@ -33,15 +32,13 @@ func NewRootNode(
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
timeout time.Duration,
opts ...NodeOption,
) (Node, error) { ) (Node, error) {
dir = fsext.DefaultDir(entrypoint, dir) dir = fsext.DefaultDir(entrypoint, dir)
// If the entrypoint is "-", we read from stdin // If the entrypoint is "-", we read from stdin
if entrypoint == "-" { if entrypoint == "-" {
return NewStdinNode(dir) return NewStdinNode(dir)
} }
return NewNode(entrypoint, dir, insecure, opts...) return NewNode(entrypoint, dir, insecure)
} }
func NewNode( func NewNode(
@@ -73,16 +70,6 @@ func NewNode(
return node, err return node, err
} }
func isRemoteEntrypoint(entrypoint string) bool {
scheme, _ := getScheme(entrypoint)
switch scheme {
case "git", "http", "https":
return true
default:
return false
}
}
func getScheme(uri string) (string, error) { func getScheme(uri string) (string, error) {
u, err := giturls.Parse(uri) u, err := giturls.Parse(uri)
if u == nil { if u == nil {
@@ -93,8 +80,8 @@ func getScheme(uri string) (string, error) {
return "git", nil return "git", nil
} }
if before, _, ok := strings.Cut(uri, "://"); ok { if i := strings.Index(uri, "://"); i != -1 {
return before, nil return uri[:i], nil
} }
return "", nil return "", nil

View File

@@ -10,9 +10,6 @@ type (
parent Node parent Node
dir string dir string
checksum string checksum string
caCert string
cert string
certKey string
} }
) )
@@ -57,21 +54,3 @@ func (node *baseNode) Checksum() string {
func (node *baseNode) Verify(checksum string) bool { func (node *baseNode) Verify(checksum string) bool {
return node.checksum == "" || node.checksum == checksum 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
}
}

View File

@@ -4,8 +4,8 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext" "github.com/go-task/task/v3/internal/fsext"
@@ -18,30 +18,15 @@ type FileNode struct {
} }
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
// Find the entrypoint file var err error
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles) base := NewBaseNode(dir, opts...)
if err != nil { entrypoint, base.dir, err = fsext.Search(entrypoint, base.dir, defaultTaskfiles)
if errors.Is(err, os.ErrNotExist) {
if entrypoint == "" {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true}
} else {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
}
} else if errors.Is(err, os.ErrPermission) {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true, OwnerChange: true}
}
return nil, err
}
// Resolve the directory
resolvedDir, err := fsext.ResolveDir(entrypoint, resolvedEntrypoint, dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &FileNode{ return &FileNode{
baseNode: NewBaseNode(resolvedDir, opts...), baseNode: base,
entrypoint: resolvedEntrypoint, entrypoint: entrypoint,
}, nil }, nil
} }
@@ -60,7 +45,10 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path // If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) { if strings.Contains(entrypoint, "://") {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
return entrypoint, nil return entrypoint, nil
} }

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