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
276 changed files with 22428 additions and 20644 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

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

@@ -1,112 +0,0 @@
name: CI
on:
pull_request:
push:
tags:
- v*
branches:
- main
concurrency:
group: ci-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
build:
name: 🔨 Build (${{ matrix.go-version }})
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: 🔨 Build
run: go build -v ./cmd/task
test:
name: 🧪 Test (${{ matrix.go-version }}, ${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: ⬇️ Setup Task
uses: go-task/setup-task@v1
- name: 🧪 Test
run: task test --output group --output-group-begin '::group::{{.TASK}}' --output-group-end '::endgroup::'
lint:
name: 🔍 Lint (${{ matrix.go-version }})
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: 🔍 Lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.7.1
lint-jsonschema:
name: 📋 Lint JSON Schema
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: ⬇️ Install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: 📋 Validate JSON Schema
run: check-jsonschema --check-metaschema website/src/public/schema.json
ci-status:
name: ✅ CI
runs-on: ubuntu-latest
needs: [build, test, lint, lint-jsonschema]
if: always()
steps:
- name: ✅ Check CI status
run: |
if [[ "${{ needs.build.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.lint-jsonschema.result }}" != "success" ]]; then
echo "CI failed"
exit 1
fi
echo "CI passed"

View File

@@ -8,7 +8,7 @@ jobs:
issue-awaiting-response: issue-awaiting-response:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -8,7 +8,7 @@ jobs:
issue-closed: issue-closed:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -9,7 +9,7 @@ jobs:
if: github.event.label.name == format('status{0} proposed', ':') if: github.event.label.name == format('status{0} proposed', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -23,7 +23,7 @@ jobs:
if: github.event.label.name == format('status{0} draft', ':') if: github.event.label.name == format('status{0} draft', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -37,7 +37,7 @@ jobs:
if: github.event.label.name == format('status{0} candidate', ':') if: github.event.label.name == format('status{0} candidate', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -51,7 +51,7 @@ jobs:
if: github.event.label.name == format('status{0} stable', ':') if: github.event.label.name == format('status{0} stable', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -65,7 +65,7 @@ jobs:
if: github.event.label.name == format('status{0} released', ':') if: github.event.label.name == format('status{0} released', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -85,7 +85,7 @@ jobs:
if: github.event.label.name == format('status{0} abandoned', ':') if: github.event.label.name == format('status{0} abandoned', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -105,7 +105,7 @@ jobs:
if: github.event.label.name == format('status{0} superseded', ':') if: github.event.label.name == format('status{0} superseded', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -8,7 +8,7 @@ jobs:
issue-needs-triage: issue-needs-triage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

74
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: Lint
on:
pull_request:
push:
tags:
- v*
branches:
- main
jobs:
lint:
name: Lint
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1.0
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: 3.12
- uses: actions/checkout@v4
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema)
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,4 +1,4 @@
name: Release nightly name: Realease nightly
on: on:
workflow_dispatch: workflow_dispatch:
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: 1.25.x go-version: 1.24.x
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
@@ -27,4 +27,3 @@ jobs:
env: env:
GITHUB_TOKEN: ${{secrets.GH_PAT}} GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}

View File

@@ -5,40 +5,19 @@ on:
tags: tags:
- 'v*' - 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: 1.25.x go-version: 1.24.x
- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Update npm
run: npm install -g npm@latest
- name: Install Task
uses: go-task/setup-task@v1
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: 'website/package.json'
run_install: 'true'
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
@@ -49,9 +28,3 @@ jobs:
env: env:
GITHUB_TOKEN: ${{secrets.GH_PAT}} GITHUB_TOKEN: ${{secrets.GH_PAT}}
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

38
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Test
on:
pull_request:
push:
tags:
- v*
branches:
- main
jobs:
test:
name: Test
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v5
with:
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
run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::'

View File

@@ -68,8 +68,7 @@ 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
@@ -127,7 +126,7 @@ winget:
repository: repository:
owner: go-task owner: go-task
name: winget-pkgs name: winget-pkgs
branch: 'task-{{.Version}}' branch: 'chore/task-{{.Version}}'
pull_request: pull_request:
enabled: true enabled: true
draft: false draft: false
@@ -136,39 +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 task runner / simpler Make alternative written in Go
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:
- "any-distro/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,129 +1,5 @@
# Changelog # Changelog
## Unreleased
- 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).
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- The `--summary` flag now displays `vars:` (both global and task-level),
`env:`, and `requires:` sections. Dynamic variables show their shell command
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
@vmaerten).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Improved performance of fuzzy task name matching by implementing lazy
initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option
to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).
- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`
and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to
skip confirmation prompts for specified hosts when using Remote Taskfiles
(#2491, #2473 by @maciejlech).
- 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).
## 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,6 +1,6 @@
<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</h1> <h1>Task</h1>
@@ -10,7 +10,7 @@
</p> </p>
<p> <p>
<a href="https://taskfile.dev/docs/installation">Installation</a> | <a href="https://taskfile.dev/docs/getting-started">Getting Started</a> | <a href="https://taskfile.dev/docs/guide">Docs</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a> <a href="https://taskfile.dev/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,12 +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://magic.dev/">
<img src="website/src/public/img/magic.png" height="100px" width="200px" title="Magic" />
</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'
@@ -132,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:
@@ -212,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"
@@ -230,3 +222,8 @@ tasks:
- "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)))
} }
@@ -121,17 +149,47 @@ func changelog(version *semver.Version) error {
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) 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

@@ -16,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"
) )
@@ -110,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)
} }
@@ -128,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
} }
@@ -166,17 +213,17 @@ func run() error {
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent}) globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose}) globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline}) globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
e.Taskfile.Vars.Merge(globals, nil) executor.Taskfile.Vars.Merge(globals, 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

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()
{ {
@@ -53,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
set -l now (date +%s)
set -l ttl 1 # Cache for 1 second only
# Return cached value if still valid
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
printf '%s\n' $__task_experiments_cache
return
end
# Refresh cache
set -g __task_experiments_cache (task --experiments 2>/dev/null)
set -g __task_experiments_cache_time $now
printf '%s\n' $__task_experiments_cache
end
# Helper function to check if an experiment is enabled
function __task_is_experiment_enabled
set -l experiment $argv[1]
__task_get_experiments | string match -qr "^\* $experiment:.*on"
end
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME 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
@@ -60,56 +33,22 @@ function __task_get_tasks --description "Prints all available tasks with their d
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 -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'
# 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

@@ -5,81 +5,22 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
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('-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')
# 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,47 +1,36 @@
#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
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)output}[2,-1]#\* }"; do for item in "${(@)${(f)$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION)}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}" task="${item%%:[[:space:]]*}"
desc="${item##[^[:space:]]##[[:space:]]##}" desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" ) scripts+=( "${task//:/\\:}:$desc" )
@@ -50,78 +39,29 @@ function __task_list() {
} }
_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]' \
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' '(-*)--version[show version and exit]' \
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' '*: :__task_list'
'(--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: '
)
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/v4" "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.Err.Error())) fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
} }
} else { } else {
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0].Err.Error())) 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())
} }

View File

@@ -11,18 +11,14 @@ 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
} }
func (err TaskfileNotFoundError) Error() string { func (err TaskfileNotFoundError) Error() string {
var walkText string var walkText string
if err.Walk { if err.Walk {
walkText = " (or any of the parent directories)." walkText = " (or any of the parent directories)"
}
if err.AskInit {
walkText += " Run `task --init` to create a new Taskfile."
} }
return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText) return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
} }

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,30 +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
Watch bool Parallel bool
Verbose bool Color bool
Silent bool Concurrency int
DisableFuzzy bool Interval time.Duration
AssumeYes bool
AssumeTerm bool // Used for testing
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
@@ -66,27 +58,26 @@ type (
UserWorkingDir string UserWorkingDir string
EnableVersionCheck bool EnableVersionCheck bool
fuzzyModel *fuzzy.Model fuzzyModel *fuzzy.Model
fuzzyModelOnce sync.Once
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,
@@ -104,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
@@ -126,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) {
@@ -187,90 +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
}
// 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.
@@ -314,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}
@@ -533,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,19 +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/fsext"
"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"
@@ -24,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
@@ -54,54 +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)
if os.IsNotExist(err) {
return nil, errors.TaskfileNotFoundError{
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
Walk: true,
AskInit: true,
}
}
if err != nil {
return nil, err
}
e.Dir = node.Dir()
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.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
@@ -123,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),
}
}
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir != "" {
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
}
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
@@ -214,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,22 +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")),
)
// 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)
@@ -188,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)
@@ -200,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)
@@ -219,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(),
) )
} }
@@ -240,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),
@@ -258,8 +297,8 @@ 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),
), ),
) )
@@ -269,25 +308,19 @@ 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"),
@@ -295,48 +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"),
) )
} }
// TODO: mock fs
func TestSpecialVars(t *testing.T) { func TestSpecialVars(t *testing.T) {
t.Parallel() t.Parallel()
@@ -357,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),
), ),
@@ -376,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),
@@ -387,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),
@@ -398,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()
@@ -429,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"),
@@ -441,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"),
@@ -451,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"),
@@ -467,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"),
@@ -476,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"),
@@ -489,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"),
@@ -498,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"),
@@ -560,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(),
) )
@@ -596,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"),
@@ -667,37 +639,22 @@ 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"), WithTask("foo"),
) )
NewExecutorTest(t,
WithName("label in error"),
WithExecutorOptions(
task.WithDir("testdata/label_error"),
),
WithTask("foo"),
WithRunError(),
)
} }
func TestPromptInSummary(t *testing.T) { func TestPromptInSummary(t *testing.T) {
@@ -722,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"),
@@ -741,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"),
@@ -755,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),
), ),
@@ -766,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"),
@@ -804,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),
), ),
@@ -847,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
@@ -893,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),
), ),
@@ -961,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),
), ),
@@ -976,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(),
) )
@@ -1006,64 +957,65 @@ 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 TestFailfast(t *testing.T) { func TestWildcard(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("Default", func(t *testing.T) { tests := []struct {
t.Parallel() name string
call string
wantErr bool
}{
{
name: "basic wildcard",
call: "wildcard-foo",
},
{
name: "double wildcard",
call: "foo-wildcard-bar",
},
{
name: "store wildcard",
call: "start-foo",
},
{
name: "matches exactly",
call: "matches-exactly-*",
},
{
name: "no matches",
call: "no-match",
wantErr: true,
},
{
name: "multiple matches",
call: "wildcard-foo-bar",
},
}
NewExecutorTest(t, for _, test := range tests {
WithName("default"), opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/wildcards"),
WithExecutorOptions( WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true), task.WithSilent(true),
task.WithForce(true),
), ),
WithPostProcessFn(PPSortedLines), WithTask(test.call),
WithRunError(), }
) if test.wantErr {
}) opts = append(opts, WithRunError())
}
t.Run("Option", func(t *testing.T) { NewExecutorTest(t, opts...)
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(),
)
})
} }

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,23 +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")),
) )
// 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)
@@ -146,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)
@@ -170,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,
}), }),
@@ -184,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,
}), }),
@@ -198,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,
}), }),
@@ -211,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,
}), }),
@@ -224,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,
}), }),

38
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.24.0 go 1.23.0
require ( require (
github.com/Ladicle/tabwriter v1.0.0 github.com/Ladicle/tabwriter v1.0.0
@@ -12,56 +12,50 @@ require (
github.com/elliotchance/orderedmap/v3 v3.1.0 github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.7.0 github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.4 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/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.2.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.0.2 github.com/zeebo/xxh3 v1.0.2
go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/sync v0.16.0
golang.org/x/sync v0.18.0 golang.org/x/term v0.33.0
golang.org/x/term v0.37.0 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b
mvdan.cc/sh/v3 v3.12.0 mvdan.cc/sh/v3 v3.12.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/otiai10/mint v1.6.3 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // 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/skeema/knownhosts v1.3.1 // 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.20251014130006-62f7144b33da // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.34.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

82
go.sum
View File

@@ -7,14 +7,16 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 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.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -34,8 +36,6 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 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=
@@ -52,12 +52,10 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
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=
@@ -78,12 +76,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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=
@@ -100,8 +94,10 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 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 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 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=
@@ -109,54 +105,46 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 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/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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.20251014130006-62f7144b33da h1:Vst9Tvq3G6f6pYBvxy7coi2arDsnOZ3Mkj8MkNarSK8=
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da/go.mod h1:R49zft13memK20EgFAvmTbXBS0t29UvglnM0BCA1ldQ=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
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.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -166,14 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
@@ -185,7 +173,5 @@ 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-20251109230715-65adef8e2c5b h1:vTpx76nZDTP/BAGnnhEXYjM+8nPKe9+I86qCErBvjCw=
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b/go.mod h1:bDyKbUYKqkFunWmxxuSPrkYpln9QZcUsqu7W128qYW4=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

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

@@ -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)
}

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),
@@ -143,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,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

@@ -5,6 +5,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -12,10 +13,10 @@ import (
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"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/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...]
@@ -51,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
@@ -70,12 +69,10 @@ 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
@@ -99,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)
@@ -110,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.")
@@ -121,12 +119,10 @@ 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, 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, func() *bool { return config.Verbose }, false), "Enables verbose mode.") pflag.BoolVarP(&Verbose, "verbose", "v", false, "Enables verbose mode.")
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.") pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.") pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
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", 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.")
@@ -139,9 +135,8 @@ func init() {
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", 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, 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, 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.")
@@ -156,12 +151,12 @@ 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, 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", config.Remote.TrustedHosts, "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, 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, 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.Parse() pflag.Parse()
} }
@@ -202,16 +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")
}
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{}
} }
@@ -238,19 +229,11 @@ 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.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.WithDry(Dry || Status), task.WithDry(Dry || Status),
task.WithSummary(Summary), task.WithSummary(Summary),
@@ -261,19 +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 directly from a pointer field with a fallback default func (o *flagsOption) ApplyToReader(r *taskfile.Reader) {
func getConfig[T any](config *taskrcast.TaskRC, fieldFunc func() *T, fallback T) T { r.Options(
if config == nil { taskfile.WithInsecure(Insecure),
return fallback taskfile.WithDownload(Download),
} taskfile.WithOffline(Offline),
taskfile.WithCacheExpiryDuration(CacheExpiryDuration),
field := fieldFunc() )
if field != nil {
return *field
}
return fallback
} }

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)
if err != nil {
return nil, err
}
return append(entrypoints, paths...), nil
}
// 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,56 +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) owner, err := sysinfo.Owner(path)
if err != nil { if err != nil {
return "", err return "", err
} }
if len(paths) == 0 { for {
return "", os.ErrNotExist
}
return paths[0], nil
}
// 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)
if err != nil {
return nil, err
}
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
} }
// Error if we reached the root directory and still haven't found a file // Error if we reached the root directory and still haven't found a file
// OR if the user id of the directory changes // OR if the user id of the directory changes
if path == parentPath || (parentOwner != owner) { if path == parentPath || (parentOwner != owner) {
return paths, nil return "", os.ErrNotExist
} }
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

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

@@ -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,168 +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))
}
return result.String()
}
func printTaskRequires(l *logger.Logger, t *ast.Task) {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "requires:\n")
l.Outf(logger.Default, " vars:\n")
for _, v := range t.Requires.Vars {
// If the variable has enum constraints, format accordingly
if len(v.Enum) > 0 {
l.Outf(logger.Yellow, " - %s:\n", v.Name)
l.Outf(logger.Yellow, " enum:\n")
for _, enumValue := range v.Enum {
l.Outf(logger.Yellow, " - %s\n", enumValue)
}
} else {
// Simple required variable
l.Outf(logger.Yellow, " - %s\n", v.Name)
}
}
}
func getEnvVarNames() map[string]bool {
envMap := make(map[string]bool)
for _, e := range os.Environ() {
parts := strings.SplitN(e, "=", 2)
if len(parts) > 0 {
envMap[parts[0]] = true
}
}
return envMap
}
// isEnvVar checks if a variable is from OS environment or auto-generated by Task.
func isEnvVar(key string, envVars map[string]bool) bool {
// Filter out auto-generated Task variables
if strings.HasPrefix(key, "TASK_") ||
strings.HasPrefix(key, "CLI_") ||
strings.HasPrefix(key, "ROOT_") ||
key == "TASK" ||
key == "TASKFILE" ||
key == "TASKFILE_DIR" ||
key == "USER_WORKING_DIR" ||
key == "ALIAS" ||
key == "MATCH" {
return true
}
return envVars[key]
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/uuid" "github.com/google/uuid"
"go.yaml.in/yaml/v4" "gopkg.in/yaml.v3"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"

View File

@@ -1 +1 @@
3.45.5 3.44.1

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"
}
}

49
task.go
View File

@@ -78,11 +78,9 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
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 {
@@ -115,7 +113,7 @@ 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
@@ -152,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
@@ -174,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),
@@ -212,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
} }
@@ -230,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 {
@@ -260,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 {
@@ -281,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{}
@@ -456,17 +459,15 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// If we found no tasks // If we found no tasks
if len(aliasedTasks) == 0 { if len(aliasedTasks) == 0 {
didYouMean := "" didYouMean := ""
if !e.DisableFuzzy { if e.fuzzyModel != nil {
e.fuzzyModelOnce.Do(e.setupFuzzyModel) didYouMean = e.fuzzyModel.SpellCheck(call.Task)
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
}
} }
return nil, &errors.TaskNotFoundError{ return nil, &errors.TaskNotFoundError{
TaskName: call.Task, TaskName: call.Task,
DidYouMean: didYouMean, DidYouMean: didYouMean,
} }
} }
return matchingTask, nil return matchingTask, nil
} }
@@ -499,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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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

@@ -5,7 +5,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"go.yaml.in/yaml/v4" "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
@@ -36,33 +36,27 @@ type Task struct {
Interactive bool Interactive bool
Internal bool Internal bool
Method string Method string
Prefix string `hash:"ignore"` Prefix string
IgnoreError bool IgnoreError bool
Run string Run string
Platforms []*Platform Platforms []*Platform
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
@@ -144,7 +138,6 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Platforms []*Platform Platforms []*Platform
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)
@@ -183,7 +176,6 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Platforms = task.Platforms t.Platforms = task.Platforms
t.Requires = task.Requires t.Requires = task.Requires
t.Watch = task.Watch t.Watch = task.Watch
t.Failfast = task.Failfast
return nil return nil
} }
@@ -228,8 +220,6 @@ func (t *Task) DeepCopy() *Task {
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,
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/v4" "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/v4" "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/v4" "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/v4" "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/v4" "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,7 +113,7 @@ 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

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,7 +32,6 @@ func NewRootNode(
entrypoint string, entrypoint string,
dir string, dir string,
insecure bool, insecure bool,
timeout time.Duration,
) (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
@@ -72,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 {

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,24 +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) {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
}
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
} }
@@ -54,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
} }

View File

@@ -98,11 +98,6 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
} }
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}
dir, _ := filepath.Split(node.path) dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint)) resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
if node.ref != "" { if node.ref != "" {

View File

@@ -21,17 +21,6 @@ func TestGitNode_ssh(t *testing.T) {
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint) assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
} }
func TestGitNode_sshWithAltRepo(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
entrypoint, err := node.ResolveEntrypoint("git@github.com:foo/other.git//Taskfile.yml?ref=dev")
assert.NoError(t, err)
assert.Equal(t, "git@github.com:foo/other.git//Taskfile.yml?ref=dev", entrypoint)
}
func TestGitNode_sshWithDir(t *testing.T) { func TestGitNode_sshWithDir(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -53,7 +53,7 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) req, err := http.NewRequest("GET", url.String(), nil)
if err != nil { if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.Location()} return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
} }

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"strings"
"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"
@@ -42,7 +43,7 @@ func (node *StdinNode) Read() ([]byte, error) {
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path // If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) { if strings.Contains(entrypoint, "://") {
return entrypoint, nil return entrypoint, nil
} }

View File

@@ -3,14 +3,13 @@ package taskfile
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/dominikbraun/graph" "github.com/dominikbraun/graph"
"go.yaml.in/yaml/v4"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"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/env" "github.com/go-task/task/v3/internal/env"
@@ -44,7 +43,6 @@ type (
insecure bool insecure bool
download bool download bool
offline bool offline bool
trustedHosts []string
tempDir string tempDir string
cacheExpiryDuration time.Duration cacheExpiryDuration time.Duration
debugFunc DebugFunc debugFunc DebugFunc
@@ -61,7 +59,6 @@ func NewReader(opts ...ReaderOption) *Reader {
insecure: false, insecure: false,
download: false, download: false,
offline: false, offline: false,
trustedHosts: nil,
tempDir: os.TempDir(), tempDir: os.TempDir(),
cacheExpiryDuration: 0, cacheExpiryDuration: 0,
debugFunc: nil, debugFunc: nil,
@@ -122,20 +119,6 @@ func (o *offlineOption) ApplyToReader(r *Reader) {
r.offline = o.offline r.offline = o.offline
} }
// WithTrustedHosts configures the [Reader] with a list of trusted hosts for remote
// Taskfiles. Hosts in this list will not prompt for user confirmation.
func WithTrustedHosts(trustedHosts []string) ReaderOption {
return &trustedHostsOption{trustedHosts: trustedHosts}
}
type trustedHostsOption struct {
trustedHosts []string
}
func (o *trustedHostsOption) ApplyToReader(r *Reader) {
r.trustedHosts = o.trustedHosts
}
// WithTempDir sets the temporary directory that will be used by the [Reader]. // WithTempDir sets the temporary directory that will be used by the [Reader].
// By default, the reader uses [os.TempDir]. // By default, the reader uses [os.TempDir].
func WithTempDir(tempDir string) ReaderOption { func WithTempDir(tempDir string) ReaderOption {
@@ -223,28 +206,6 @@ func (r *Reader) promptf(format string, a ...any) error {
return nil return nil
} }
// isTrusted checks if a URI's host matches any of the trusted hosts patterns.
func (r *Reader) isTrusted(uri string) bool {
if len(r.trustedHosts) == 0 {
return false
}
// Parse the URI to extract the host
parsedURL, err := url.Parse(uri)
if err != nil {
return false
}
host := parsedURL.Host
// Check against each trusted pattern (exact match including port if provided)
for _, pattern := range r.trustedHosts {
if host == pattern {
return true
}
}
return false
}
func (r *Reader) include(ctx context.Context, node Node) error { func (r *Reader) include(ctx context.Context, node Node) error {
// Create a new vertex for the Taskfile // Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{ vertex := &ast.TaskfileVertex{
@@ -498,9 +459,9 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
// If there is no manual checksum pin, run the automatic checks // If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" { if node.Checksum() == "" {
// Prompt the user if required (unless host is trusted) // Prompt the user if required
prompt := cache.ChecksumPrompt(checksum) prompt := cache.ChecksumPrompt(checksum)
if prompt != "" && !r.isTrusted(node.Location()) { if prompt != "" {
if err := func() error { if err := func() error {
r.promptMutex.Lock() r.promptMutex.Lock()
defer r.promptMutex.Unlock() defer r.promptMutex.Unlock()

View File

@@ -12,8 +12,7 @@ import (
) )
var ( var (
// DefaultTaskfiles is the list of Taskfile file names supported by default. defaultTaskfiles = []string{
DefaultTaskfiles = []string{
"Taskfile.yml", "Taskfile.yml",
"taskfile.yml", "taskfile.yml",
"Taskfile.yaml", "Taskfile.yaml",
@@ -29,7 +28,6 @@ var (
"text/x-yaml", "text/x-yaml",
"application/yaml", "application/yaml",
"application/x-yaml", "application/x-yaml",
"application/octet-stream",
} }
) )
@@ -68,7 +66,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
// If the request was not successful, append the default Taskfile names to // If the request was not successful, append the default Taskfile names to
// the URL and return the URL of the first successful request // the URL and return the URL of the first successful request
for _, taskfile := range DefaultTaskfiles { for _, taskfile := range defaultTaskfiles {
// Fixes a bug with JoinPath where a leading slash is not added to the // Fixes a bug with JoinPath where a leading slash is not added to the
// path if it is empty // path if it is empty
if u.Path == "" { if u.Path == "" {

View File

@@ -1,13 +1,12 @@
# yaml-language-server: $schema=https://taskfile.dev/schema.json # https://taskfile.dev
version: '3' version: '3'
vars: vars:
GREETING: Hello, world! GREETING: Hello, World!
tasks: tasks:
default: default:
desc: Print a greeting message
cmds: cmds:
- echo "{{.GREETING}}" - echo "{{.GREETING}}"
silent: true silent: true

View File

@@ -1,60 +1,8 @@
package ast package ast
import ( import "github.com/Masterminds/semver/v3"
"cmp"
"maps"
"slices"
"time"
"github.com/Masterminds/semver/v3"
)
type TaskRC struct { type TaskRC struct {
Version *semver.Version `yaml:"version"` Version *semver.Version `yaml:"version"`
Verbose *bool `yaml:"verbose"` Experiments map[string]int `yaml:"experiments"`
DisableFuzzy *bool `yaml:"disable-fuzzy"`
Concurrency *int `yaml:"concurrency"`
Remote Remote `yaml:"remote"`
Failfast bool `yaml:"failfast"`
Experiments map[string]int `yaml:"experiments"`
}
type Remote struct {
Insecure *bool `yaml:"insecure"`
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
TrustedHosts []string `yaml:"trusted-hosts"`
}
// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
func (t *TaskRC) Merge(other *TaskRC) {
if other == nil {
return
}
t.Version = cmp.Or(other.Version, t.Version)
if t.Experiments == nil && other.Experiments != nil {
t.Experiments = other.Experiments
} else if t.Experiments != nil && other.Experiments != nil {
maps.Copy(t.Experiments, other.Experiments)
}
// Merge Remote fields
t.Remote.Insecure = cmp.Or(other.Remote.Insecure, t.Remote.Insecure)
t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline)
t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)
t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)
if len(other.Remote.TrustedHosts) > 0 {
merged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts)
slices.Sort(merged)
t.Remote.TrustedHosts = slices.Compact(merged)
}
t.Verbose = cmp.Or(other.Verbose, t.Verbose)
t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
} }

View File

@@ -1,24 +1,24 @@
package taskrc package taskrc
import ( import "github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/internal/fsext"
)
type Node struct { type Node struct {
entrypoint string entrypoint string
dir string
} }
func NewNode( func NewNode(
entrypoint string, entrypoint string,
dir string, dir string,
possibleFileNames []string,
) (*Node, error) { ) (*Node, error) {
dir = fsext.DefaultDir(entrypoint, dir) dir = fsext.DefaultDir(entrypoint, dir)
resolvedEntrypoint, err := fsext.SearchPath(dir, possibleFileNames) var err error
entrypoint, dir, err = fsext.Search(entrypoint, dir, defaultTaskRCs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Node{ return &Node{
entrypoint: resolvedEntrypoint, entrypoint: entrypoint,
dir: dir,
}, nil }, nil
} }

View File

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

View File

@@ -1,92 +1,6 @@
package taskrc package taskrc
import ( var defaultTaskRCs = []string{
"os" ".taskrc.yml",
"path/filepath" ".taskrc.yaml",
"slices"
"strings"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/taskrc/ast"
)
var (
defaultXDGTaskRCs = []string{
"taskrc.yml",
"taskrc.yaml",
}
defaultTaskRCs = []string{
".taskrc.yml",
".taskrc.yaml",
}
)
// GetConfig loads and merges local and global Task configuration files
func GetConfig(dir string) (*ast.TaskRC, error) {
var config *ast.TaskRC
reader := NewReader()
// Read the XDG config file
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
xdgConfigNode, err := NewNode("", filepath.Join(xdgConfigHome, "task"), defaultXDGTaskRCs)
if err == nil && xdgConfigNode != nil {
xdgConfig, err := reader.Read(xdgConfigNode)
if err != nil {
return nil, err
}
config = xdgConfig
}
}
// If the current path does not contain $HOME
// If it does contain $HOME, then we will find this config later anyway
home, err := os.UserHomeDir()
if err == nil && !strings.Contains(home, dir) {
homeNode, err := NewNode("", home, defaultTaskRCs)
if err == nil && homeNode != nil {
homeConfig, err := reader.Read(homeNode)
if err != nil {
return nil, err
}
if config == nil {
config = homeConfig
} else {
config.Merge(homeConfig)
}
}
}
// Find all the nodes from the given directory up to the users home directory
absDir, err := filepath.Abs(dir)
if err != nil {
return config, err
}
entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs)
if err != nil {
return config, err
}
// Reverse the entrypoints since we want the child files to override parent ones
slices.Reverse(entrypoints)
// Loop over the nodes, and merge them into the main config
for _, entrypoint := range entrypoints {
node, err := NewNode("", entrypoint, defaultTaskRCs)
if err != nil {
return nil, err
}
localConfig, err := reader.Read(node)
if err != nil {
return nil, err
}
if localConfig == nil {
continue
}
if config == nil {
config = localConfig
continue
}
config.Merge(localConfig)
}
return config, nil
} }

View File

@@ -1,309 +0,0 @@
package taskrc
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/taskrc/ast"
)
const (
xdgConfigYAML = `
experiments:
FOO: 1
BAR: 1
BAZ: 1
`
homeConfigYAML = `
experiments:
FOO: 2
BAR: 2
`
localConfigYAML = `
experiments:
FOO: 3
`
)
func setupDirs(t *testing.T) (string, string, string) {
t.Helper()
xdgConfigDir := t.TempDir()
xdgTaskConfigDir := filepath.Join(xdgConfigDir, "task")
require.NoError(t, os.Mkdir(xdgTaskConfigDir, 0o755))
homeDir := t.TempDir()
localDir := filepath.Join(homeDir, "local")
require.NoError(t, os.Mkdir(localDir, 0o755))
t.Setenv("XDG_CONFIG_HOME", xdgConfigDir)
t.Setenv("HOME", homeDir)
return xdgTaskConfigDir, homeDir, localDir
}
func writeFile(t *testing.T, dir, filename, content string) {
t.Helper()
err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644)
assert.NoError(t, err)
}
func TestGetConfig_NoConfigFiles(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Nil(t, cfg)
}
func TestGetConfig_OnlyXDG(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgDir, _, localDir := setupDirs(t)
writeFile(t, xdgDir, "taskrc.yml", xdgConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 1,
"BAR": 1,
"BAZ": 1,
},
}, cfg)
}
func TestGetConfig_OnlyHome(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, homeDir, localDir := setupDirs(t)
writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 2,
"BAR": 2,
},
}, cfg)
}
func TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
writeFile(t, localDir, ".taskrc.yml", localConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 3,
},
}, cfg)
}
func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)
// Write local config
writeFile(t, localDir, ".taskrc.yml", localConfigYAML)
// Write home config
writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML)
// Write XDG config
writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 3,
"BAR": 2,
"BAZ": 1,
},
}, cfg)
}
func TestGetConfig_RemoteTrustedHosts(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
// Test with single host
configYAML := `
remote:
trusted-hosts:
- github.com
`
writeFile(t, localDir, ".taskrc.yml", configYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, []string{"github.com"}, cfg.Remote.TrustedHosts)
// Test with multiple hosts
configYAML = `
remote:
trusted-hosts:
- github.com
- gitlab.com
- example.com:8080
`
writeFile(t, localDir, ".taskrc.yml", configYAML)
cfg, err = GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, []string{"github.com", "gitlab.com", "example.com:8080"}, cfg.Remote.TrustedHosts)
}
func TestGetConfig_RemoteTrustedHostsMerge(t *testing.T) { //nolint:paralleltest // cannot run in parallel
t.Run("file-based merge precedence", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)
// XDG config has github.com and gitlab.com
xdgConfig := `
remote:
trusted-hosts:
- github.com
- gitlab.com
timeout: "30s"
`
writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfig)
// Home config has example.com (should be combined with XDG)
homeConfig := `
remote:
trusted-hosts:
- example.com
`
writeFile(t, homeDir, ".taskrc.yml", homeConfig)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
// Home config entries come first, then XDG
assert.Equal(t, []string{"example.com", "github.com", "gitlab.com"}, cfg.Remote.TrustedHosts)
// Test with local config too
localConfig := `
remote:
trusted-hosts:
- local.dev
`
writeFile(t, localDir, ".taskrc.yml", localConfig)
cfg, err = GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
// Local config entries come first
assert.Equal(t, []string{"example.com", "github.com", "gitlab.com", "local.dev"}, cfg.Remote.TrustedHosts)
})
t.Run("merge edge cases", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
tests := []struct {
name string
base *ast.TaskRC
other *ast.TaskRC
expected []string
}{
{
name: "merge hosts into empty",
base: &ast.TaskRC{},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"github.com"},
},
},
expected: []string{"github.com"},
},
{
name: "merge combines lists",
base: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"base.com"},
},
},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"other.com"},
},
},
expected: []string{"base.com", "other.com"},
},
{
name: "merge empty list does not override",
base: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"base.com"},
},
},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{},
},
},
expected: []string{"base.com"},
},
{
name: "merge nil does not override",
base: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"base.com"},
},
},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: nil,
},
},
expected: []string{"base.com"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
tt.base.Merge(tt.other)
assert.Equal(t, tt.expected, tt.base.Remote.TrustedHosts)
})
}
})
t.Run("all remote fields merge", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
insecureTrue := true
offlineTrue := true
timeout := 30 * time.Second
cacheExpiry := 1 * time.Hour
base := &ast.TaskRC{}
other := &ast.TaskRC{
Remote: ast.Remote{
Insecure: &insecureTrue,
Offline: &offlineTrue,
Timeout: &timeout,
CacheExpiry: &cacheExpiry,
TrustedHosts: []string{"github.com", "gitlab.com"},
},
}
base.Merge(other)
assert.Equal(t, &insecureTrue, base.Remote.Insecure)
assert.Equal(t, &offlineTrue, base.Remote.Offline)
assert.Equal(t, &timeout, base.Remote.Timeout)
assert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry)
assert.Equal(t, []string{"github.com", "gitlab.com"}, base.Remote.TrustedHosts)
})
}

78
temp_dir.go Normal file
View File

@@ -0,0 +1,78 @@
package task
import (
"path/filepath"
"strings"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
)
type TempDir struct {
Remote string
Fingerprint string
}
func NewTempDir(dir string) (*TempDir, error) {
tempDir, err := setupTempDirFingerprint(dir)
if err != nil {
return nil, err
}
err = setupTempDirRemote(dir, tempDir)
if err != nil {
return nil, err
}
return tempDir, nil
}
func setupTempDirFingerprint(dir string) (*TempDir, error) {
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
return &TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}, nil
}
if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return nil, err
}
projectDir, _ := filepath.Abs(dir)
projectName := filepath.Base(projectDir)
return &TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}, nil
}
return &TempDir{
Remote: filepathext.SmartJoin(dir, tempDir),
Fingerprint: filepathext.SmartJoin(dir, tempDir),
}, nil
}
func setupTempDirRemote(dir string, tempDir *TempDir) error {
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir == "" {
return nil
}
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
if err != nil {
return err
}
tempDir.Remote = remoteTempDir
return nil
}
tempDir.Remote = filepathext.SmartJoin(dir, ".task")
return nil
}

View File

@@ -12,14 +12,6 @@ tasks:
generates: generates:
- ./generated.txt - ./generated.txt
method: checksum method: checksum
build-*:
cmds:
- cp ./source.txt ./generated-{{index .MATCH 0}}.txt
sources:
- ./source.txt
generates:
- ./generated-{{index .MATCH 0}}.txt
method: checksum
build-with-status: build-with-status:
cmds: cmds:

View File

@@ -1 +0,0 @@
Hello, World!

View File

@@ -1,14 +0,0 @@
version: '3'
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -1 +0,0 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -1,3 +0,0 @@
dep1
dep2
dep3

View File

@@ -1 +0,0 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -1,15 +0,0 @@
version: '3'
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
failfast: true
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -1 +0,0 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -1,7 +0,0 @@
version: '3'
tasks:
foo:
label: "foobar"
cmds:
- "false"

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