Compare commits

..

4 Commits

Author SHA1 Message Date
Valentin Maerten
442aab1f3e ci(github): use setup-task with output grouping for tests
Restore the --output group options for better GitHub Actions log
grouping, while keeping the separate build job for compilation check.
2025-12-10 21:51:37 +01:00
Valentin Maerten
17576081b3 ci(github): merge build step into test job 2025-12-07 22:46:05 +01:00
Valentin Maerten
2cb7eaa3cc ci(github): improve workflow structure and add build job 2025-12-07 21:48:18 +01:00
Valentin Maerten
8cc70d5922 ci(github): consolidate test and lint into single workflow 2025-12-07 21:48:18 +01:00
98 changed files with 1378 additions and 4023 deletions

112
.github/workflows/ci.yml vendored Normal file
View File

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

@@ -1,43 +0,0 @@
name: Lint
on:
pull_request:
push:
tags:
- v*
branches:
- main
jobs:
lint:
name: Lint
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v6
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v6
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.8.0
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v6
with:
python-version: 3.14
- uses: actions/checkout@v6
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/src/public/schema.json

View File

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

View File

@@ -1,38 +0,0 @@
name: Test
on:
pull_request:
push:
tags:
- v*
branches:
- main
jobs:
test:
name: Test
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v6
with:
go-version: ${{matrix.go-version}}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v6
- 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::'

1
.gitignore vendored
View File

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

View File

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

View File

@@ -169,6 +169,6 @@ cloudsmiths:
rpm:
- "any-distro/any-version"
alpine:
- "alpine/any-version"
- "any-distro/any-version"
component: main
republish: true

View File

@@ -1,117 +1,39 @@
# Changelog
## v3.48.0 - 2026-01-26
- Fixed `if:` conditions when using to check dynamic variables. Also, skip
variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
@trulede).
- Included Taskfiles with `silent: true` now properly propagate silence to their
tasks, while still allowing individual tasks to override with `silent: false`
(#2640, #1319 by @trulede).
- Added TLS certificate options for Remote Taskfiles: use `--cacert` for
self-signed certificates and `--cert`/`--cert-key` for mTLS authentication
(#2537, #2242 by @vmaerten).
## v3.47.0 - 2026-01-24
- Fixed remote git Taskfiles: cloning now works without explicit ref, and
directory includes are properly resolved (#2602 by @vmaerten).
- For `output: prefixed`, print `prefix:` if set instead of task name (#1566,
#2633 by @trulede).
- Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by
@trulede).
- Task aliases can now contain wildcards and will match accordingly (e.g., `s-*`
as alias for `start-*`) (#1900, #2234 by @vmaerten).
- Added conditional execution with the `if` field: skip tasks, commands, or task
calls based on shell exit codes or template expressions like
`{{ eq .ENV "prod" }}` (#2564, #608 by @vmaerten).
- Task can now interactively prompt for missing required variables when running
in a TTY, with support for enum selection menus. Enable with `--interactive`
flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten).
## v3.46.4 - 2025-12-24
- Fixed regressions in completion script for Fish (#2591, #2604, #2592 by
@WinkelCode).
## v3.46.3 - 2025-12-19
- Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten).
## v3.46.2 - 2025-12-18
- Fixed a regression on previous release that affected variables passed via
command line (#2588, #2589 by @vmaerten).
## v3.46.1 - 2025-12-18
### ✨ Features
## 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
dependencies to finish running before continuing, even if any of them fail.
To opt for the previous behavior, set `failfast: true` either on your
`.taskrc.yml` or per task, or use the `--failfast` flag, which will also work
for `--parallel` (#1246, #2525 by @andreynering).
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- The `--summary` flag now displays `vars:` (both global and task-level),
`env:`, and `requires:` sections. Dynamic variables show their shell command
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
@vmaerten).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Improved performance of fuzzy task name matching by implementing lazy
initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option
to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).
- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`
and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to
skip confirmation prompts for specified hosts when using Remote Taskfiles
(#2491, #2473 by @maciejlech).
- When running in GitHub Actions, Task now automatically emits error annotations
on failure, improving visibility in workflow summaries (#2568 by @vmaerten).
- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`
variable (#2577, #2479 by @semihbkgr).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- Shell completion now works when Task is installed or aliased under a different
binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).
- Some small fixes and improvements were made to `task --init` and to the
default Taskfile it generates (#2433 by @andreynering).
- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to
customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).
- Zsh completion now supports zstyle verbose option to show or hide task
descriptions (#2571 by @vmaerten).
- Task now automatically enables colored output in CI environments (GitHub
Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by
@vmaerten).
- Added color taskrc option to explicitly enable or disable colored output
globally (#2569 by @vmaerten).
- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication
now works out of the box and `applyOf` is properly supported (#2512 by
@vmaerten).
### 🐛 Fixes
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or
`run: when_changed` settings (#2566, #1388 by @trulede).
- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in
root-level vars section (#2403, #2397 by @trulede, @vmaerten).
- Fixed a bug where `ignore_error` was ignored when using `task:` to call
another task (#2552, #363 by @trulede).
- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`
flag (#1574, #2574 by @vmaerten).
- Fixed Fish completion failing to parse task descriptions containing colons
(e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).
- Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when
using `for` loops in Taskfiles (#2576 by @vmaerten).
## v3.45.5 - 2025-11-11

View File

@@ -29,7 +29,6 @@ tasks:
aliases: [i]
sources:
- './**/*.go'
- go.mod
cmds:
- go install -v ./cmd/task
@@ -227,7 +226,7 @@ tasks:
- "git add --all"
- "git commit -m v{{.VERSION}}"
- "git push"
- "git tag -a v{{.VERSION}} -m v{{.VERSION}}"
- "git tag v{{.VERSION}}"
- "git push origin tag v{{.VERSION}}"
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
silent: true

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/spf13/pflag"
@@ -29,34 +28,19 @@ func main() {
Color: flags.Color,
}
if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode {
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err)
os.Exit(err.TaskExitCode())
}
if err, ok := err.(errors.TaskError); ok {
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err)
os.Exit(err.Code())
}
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err)
os.Exit(errors.CodeUnknown)
}
os.Exit(errors.CodeOk)
}
// emitCIErrorAnnotation emits an error annotation for supported CI providers.
func emitCIErrorAnnotation(err error) {
if isGA, _ := strconv.ParseBool(os.Getenv("GITHUB_ACTIONS")); !isGA {
return
}
if e, ok := err.(*errors.TaskRunError); ok {
fmt.Fprintf(os.Stdout, "::error title=Task '%s' failed::%v\n", e.TaskName, e.Err)
return
}
fmt.Fprintf(os.Stdout, "::error title=Task failed::%v\n", err)
}
func run() error {
log := &logger.Logger{
Stdout: os.Stdout,
@@ -172,23 +156,18 @@ func run() error {
calls = append(calls, &task.Call{Task: "default"})
}
// Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults
e.Taskfile.Vars.Merge(globals, nil)
// Then ReverseMerge special variables so they're available for templating
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
if err != nil {
return err
}
specialVars := ast.NewVars()
specialVars.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
specialVars.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
specialVars.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
specialVars.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
specialVars.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
specialVars.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
specialVars.Set("CLI_ASSUME_YES", ast.Var{Value: flags.AssumeYes})
e.Taskfile.Vars.ReverseMerge(specialVars, nil)
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch {
e.InterceptInterruptSignals()
}

View File

@@ -22,14 +22,10 @@ function _task()
# Handle special arguments of options.
case "$prev" in
-d|--dir|--remote-cache-dir)
-d|--dir)
_filedir -d
return $?
;;
--cacert|--cert|--cert-key)
_filedir
return $?
;;
-t|--taskfile)
_filedir yaml || return $?
_filedir yml

View File

@@ -54,7 +54,7 @@ function __task_get_tasks --description "Prints all available tasks with their d
end
# Grab names and descriptions (if any) of the tasks
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):[[:space:]]\{2,\}\(.*\)[[:space:]]\{2,\}(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):[[:space:]]\{2,\}\(.*\)/\1\t\2/'| string split0)
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
if test $output
echo $output
end
@@ -86,7 +86,6 @@ complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
@@ -107,13 +106,9 @@ complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes t
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'
# RemoteTaskfiles experiment - Options
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
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'

View File

@@ -40,7 +40,6 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
@@ -76,10 +75,6 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
# Operations
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')

View File

@@ -33,10 +33,6 @@ function __task_list() {
cmd+=(--taskfile "$taskfile")
fi
# Check if global flag is set
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
cmd+=(--global)
fi
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
enabled=1
@@ -45,27 +41,12 @@ function __task_list() {
(( enabled )) || return 0
scripts=()
# Read zstyle verbose option (default = true via -T)
local show_desc
zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false
for item in "${(@)${(f)output}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
if [[ "$show_desc" == "true" ]]; then
local desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
else
scripts+=( "$task" )
fi
desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
done
if [[ "$show_desc" == "true" ]]; then
_describe 'Task to run' scripts
else
compadd -Q -a scripts
fi
_describe 'Task to run' scripts
}
_task() {
@@ -90,7 +71,6 @@ _task() {
'(-j --json)'{-j,--json}'[format task list as JSON]'
'(--nested)--nested[nest namespaces when listing as JSON]'
'(--no-status)--no-status[ignore status when listing as JSON]'
'(--interactive)--interactive[prompt for missing required variables]'
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
@@ -116,10 +96,6 @@ _task() {
'(--offline --download)--offline[use only local or cached Taskfiles]'
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
'(--expiry)--expiry[cache expiry duration]:duration: '
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
'(--cert)--cert[client certificate for mTLS]:file:_files'
'(--cert-key)--cert-key[client certificate private key]:file:_files'
)
fi

View File

@@ -37,17 +37,12 @@ type (
TrustedHosts []string
Timeout time.Duration
CacheExpiryDuration time.Duration
RemoteCacheDir string
CACert string
Cert string
CertKey string
Watch bool
Verbose bool
Silent bool
DisableFuzzy bool
AssumeYes bool
AssumeTerm bool // Used for testing
Interactive bool
Dry bool
Summary bool
Parallel bool
@@ -74,7 +69,6 @@ type (
fuzzyModel *fuzzy.Model
fuzzyModelOnce sync.Once
promptedVars *ast.Vars // vars collected via interactive prompts
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
@@ -277,58 +271,6 @@ func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
r.CacheExpiryDuration = o.duration
}
// WithRemoteCacheDir sets the directory where remote taskfiles are cached.
func WithRemoteCacheDir(dir string) ExecutorOption {
return &remoteCacheDirOption{dir: dir}
}
type remoteCacheDirOption struct {
dir string
}
func (o *remoteCacheDirOption) ApplyToExecutor(e *Executor) {
e.RemoteCacheDir = o.dir
}
// WithCACert sets the path to a custom CA certificate for TLS connections.
func WithCACert(caCert string) ExecutorOption {
return &caCertOption{caCert: caCert}
}
type caCertOption struct {
caCert string
}
func (o *caCertOption) ApplyToExecutor(e *Executor) {
e.CACert = o.caCert
}
// WithCert sets the path to a client certificate for TLS connections.
func WithCert(cert string) ExecutorOption {
return &certOption{cert: cert}
}
type certOption struct {
cert string
}
func (o *certOption) ApplyToExecutor(e *Executor) {
e.Cert = o.cert
}
// WithCertKey sets the path to a client certificate key for TLS connections.
func WithCertKey(certKey string) ExecutorOption {
return &certKeyOption{certKey: certKey}
}
type certKeyOption struct {
certKey string
}
func (o *certKeyOption) ApplyToExecutor(e *Executor) {
e.CertKey = o.certKey
}
// WithWatch tells the [Executor] to keep running in the background and watch
// for changes to the fingerprint of the tasks that are run. When changes are
// detected, a new task run is triggered.
@@ -411,19 +353,6 @@ func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
e.AssumeTerm = o.assumeTerm
}
// WithInteractive tells the [Executor] to prompt for missing required variables.
func WithInteractive(interactive bool) ExecutorOption {
return &interactiveOption{interactive}
}
type interactiveOption struct {
interactive bool
}
func (o *interactiveOption) ApplyToExecutor(e *Executor) {
e.Interactive = o.interactive
}
// WithDry tells the [Executor] to output the commands that would be run without
// actually running them.
func WithDry(dry bool) ExecutorOption {

View File

@@ -263,23 +263,6 @@ func TestVars(t *testing.T) {
task.WithSilent(true),
),
)
NewExecutorTest(t,
WithName("cli-var-priority-default"),
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
WithTask("cli-var-priority"),
)
NewExecutorTest(t,
WithName("cli-var-priority-override"),
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
WithTask("cli-var-priority"),
WithVar("CLI_VAR", "from_cli"),
)
}
func TestRequires(t *testing.T) {
@@ -364,7 +347,6 @@ func TestSpecialVars(t *testing.T) {
// Root
"print-task",
"print-root-dir",
"print-root-taskfile",
"print-taskfile",
"print-taskfile-dir",
"print-task-dir",
@@ -718,27 +700,6 @@ func TestLabel(t *testing.T) {
)
}
func TestPrefix(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("up to date"),
WithExecutorOptions(
task.WithDir("testdata/prefix_uptodate"),
task.WithOutputStyle(ast.Output{Name: "prefixed"}),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("up to dat with no output style"),
WithExecutorOptions(
task.WithDir("testdata/prefix_uptodate"),
),
WithTask("foo"),
)
}
func TestPromptInSummary(t *testing.T) {
t.Parallel()
@@ -1060,18 +1021,6 @@ func TestIncludeChecksum(t *testing.T) {
)
}
func TestIncludeSilent(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("include-taskfile-silent"),
WithExecutorOptions(
task.WithDir("testdata/includes_silent"),
),
WithTask("default"),
)
}
func TestFailfast(t *testing.T) {
t.Parallel()
@@ -1118,69 +1067,3 @@ func TestFailfast(t *testing.T) {
)
})
}
func TestIf(t *testing.T) {
t.Parallel()
tests := []struct {
name string
task string
vars map[string]any
verbose bool
}{
// Basic command-level if
{name: "cmd-if-true", task: "cmd-if-true"},
{name: "cmd-if-false", task: "cmd-if-false"},
// Task-level if
{name: "task-if-true", task: "task-if-true"},
{name: "task-if-false", task: "task-if-false", verbose: true},
// Task call with if
{name: "task-call-if-true", task: "task-call-if-true"},
{name: "task-call-if-false", task: "task-call-if-false", verbose: true},
// Go template conditions
{name: "template-eq-true", task: "template-eq-true"},
{name: "template-eq-false", task: "template-eq-false", verbose: true},
{name: "template-ne", task: "template-ne"},
{name: "template-bool-true", task: "template-bool-true"},
{name: "template-bool-false", task: "template-bool-false"},
{name: "template-direct-true", task: "template-direct-true"},
{name: "template-direct-false", task: "template-direct-false"},
{name: "template-and", task: "template-and"},
{name: "template-or", task: "template-or"},
// CLI variable override
{name: "template-cli-var", task: "template-cli-var", vars: map[string]any{"MY_VAR": "yes"}},
// Task-level if with template
{name: "task-level-template", task: "task-level-template"},
{name: "task-level-template-false", task: "task-level-template-false", verbose: true},
// For loop with if
{name: "if-in-for-loop", task: "if-in-for-loop", verbose: true},
// Task-level if with dynamic variable
{name: "task-if-dynamic-true", task: "task-if-dynamic-true"},
{name: "task-if-dynamic-false", task: "task-if-dynamic-false", verbose: true},
}
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithExecutorOptions(
task.WithDir("testdata/if"),
task.WithSilent(true),
task.WithVerbose(test.verbose),
),
WithTask(test.task),
}
if test.vars != nil {
for k, v := range test.vars {
opts = append(opts, WithVar(k, v))
}
}
NewExecutorTest(t, opts...)
}
}

135
go.mod
View File

@@ -1,140 +1,67 @@
module github.com/go-task/task/v3
go 1.24.6
toolchain go1.25.6
go 1.24.0
require (
charm.land/bubbles/v2 v2.0.0-rc.1
charm.land/bubbletea/v2 v2.0.0-rc.2
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.23.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.7.0
github.com/go-git/go-git/v5 v5.16.4
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-getter v1.8.4
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/puzpuzpuz/xsync/v4 v4.3.0
github.com/puzpuzpuz/xsync/v4 v4.2.0
github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.8.0
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/zeebo/xxh3 v1.0.2
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/sync v0.19.0
golang.org/x/term v0.39.0
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b
golang.org/x/sync v0.18.0
golang.org/x/term v0.37.0
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b
mvdan.cc/sh/v3 v3.12.0
)
require (
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/storage v1.58.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 // indirect
github.com/charmbracelet/x/ansi v0.11.1 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.5.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // 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/ulikunitz/xz v0.5.15 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

325
go.sum
View File

@@ -1,188 +1,85 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM=
charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
charm.land/bubbletea/v2 v2.0.0-rc.2 h1:TdTbUOFzbufDJmSz/3gomL6q+fR6HwfY+P13hXQzD7k=
charm.land/bubbletea/v2 v2.0.0-rc.2/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 h1:059k1h5vvZ4ASinki9nmBguxu9Rq0UDDSa6q8LOUphk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0EofhbyKhhk=
github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 h1:7Rs87fbKJoIIxsQS8YKJYGYa0tlsDwwb0twQjV1KB+g=
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38/go.mod h1:6lfcr3MNP+kZR25sF1nQwJFuQnNYBlFy3PGX5rvslXc=
github.com/charmbracelet/x/ansi v0.11.1 h1:iXAC8SyMQDJgtcz9Jnw+HU8WMEctHzoTAETIeA3JXMk=
github.com/charmbracelet/x/ansi v0.11.1/go.mod h1:M49wjzpIujwPceJ+t5w3qh2i87+HRtHohgb5iTyepL0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -194,32 +91,28 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
@@ -229,96 +122,70 @@ github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvK
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA=
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss=
github.com/u-root/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/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/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/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.yaml.in/yaml/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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE=
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo=
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b h1:PUPnLxbDzRO9kg/03l7TZk7+ywTv7FxmOhDHOtOdOtk=
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b/go.mod h1:mencVHx2sy9XZG5wJbCA9nRUOE3zvMtoRXOmXMxH7sc=
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b h1:vTpx76nZDTP/BAGnnhEXYjM+8nPKe9+I86qCErBvjCw=
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b/go.mod h1:bDyKbUYKqkFunWmxxuSPrkYpln9QZcUsqu7W128qYW4=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

View File

@@ -10,15 +10,6 @@ type Copier[T any] interface {
DeepCopy() T
}
func Scalar[T any](orig *T) *T {
if orig == nil {
return nil
} else {
v := *orig
return &v
}
}
func Slice[T any](orig []T) []T {
if orig == nil {
return nil

View File

@@ -127,8 +127,12 @@ func ExpandFields(s string) ([]string, error) {
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
for w := range p.WordsSeq(strings.NewReader(s)) {
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
if err != nil {
return nil, err
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),

View File

@@ -5,16 +5,13 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"time"
"github.com/fatih/color"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/task/v3/taskrc"
@@ -82,11 +79,6 @@ var (
ClearCache bool
Timeout time.Duration
CacheExpiryDuration time.Duration
RemoteCacheDir string
CACert string
Cert string
CertKey string
Interactive bool
)
func init() {
@@ -136,7 +128,6 @@ func init() {
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.BoolVar(&Interactive, "interactive", getConfig(config, func() *bool { return config.Interactive }, false), "Prompt for missing required variables.")
pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
@@ -147,7 +138,7 @@ func init() {
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", getConfig(config, func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.")
@@ -166,39 +157,12 @@ func init() {
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).")
pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", config.Remote.TrustedHosts, "List of trusted hosts for remote Taskfiles (comma-separated).")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&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.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
pflag.StringVar(&CACert, "cacert", getConfig(config, func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
pflag.StringVar(&Cert, "cert", getConfig(config, func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
pflag.StringVar(&CertKey, "cert-key", getConfig(config, func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
}
pflag.Parse()
// Auto-detect color based on environment when not explicitly configured
// Priority: CLI flag > taskrc config > NO_COLOR > FORCE_COLOR/CI > default
colorExplicitlySet := pflag.Lookup("color").Changed || (config != nil && config.Color != nil)
if !colorExplicitlySet {
if os.Getenv("NO_COLOR") != "" {
Color = false
color.NoColor = true
} else if os.Getenv("FORCE_COLOR") != "" || isCI() {
Color = true
color.NoColor = false // Force colors even without TTY
}
// Otherwise, let fatih/color auto-detect TTY
} else {
// Explicit config: sync with fatih/color
color.NoColor = !Color
}
}
// isCI returns true if running in a CI environment
func isCI() bool {
ci, _ := strconv.ParseBool(os.Getenv("CI"))
return ci
}
func Validate() error {
@@ -242,11 +206,6 @@ func Validate() error {
return errors.New("task: --nested only applies to --json with --list or --list-all")
}
// Validate certificate flags
if (Cert != "" && CertKey == "") || (Cert == "" && CertKey != "") {
return errors.New("task: --cert and --cert-key must be provided together")
}
return nil
}
@@ -288,16 +247,11 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithTrustedHosts(TrustedHosts),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithRemoteCacheDir(RemoteCacheDir),
task.WithCACert(CACert),
task.WithCert(Cert),
task.WithCertKey(CertKey),
task.WithWatch(Watch),
task.WithVerbose(Verbose),
task.WithSilent(Silent),
task.WithDisableFuzzy(DisableFuzzy),
task.WithAssumeYes(AssumeYes),
task.WithInteractive(Interactive),
task.WithDry(Dry || Status),
task.WithSummary(Summary),
task.WithParallel(Parallel),

View File

@@ -1,211 +0,0 @@
package input
import (
"fmt"
"io"
"strings"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/go-task/task/v3/errors"
)
var ErrCancelled = errors.New("prompt cancelled")
var (
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true) // green bold
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray
)
// Prompter handles interactive variable prompting
type Prompter struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
// Text prompts the user for a text value
func (p *Prompter) Text(varName string) (string, error) {
m := newTextModel(varName)
prog := tea.NewProgram(m,
tea.WithInput(p.Stdin),
tea.WithOutput(p.Stderr),
)
result, err := prog.Run()
if err != nil {
return "", err
}
model := result.(textModel)
if model.cancelled {
return "", ErrCancelled
}
return model.value, nil
}
// Select prompts the user to select from a list of options
func (p *Prompter) Select(varName string, options []string) (string, error) {
if len(options) == 0 {
return "", errors.New("no options provided")
}
m := newSelectModel(varName, options)
prog := tea.NewProgram(m,
tea.WithInput(p.Stdin),
tea.WithOutput(p.Stderr),
)
result, err := prog.Run()
if err != nil {
return "", err
}
model := result.(selectModel)
if model.cancelled {
return "", ErrCancelled
}
return model.options[model.cursor], nil
}
// Prompt prompts for a variable value, using Select if enum is provided, Text otherwise
func (p *Prompter) Prompt(varName string, enum []string) (string, error) {
if len(enum) > 0 {
return p.Select(varName, enum)
}
return p.Text(varName)
}
// textModel is the Bubble Tea model for text input
type textModel struct {
varName string
textInput textinput.Model
value string
cancelled bool
done bool
}
func newTextModel(varName string) textModel {
ti := textinput.New()
ti.Placeholder = ""
ti.CharLimit = 256
ti.SetWidth(40)
ti.Focus()
return textModel{
varName: varName,
textInput: ti,
}
}
func (m textModel) Init() tea.Cmd {
return tea.Batch(m.textInput.Focus(), textinput.Blink)
}
func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.Keystroke() {
case "ctrl+c", "escape":
m.cancelled = true
m.done = true
return m, tea.Quit
case "enter":
m.value = m.textInput.Value()
m.done = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
func (m textModel) View() tea.View {
if m.done {
return tea.NewView("")
}
prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", m.varName))
return tea.NewView(prompt + m.textInput.View() + "\n")
}
// selectModel is the Bubble Tea model for selection
type selectModel struct {
varName string
options []string
cursor int
cancelled bool
done bool
}
func newSelectModel(varName string, options []string) selectModel {
return selectModel{
varName: varName,
options: options,
cursor: 0,
}
}
func (m selectModel) Init() tea.Cmd {
return nil
}
func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.Keystroke() {
case "ctrl+c", "escape":
m.cancelled = true
m.done = true
return m, tea.Quit
case "up", "shift+tab", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "tab", "j":
if m.cursor < len(m.options)-1 {
m.cursor++
}
case "enter":
m.done = true
return m, tea.Quit
}
}
return m, nil
}
func (m selectModel) View() tea.View {
if m.done {
return tea.NewView("")
}
var b strings.Builder
b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", m.varName)))
b.WriteString("\n")
for i, opt := range m.options {
if i == m.cursor {
b.WriteString(cursorStyle.Render(" "))
b.WriteString(selectedStyle.Render(opt))
} else {
b.WriteString(" " + opt)
}
b.WriteString("\n")
}
b.WriteString(dimStyle.Render(" (↑/↓ to move, enter to select, esc to cancel)"))
return tea.NewView(b.String())
}

View File

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

View File

@@ -1 +1 @@
3.48.0
3.45.5

View File

@@ -4,180 +4,35 @@ import (
"slices"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/input"
"github.com/go-task/task/v3/internal/term"
"github.com/go-task/task/v3/taskfile/ast"
)
func (e *Executor) canPrompt() bool {
return e.Interactive && (e.AssumeTerm || term.IsTerminal())
}
func (e *Executor) newPrompter() *input.Prompter {
return &input.Prompter{
Stdin: e.Stdin,
Stdout: e.Stdout,
Stderr: e.Stderr,
}
}
// promptDepsVars traverses the dependency tree, collects all missing required
// variables, and prompts for them upfront. This is used for deps which execute
// in parallel, so all prompts must happen before execution to avoid interleaving.
// Prompted values are stored in e.promptedVars for injection into task calls.
func (e *Executor) promptDepsVars(calls []*Call) error {
if !e.canPrompt() {
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
}
// Collect all missing vars from the dependency tree
visited := make(map[string]bool)
varsMap := make(map[string]*ast.VarsWithValidation)
var collect func(call *Call) error
collect = func(call *Call) error {
compiledTask, err := e.FastCompiledTask(call)
if err != nil {
return err
}
for _, v := range getMissingRequiredVars(compiledTask) {
if _, exists := varsMap[v.Name]; !exists {
varsMap[v.Name] = v
}
}
// Check visited AFTER collecting vars to handle duplicate task calls with different vars
if visited[call.Task] {
return nil
}
visited[call.Task] = true
for _, dep := range compiledTask.Deps {
depCall := &Call{
Task: dep.Task,
Vars: dep.Vars,
Silent: dep.Silent,
}
if err := collect(depCall); err != nil {
return err
}
}
return nil
}
for _, call := range calls {
if err := collect(call); err != nil {
return err
var missingVars []errors.MissingVar
for _, requiredVar := range t.Requires.Vars {
_, ok := t.Vars.Get(requiredVar.Name)
if !ok {
missingVars = append(missingVars, errors.MissingVar{
Name: requiredVar.Name,
AllowedValues: requiredVar.Enum,
})
}
}
if len(varsMap) == 0 {
return nil
}
prompter := e.newPrompter()
e.promptedVars = ast.NewVars()
for _, v := range varsMap {
value, err := prompter.Prompt(v.Name, v.Enum)
if err != nil {
if errors.Is(err, input.ErrCancelled) {
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
}
return err
if len(missingVars) > 0 {
return &errors.TaskMissingRequiredVarsError{
TaskName: t.Name(),
MissingVars: missingVars,
}
e.promptedVars.Set(v.Name, ast.Var{Value: value})
}
return nil
}
// promptTaskVars prompts for any missing required vars from a single task.
// Used for sequential task calls (cmds) where we can prompt just-in-time.
// Returns true if any vars were prompted (caller should recompile the task).
func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
if !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 {
return false, nil
}
// Find missing vars, excluding already prompted ones
var missing []*ast.VarsWithValidation
for _, v := range getMissingRequiredVars(t) {
if e.promptedVars != nil {
if _, ok := e.promptedVars.Get(v.Name); ok {
continue
}
}
missing = append(missing, v)
}
if len(missing) == 0 {
return false, nil
}
prompter := e.newPrompter()
for _, v := range missing {
value, err := prompter.Prompt(v.Name, v.Enum)
if err != nil {
if errors.Is(err, input.ErrCancelled) {
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
}
return false, err
}
// Add to call.Vars for recompilation
if call.Vars == nil {
call.Vars = ast.NewVars()
}
call.Vars.Set(v.Name, ast.Var{Value: value})
// Cache for reuse by other tasks
if e.promptedVars == nil {
e.promptedVars = ast.NewVars()
}
e.promptedVars.Set(v.Name, ast.Var{Value: value})
}
return true, nil
}
// getMissingRequiredVars returns required vars that are not set in the task's vars.
func getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation {
if t.Requires == nil {
return nil
}
var missing []*ast.VarsWithValidation
for _, v := range t.Requires.Vars {
if _, ok := t.Vars.Get(v.Name); !ok {
missing = append(missing, v)
}
}
return missing
}
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
missing := getMissingRequiredVars(t)
if len(missing) == 0 {
return nil
}
missingVars := make([]errors.MissingVar, len(missing))
for i, v := range missing {
missingVars[i] = errors.MissingVar{
Name: v.Name,
AllowedValues: v.Enum,
}
}
return &errors.TaskMissingRequiredVarsError{
TaskName: t.Name(),
MissingVars: missingVars,
}
}
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
@@ -195,6 +50,7 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
Name: requiredVar.Name,
})
}
}
if len(notAllowedValuesVars) > 0 {

View File

@@ -55,11 +55,7 @@ func (e *Executor) Setup() error {
}
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout,
taskfile.WithCACert(e.CACert),
taskfile.WithCert(e.Cert),
taskfile.WithCertKey(e.CertKey),
)
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),
@@ -71,7 +67,6 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
return nil, err
}
e.Dir = node.Dir()
e.Entrypoint = node.Location()
return node, err
}
@@ -91,9 +86,6 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
taskfile.WithTrustedHosts(e.TrustedHosts),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithReaderCACert(e.CACert),
taskfile.WithReaderCert(e.Cert),
taskfile.WithReaderCertKey(e.CertKey),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
@@ -161,16 +153,16 @@ func (e *Executor) setupTempDir() error {
}
}
// RemoteCacheDir from taskrc/env can override the remote cache directory
if e.RemoteCacheDir != "" {
if filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, "~") {
remoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir)
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 = remoteCacheDir
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
}

163
task.go
View File

@@ -6,7 +6,6 @@ import (
"os"
"runtime"
"slices"
"strings"
"sync/atomic"
"golang.org/x/sync/errgroup"
@@ -74,11 +73,6 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
return nil
}
// Prompt for all required vars from deps upfront (parallel execution)
if err := e.promptDepsVars(calls); err != nil {
return err
}
regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
if err != nil {
return err
@@ -126,19 +120,6 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call *Call) error {
// Inject prompted vars into call if available
if e.promptedVars != nil {
if call.Vars == nil {
call.Vars = ast.NewVars()
}
for name, v := range e.promptedVars.All() {
// Only inject if not already set in call
if _, ok := call.Vars.Get(name); !ok {
call.Vars.Set(name, v)
}
}
}
t, err := e.FastCompiledTask(call)
if err != nil {
return err
@@ -148,12 +129,8 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return nil
}
// Check required vars early (before template compilation) if we can't prompt.
// This gives a clear "missing required variables" error instead of a template error.
if !e.canPrompt() {
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
t, err = e.CompiledTask(call)
@@ -161,35 +138,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return err
}
// Check if condition after CompiledTask so dynamic variables are resolved
if strings.TrimSpace(t.If) != "" {
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: t.If,
Dir: t.Dir,
Env: env.Get(t),
}); err != nil {
e.Logger.VerboseOutf(logger.Yellow, "task: if condition not met - skipped: %q\n", call.Task)
return nil
}
}
// Prompt for missing required vars after if check (avoid prompting if task won't run)
prompted, err := e.promptTaskVars(t, call)
if err != nil {
return err
}
if prompted {
// Recompile with the new vars
t, err = e.FastCompiledTask(call)
if err != nil {
return err
}
}
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
return err
}
@@ -237,12 +185,8 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
}
if upToDate && preCondMet {
if e.Verbose || (!call.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
name := t.Name()
if e.OutputStyle.Name == "prefixed" {
name = t.Prefix
}
e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", name)
if e.Verbose || (!call.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", t.Name())
}
return nil
}
@@ -351,7 +295,6 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
if err := e.runCommand(ctx, t, call, i); err != nil {
@@ -362,37 +305,23 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {
cmd := t.Cmds[i]
// Check if condition for any command type
if strings.TrimSpace(cmd.If) != "" {
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.If,
Dir: t.Dir,
Env: env.Get(t),
}); err != nil {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] if condition not met - skipped\n", t.Name())
return nil
}
}
switch {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
return nil
if err != nil {
return err
}
return err
return nil
case cmd.Cmd != "":
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
return nil
}
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
}
@@ -441,7 +370,7 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
return err
}
if h == "" || t.Watch {
if h == "" {
return execute(ctx)
}
@@ -469,40 +398,19 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
}
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name, or one of aliases, or if it matches
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
// If multiple tasks match due to aliases, a TaskNameConflictError is returned.
func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil {
return nil, nil
return nil
}
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks, nil
return matchingTasks
}
var aliasedTasks []string
for task := range e.Taskfile.Tasks.Values(nil) {
if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task)
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
}
}
if len(aliasedTasks) == 1 {
return matchingTasks, nil
}
// If we found multiple tasks
if len(aliasedTasks) > 1 {
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: aliasedTasks,
}
}
// Attempt a wildcard match
for _, value := range e.Taskfile.Tasks.All(nil) {
if match, wildcards := value.WildcardMatch(call.Task); match {
@@ -512,7 +420,7 @@ func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
})
}
}
return matchingTasks, nil
return matchingTasks
}
// GetTask will return the task with the name matching the given call from the taskfile.
@@ -520,11 +428,7 @@ func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
// If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// Search for a matching task
matchingTasks, err := e.FindMatchingTasks(call)
if err != nil {
return nil, err
}
matchingTasks := e.FindMatchingTasks(call)
if len(matchingTasks) > 0 {
if call.Vars == nil {
call.Vars = ast.NewVars()
@@ -533,18 +437,37 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
return matchingTasks[0].Task, nil
}
// If we found no tasks
didYouMean := ""
if !e.DisableFuzzy {
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
// If didn't find one, search for a task with a matching alias
var matchingTask *ast.Task
var aliasedTasks []string
for task := range e.Taskfile.Tasks.Values(nil) {
if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task)
matchingTask = task
}
}
return nil, &errors.TaskNotFoundError{
TaskName: call.Task,
DidYouMean: didYouMean,
// If we found multiple tasks
if len(aliasedTasks) > 1 {
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: aliasedTasks,
}
}
// If we found no tasks
if len(aliasedTasks) == 0 {
didYouMean := ""
if !e.DisableFuzzy {
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
}
}
return nil, &errors.TaskNotFoundError{
TaskName: call.Task,
DidYouMean: didYouMean,
}
}
return matchingTask, nil
}
type FilterFunc func(task *ast.Task) bool

View File

@@ -2612,11 +2612,6 @@ func TestWildcard(t *testing.T) {
call: "start-foo",
expectedOutput: "Starting foo\n",
},
{
name: "alias",
call: "s-foo",
expectedOutput: "Starting foo\n",
},
{
name: "matches exactly",
call: "matches-exactly-*",

View File

@@ -12,7 +12,6 @@ type Cmd struct {
Cmd string
Task string
For *For
If string
Silent bool
Set []string
Shopt []string
@@ -30,7 +29,6 @@ func (c *Cmd) DeepCopy() *Cmd {
Cmd: c.Cmd,
Task: c.Task,
For: c.For.DeepCopy(),
If: c.If,
Silent: c.Silent,
Set: deepcopy.Slice(c.Set),
Shopt: deepcopy.Slice(c.Shopt),
@@ -57,7 +55,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
Cmd string
Task string
For *For
If string
Silent bool
Set []string
Shopt []string
@@ -95,9 +92,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
c.Task = cmdStruct.Task
c.Vars = cmdStruct.Vars
c.For = cmdStruct.For
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
return nil
}
@@ -105,7 +100,6 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
if cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.For = cmdStruct.For
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt

View File

@@ -32,7 +32,7 @@ type Task struct {
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool
Silent bool
Interactive bool
Internal bool
Method string
@@ -40,7 +40,6 @@ type Task struct {
IgnoreError bool
Run string
Platforms []*Platform
If string
Watch bool
Location *Location
Failfast bool
@@ -69,37 +68,28 @@ func (t *Task) LocalName() string {
return name
}
// IsSilent returns true if the task has silent mode explicitly enabled.
// Returns false if Silent is nil (not set) or explicitly set to false.
func (t *Task) IsSilent() bool {
return t.Silent != nil && *t.Silent
}
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
func (t *Task) WildcardMatch(name string) (bool, []string) {
names := append([]string{t.Task}, t.Aliases...)
// Convert the name into a regex string
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(t.Task, "*", "(.*)"))
regex := regexp.MustCompile(regexStr)
wildcards := regex.FindStringSubmatch(name)
wildcardCount := strings.Count(t.Task, "*")
for _, taskName := range names {
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)"))
regex := regexp.MustCompile(regexStr)
wildcards := regex.FindStringSubmatch(name)
if len(wildcards) == 0 {
continue
}
// Remove the first match, which is the full string
wildcards = wildcards[1:]
wildcardCount := strings.Count(taskName, "*")
if len(wildcards) != wildcardCount {
continue
}
return true, wildcards
// If there are no wildcards, return false
if len(wildcards) == 0 {
return false, nil
}
return false, nil
// Remove the first match, which is the full string
wildcards = wildcards[1:]
// If there are more/less wildcards than matches, return false
if len(wildcards) != wildcardCount {
return false, wildcards
}
return true, wildcards
}
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
@@ -144,7 +134,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Vars *Vars
Env *Vars
Dotenv []string
Silent *bool `yaml:"silent,omitempty"`
Silent bool
Interactive bool
Internal bool
Method string
@@ -152,7 +142,6 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
IgnoreError bool `yaml:"ignore_error"`
Run string
Platforms []*Platform
If string
Requires *Requires
Watch bool
Failfast bool
@@ -184,7 +173,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Vars = task.Vars
t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = deepcopy.Scalar(task.Silent)
t.Silent = task.Silent
t.Interactive = task.Interactive
t.Internal = task.Internal
t.Method = task.Method
@@ -192,7 +181,6 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.IgnoreError = task.IgnoreError
t.Run = task.Run
t.Platforms = task.Platforms
t.If = task.If
t.Requires = task.Requires
t.Watch = task.Watch
t.Failfast = task.Failfast
@@ -227,7 +215,7 @@ func (t *Task) DeepCopy() *Task {
Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(),
Dotenv: deepcopy.Slice(t.Dotenv),
Silent: deepcopy.Scalar(t.Silent),
Silent: t.Silent,
Interactive: t.Interactive,
Internal: t.Internal,
Method: t.Method,
@@ -237,7 +225,6 @@ func (t *Task) DeepCopy() *Task {
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
Platforms: deepcopy.Slice(t.Platforms),
If: t.If,
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,

View File

@@ -59,14 +59,6 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Tasks == nil {
t1.Tasks = NewTasks()
}
if t2.Silent {
for _, t := range t2.Tasks.All(nil) {
if t.Silent == nil {
v := true
t.Silent = &v
}
}
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)

View File

@@ -118,7 +118,7 @@ func (vars *Vars) ToCacheMap() (m map[string]any) {
// Merge loops over other and merges it values with the variables in vars. If
// the include parameter is not nil and its it is an advanced import, the
// directory is set to the value of the include parameter.
// directory is set set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil {
return
@@ -133,35 +133,6 @@ func (vars *Vars) Merge(other *Vars, include *Include) {
}
}
// ReverseMerge merges other variables with the existing variables in vars, but
// keeps the other variables first in order. If the include parameter is not
// nil and it is an advanced import, the directory is set to the value of the
// include parameter.
func (vars *Vars) ReverseMerge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil || other.om == nil {
return
}
newOM := orderedmap.NewOrderedMap[string, Var]()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
val := pair.Value
if include != nil && include.AdvancedImport {
val.Dir = include.Dir
}
newOM.Set(pair.Key, val)
}
other.mutex.RUnlock()
vars.mutex.Lock()
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
newOM.Set(pair.Key, pair.Value)
}
vars.om = newOM
vars.mutex.Unlock()
}
func (vs *Vars) DeepCopy() *Vars {
if vs == nil {
return nil

View File

@@ -34,14 +34,13 @@ func NewRootNode(
dir string,
insecure bool,
timeout time.Duration,
opts ...NodeOption,
) (Node, error) {
dir = fsext.DefaultDir(entrypoint, dir)
// If the entrypoint is "-", we read from stdin
if entrypoint == "-" {
return NewStdinNode(dir)
}
return NewNode(entrypoint, dir, insecure, opts...)
return NewNode(entrypoint, dir, insecure)
}
func NewNode(

View File

@@ -10,9 +10,6 @@ type (
parent Node
dir string
checksum string
caCert string
cert string
certKey string
}
)
@@ -57,21 +54,3 @@ func (node *baseNode) Checksum() string {
func (node *baseNode) Verify(checksum string) bool {
return node.checksum == "" || node.checksum == checksum
}
func WithCACert(caCert string) NodeOption {
return func(node *baseNode) {
node.caCert = caCert
}
}
func WithCert(cert string) NodeOption {
return func(node *baseNode) {
node.cert = cert
}
}
func WithCertKey(certKey string) NodeOption {
return func(node *baseNode) {
node.certKey = certKey
}
}

View File

@@ -3,19 +3,20 @@ package taskfile
import (
"context"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
giturls "github.com/chainguard-dev/git-urls"
"github.com/hashicorp/go-getter"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
)
// An GitNode is a node that reads a Taskfile from a remote location via Git.
@@ -27,36 +28,6 @@ type GitNode struct {
path string
}
type gitRepoCache struct {
mu sync.Mutex // Protects the locks map
locks map[string]*sync.Mutex // One mutex per repo cache key
}
func (c *gitRepoCache) getLockForRepo(cacheKey string) *sync.Mutex {
c.mu.Lock()
defer c.mu.Unlock()
if _, exists := c.locks[cacheKey]; !exists {
c.locks[cacheKey] = &sync.Mutex{}
}
return c.locks[cacheKey]
}
var globalGitRepoCache = &gitRepoCache{
locks: make(map[string]*sync.Mutex),
}
func CleanGitCache() error {
// Clear the in-memory locks map to prevent memory leak
globalGitRepoCache.mu.Lock()
globalGitRepoCache.locks = make(map[string]*sync.Mutex)
globalGitRepoCache.mu.Unlock()
cacheDir := filepath.Join(os.TempDir(), "task-git-repos")
return os.RemoveAll(cacheDir)
}
func NewGitNode(
entrypoint string,
dir string,
@@ -101,83 +72,24 @@ func (node *GitNode) Read() ([]byte, error) {
return node.ReadContext(context.Background())
}
func (node *GitNode) buildURL() string {
// Get the base URL
baseURL := node.url.String()
// Always use git:: prefix for git URLs (following Terraform's pattern)
// This forces go-getter to use git protocol
if node.ref != "" {
return fmt.Sprintf("git::%s?ref=%s&depth=1", baseURL, node.ref)
}
// When no ref is specified, omit it entirely to let git clone the default branch
return fmt.Sprintf("git::%s?depth=1", baseURL)
}
// getOrCloneRepo returns the path to a cached git repository.
// If the repository is not cached, it clones it first.
// This function is thread-safe: multiple goroutines cloning the same repo+ref
// will synchronize, and only one clone operation will occur.
//
// The cache directory is /tmp/task-git-repos/{cache_key}/
func (node *GitNode) getOrCloneRepo(ctx context.Context) (string, error) {
cacheKey := node.repoCacheKey()
repoMutex := globalGitRepoCache.getLockForRepo(cacheKey)
repoMutex.Lock()
defer repoMutex.Unlock()
// Check if context was cancelled while waiting for lock
if err := ctx.Err(); err != nil {
return "", fmt.Errorf("context cancelled while waiting for repository lock: %w", err)
}
cacheDir := filepath.Join(os.TempDir(), "task-git-repos", cacheKey)
// check if repo is already cached (under the lock)
gitDir := filepath.Join(cacheDir, ".git")
if _, err := os.Stat(gitDir); err == nil {
return cacheDir, nil
}
getterURL := node.buildURL()
client := &getter.Client{
Ctx: ctx,
Src: getterURL,
Dst: cacheDir,
Mode: getter.ClientModeDir,
}
if err := client.Get(); err != nil {
_ = os.RemoveAll(cacheDir)
return "", fmt.Errorf("failed to clone repository: %w", err)
}
return cacheDir, nil
}
func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
// Get or clone the repository into cache
repoDir, err := node.getOrCloneRepo(ctx)
func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.url.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
})
if err != nil {
return nil, err
}
// Build path to Taskfile in the cached repo
// If node.path is empty, search in repo root; otherwise search in the specified path
// fsext.SearchPath handles both files and directories (searching for DefaultTaskfiles)
searchPath := repoDir
if node.path != "" {
searchPath = filepath.Join(repoDir, node.path)
}
filePath, err := fsext.SearchPath(searchPath, DefaultTaskfiles)
file, err := fs.Open(node.path)
if err != nil {
return nil, err
}
// Read file from cached repo
b, err := os.ReadFile(filePath)
// Read the entire response body
b, err := io.ReadAll(file)
if err != nil {
return nil, err
}
@@ -226,22 +138,6 @@ func (node *GitNode) CacheKey() string {
return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum)
}
// repoCacheKey generates a unique cache key for the repository+ref combination.
// Unlike CacheKey() which includes the file path, this identifies the repository itself.
// Two GitNodes with the same repo+ref but different file paths will share the same cache.
//
// Returns a path like: github.com/user/repo.git/main
func (node *GitNode) repoCacheKey() string {
repoPath := strings.Trim(node.url.Path, "/")
ref := node.ref
if ref == "" {
ref = "_default_" // Placeholder for the remote's default branch
}
return filepath.Join(node.url.Host, repoPath, ref)
}
func splitURLOnDoubleSlash(u *url.URL) (string, string) {
x := strings.Split(u.Path, "//")
switch len(x) {

View File

@@ -102,146 +102,3 @@ func TestGitNode_CacheKey(t *testing.T) {
assert.Equal(t, tt.expectedKey, key)
}
}
func TestGitNode_buildURL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
entrypoint string
expectedURL string
}{
{
name: "HTTPS with ref",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
expectedURL: "git::https://github.com/foo/bar.git?ref=main&depth=1",
},
{
name: "SSH with ref",
entrypoint: "git@github.com:foo/bar.git//Taskfile.yml?ref=main",
expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=main&depth=1",
},
{
name: "HTTPS with tag ref",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=v1.0.0",
expectedURL: "git::https://github.com/foo/bar.git?ref=v1.0.0&depth=1",
},
{
name: "HTTPS without ref (uses remote default branch)",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml",
expectedURL: "git::https://github.com/foo/bar.git?depth=1",
},
{
name: "SSH with directory path",
entrypoint: "git@github.com:foo/bar.git//directory/Taskfile.yml?ref=dev",
expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=dev&depth=1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
node, err := NewGitNode(tt.entrypoint, "", false)
require.NoError(t, err)
gotURL := node.buildURL()
assert.Equal(t, tt.expectedURL, gotURL)
})
}
}
func TestRepoCacheKey_SameRepoSameRef(t *testing.T) {
t.Parallel()
// Same repo, same ref, different files should have SAME cache key
node1, err := NewGitNode("https://github.com/foo/bar.git//file1.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//dir/file2.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.Equal(t, key1, key2, "Same repo+ref should generate same cache key regardless of file path")
}
func TestRepoCacheKey_SameRepoDifferentRef(t *testing.T) {
t.Parallel()
// Same repo, different ref should have DIFFERENT cache keys
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=dev", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.NotEqual(t, key1, key2, "Different refs should generate different cache keys")
}
func TestRepoCacheKey_DifferentRepos(t *testing.T) {
t.Parallel()
// Different repos should have DIFFERENT cache keys
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/other.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.NotEqual(t, key1, key2, "Different repos should generate different cache keys")
}
func TestRepoCacheKey_NoRefVsExplicitRef(t *testing.T) {
t.Parallel()
// No ref (uses default branch) vs explicit ref should have DIFFERENT cache keys
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.NotEqual(t, key1, key2, "No ref and explicit ref should generate different cache keys")
}
func TestRepoCacheKey_SSHvsHTTPS(t *testing.T) {
t.Parallel()
// SSH vs HTTPS pointing to same repo should have SAME cache key
// They clone the same repo, so we want to share the cache
node1, err := NewGitNode("git@github.com:foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.Equal(t, key1, key2, "SSH and HTTPS for same repo should share cache")
}
func TestRepoCacheKey_Consistency(t *testing.T) {
t.Parallel()
// Calling repoCacheKey multiple times on same node should return same key
node, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node.repoCacheKey()
key2 := node.repoCacheKey()
key3 := node.repoCacheKey()
assert.Equal(t, key1, key2)
assert.Equal(t, key2, key3)
}

View File

@@ -2,13 +2,10 @@ package taskfile
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -20,54 +17,7 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*baseNode
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
client *http.Client // HTTP client with optional TLS configuration
}
// buildHTTPClient creates an HTTP client with optional TLS configuration.
// If no certificate options are provided, it returns http.DefaultClient.
func buildHTTPClient(insecure bool, caCert, cert, certKey string) (*http.Client, error) {
// Validate that cert and certKey are provided together
if (cert != "" && certKey == "") || (cert == "" && certKey != "") {
return nil, fmt.Errorf("both --cert and --cert-key must be provided together")
}
// If no TLS customization is needed, return the default client
if !insecure && caCert == "" && cert == "" {
return http.DefaultClient, nil
}
tlsConfig := &tls.Config{
InsecureSkipVerify: insecure,
}
// Load custom CA certificate if provided
if caCert != "" {
caCertData, err := os.ReadFile(caCert)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCertData) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
tlsConfig.RootCAs = caCertPool
}
// Load client certificate and key if provided
if cert != "" && certKey != "" {
clientCert, err := tls.LoadX509KeyPair(cert, certKey)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{clientCert}
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}, nil
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
}
func NewHTTPNode(
@@ -84,16 +34,9 @@ func NewHTTPNode(
if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}
}
client, err := buildHTTPClient(insecure, base.caCert, base.cert, base.certKey)
if err != nil {
return nil, err
}
return &HTTPNode{
baseNode: base,
url: url,
client: client,
}, nil
}
@@ -106,7 +49,7 @@ func (node *HTTPNode) Read() ([]byte, error) {
}
func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, *node.url, node.client)
url, err := RemoteExists(ctx, *node.url)
if err != nil {
return nil, err
}
@@ -115,7 +58,7 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
}
resp, err := node.client.Do(req.WithContext(ctx))
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
if ctx.Err() != nil {
return nil, err

View File

@@ -1,18 +1,7 @@
package taskfile
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -58,227 +47,3 @@ func TestHTTPNode_CacheKey(t *testing.T) {
assert.Equal(t, tt.expectedKey, key)
}
}
func TestBuildHTTPClient_Default(t *testing.T) {
t.Parallel()
// When no TLS customization is needed, should return http.DefaultClient
client, err := buildHTTPClient(false, "", "", "")
require.NoError(t, err)
assert.Equal(t, http.DefaultClient, client)
}
func TestBuildHTTPClient_Insecure(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(true, "", "", "")
require.NoError(t, err)
require.NotNil(t, client)
assert.NotEqual(t, http.DefaultClient, client)
// Check that InsecureSkipVerify is set
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
}
func TestBuildHTTPClient_CACert(t *testing.T) {
t.Parallel()
// Create a temporary CA cert file
tempDir := t.TempDir()
caCertPath := filepath.Join(tempDir, "ca.crt")
// Generate a valid CA certificate
caCertPEM := generateTestCACert(t)
err := os.WriteFile(caCertPath, caCertPEM, 0o600)
require.NoError(t, err)
client, err := buildHTTPClient(false, caCertPath, "", "")
require.NoError(t, err)
require.NotNil(t, client)
assert.NotEqual(t, http.DefaultClient, client)
// Check that custom RootCAs is set
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
}
func TestBuildHTTPClient_CACertNotFound(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "/nonexistent/ca.crt", "", "")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "failed to read CA certificate")
}
func TestBuildHTTPClient_CACertInvalid(t *testing.T) {
t.Parallel()
// Create a temporary file with invalid content
tempDir := t.TempDir()
caCertPath := filepath.Join(tempDir, "invalid.crt")
err := os.WriteFile(caCertPath, []byte("not a valid certificate"), 0o600)
require.NoError(t, err)
client, err := buildHTTPClient(false, caCertPath, "", "")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "failed to parse CA certificate")
}
func TestBuildHTTPClient_CertWithoutKey(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "", "/path/to/cert.crt", "")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together")
}
func TestBuildHTTPClient_KeyWithoutCert(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "", "", "/path/to/key.pem")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together")
}
func TestBuildHTTPClient_CertAndKey(t *testing.T) {
t.Parallel()
// Create temporary cert and key files
tempDir := t.TempDir()
certPath := filepath.Join(tempDir, "client.crt")
keyPath := filepath.Join(tempDir, "client.key")
// Generate a self-signed certificate and key for testing
cert, key := generateTestCertAndKey(t)
err := os.WriteFile(certPath, cert, 0o600)
require.NoError(t, err)
err = os.WriteFile(keyPath, key, 0o600)
require.NoError(t, err)
client, err := buildHTTPClient(false, "", certPath, keyPath)
require.NoError(t, err)
require.NotNil(t, client)
assert.NotEqual(t, http.DefaultClient, client)
// Check that client certificate is set
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.Len(t, transport.TLSClientConfig.Certificates, 1)
}
func TestBuildHTTPClient_CertNotFound(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "", "/nonexistent/cert.crt", "/nonexistent/key.pem")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "failed to load client certificate")
}
func TestBuildHTTPClient_InsecureWithCACert(t *testing.T) {
t.Parallel()
// Create a temporary CA cert file
tempDir := t.TempDir()
caCertPath := filepath.Join(tempDir, "ca.crt")
// Generate a valid CA certificate
caCertPEM := generateTestCACert(t)
err := os.WriteFile(caCertPath, caCertPEM, 0o600)
require.NoError(t, err)
// Both insecure and CA cert can be set together
client, err := buildHTTPClient(true, caCertPath, "", "")
require.NoError(t, err)
require.NotNil(t, client)
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
}
// generateTestCertAndKey generates a self-signed certificate and key for testing
func generateTestCertAndKey(t *testing.T) (certPEM, keyPEM []byte) {
t.Helper()
// Generate a new ECDSA private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// Create a certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Task Org"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// Create the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
// Encode certificate to PEM
certPEM = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
// Encode private key to PEM
keyDER, err := x509.MarshalECPrivateKey(privateKey)
require.NoError(t, err)
keyPEM = pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyDER,
})
return certPEM, keyPEM
}
// generateTestCACert generates a self-signed CA certificate for testing
func generateTestCACert(t *testing.T) []byte {
t.Helper()
// Generate a new ECDSA private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// Create a CA certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test CA"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
IsCA: true,
BasicConstraintsValid: true,
}
// Create the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
// Encode certificate to PEM
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
}

View File

@@ -47,9 +47,6 @@ type (
trustedHosts []string
tempDir string
cacheExpiryDuration time.Duration
caCert string
cert string
certKey string
debugFunc DebugFunc
promptFunc PromptFunc
promptMutex sync.Mutex
@@ -202,59 +199,14 @@ func (o *promptFuncOption) ApplyToReader(r *Reader) {
r.promptFunc = o.promptFunc
}
// WithReaderCACert sets the path to a custom CA certificate for TLS connections.
func WithReaderCACert(caCert string) ReaderOption {
return &readerCACertOption{caCert: caCert}
}
type readerCACertOption struct {
caCert string
}
func (o *readerCACertOption) ApplyToReader(r *Reader) {
r.caCert = o.caCert
}
// WithReaderCert sets the path to a client certificate for TLS connections.
func WithReaderCert(cert string) ReaderOption {
return &readerCertOption{cert: cert}
}
type readerCertOption struct {
cert string
}
func (o *readerCertOption) ApplyToReader(r *Reader) {
r.cert = o.cert
}
// WithReaderCertKey sets the path to a client certificate key for TLS connections.
func WithReaderCertKey(certKey string) ReaderOption {
return &readerCertKeyOption{certKey: certKey}
}
type readerCertKeyOption struct {
certKey string
}
func (o *readerCertKeyOption) ApplyToReader(r *Reader) {
r.certKey = o.certKey
}
// Read will read the Taskfile defined by the [Reader]'s [Node] and recurse
// through any [ast.Includes] it finds, reading each included Taskfile and
// building an [ast.TaskfileGraph] as it goes. If any errors occur, they will be
// returned immediately.
func (r *Reader) Read(ctx context.Context, node Node) (*ast.TaskfileGraph, error) {
// Clean up git cache after reading all taskfiles
defer func() {
_ = CleanGitCache()
}()
if err := r.include(ctx, node); err != nil {
return nil, err
}
return r.graph, nil
}
@@ -356,9 +308,6 @@ func (r *Reader) include(ctx context.Context, node Node) error {
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
WithParent(node),
WithChecksum(include.Checksum),
WithCACert(r.caCert),
WithCert(r.cert),
WithCertKey(r.certKey),
)
if err != nil {
if include.Optional {

View File

@@ -38,7 +38,7 @@ var (
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL, error) {
func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil {
@@ -46,7 +46,7 @@ func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL
}
// Request the given URL
resp, err := client.Do(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
@@ -78,7 +78,7 @@ func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL
req.URL = alt
// Try the alternative URL
resp, err = client.Do(req)
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}

View File

@@ -12,10 +12,8 @@ import (
type TaskRC struct {
Version *semver.Version `yaml:"version"`
Verbose *bool `yaml:"verbose"`
Color *bool `yaml:"color"`
DisableFuzzy *bool `yaml:"disable-fuzzy"`
Concurrency *int `yaml:"concurrency"`
Interactive *bool `yaml:"interactive"`
Remote Remote `yaml:"remote"`
Failfast bool `yaml:"failfast"`
Experiments map[string]int `yaml:"experiments"`
@@ -26,11 +24,7 @@ type Remote struct {
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
CacheDir *string `yaml:"cache-dir"`
TrustedHosts []string `yaml:"trusted-hosts"`
CACert *string `yaml:"cacert"`
Cert *string `yaml:"cert"`
CertKey *string `yaml:"cert-key"`
}
// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
@@ -52,20 +46,15 @@ func (t *TaskRC) Merge(other *TaskRC) {
t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline)
t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)
t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)
t.Remote.CacheDir = cmp.Or(other.Remote.CacheDir, t.Remote.CacheDir)
if len(other.Remote.TrustedHosts) > 0 {
merged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts)
slices.Sort(merged)
t.Remote.TrustedHosts = slices.Compact(merged)
}
t.Remote.CACert = cmp.Or(other.Remote.CACert, t.Remote.CACert)
t.Remote.Cert = cmp.Or(other.Remote.Cert, t.Remote.Cert)
t.Remote.CertKey = cmp.Or(other.Remote.CertKey, t.Remote.CertKey)
t.Verbose = cmp.Or(other.Verbose, t.Verbose)
t.Color = cmp.Or(other.Color, t.Color)
t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
t.Interactive = cmp.Or(other.Interactive, t.Interactive)
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
}

View File

@@ -1,178 +0,0 @@
version: '3'
vars:
SHOULD_RUN: "yes"
ENV: "prod"
FEATURE_ENABLED: "true"
FEATURE_DISABLED: "false"
tasks:
# Basic command-level if (condition met)
cmd-if-true:
cmds:
- cmd: echo "executed"
if: "true"
# Basic command-level if (condition not met)
cmd-if-false:
cmds:
- cmd: echo "should not appear"
if: "false"
- echo "this runs"
# Task-level if (condition met)
task-if-true:
if: "true"
cmds:
- echo "task executed"
# Task-level if (condition not met)
task-if-false:
if: "false"
cmds:
- echo "should not appear"
# With template variables
if-with-template:
cmds:
- cmd: echo "Running because SHOULD_RUN={{.SHOULD_RUN}}"
if: '[ "{{.SHOULD_RUN}}" = "yes" ]'
# If inside for loop
if-in-for-loop:
cmds:
- for: ["a", "b", "c"]
cmd: echo "processing {{.ITEM}}"
if: '[ "{{.ITEM}}" != "b" ]'
# If on task call
if-on-task-call:
cmds:
- task: subtask
if: "true"
subtask:
internal: true
cmds:
- echo "subtask ran"
# If combined with platforms (both must pass)
if-with-platforms:
cmds:
- cmd: echo "condition and platform met"
platforms: [linux, darwin, windows]
if: "true"
# Skip task call
skip-task-call:
cmds:
- task: subtask
if: "false"
- echo "after skipped task call"
# Task call in cmds with if condition met
task-call-if-true:
cmds:
- task: subtask
if: "true"
- echo "after task call"
# Task call in cmds with if condition not met
task-call-if-false:
cmds:
- task: subtask
if: "false"
- echo "continues after skipped task"
# Template eq - condition met
template-eq-true:
cmds:
- cmd: echo "env is prod"
if: '{{ eq .ENV "prod" }}'
# Template eq - condition not met
template-eq-false:
cmds:
- cmd: echo "should not appear"
if: '{{ eq .ENV "dev" }}'
- echo "this runs"
# Template ne (not equal)
template-ne:
cmds:
- cmd: echo "env is not dev"
if: '{{ ne .ENV "dev" }}'
# Template with boolean-like variable
template-bool-true:
cmds:
- cmd: echo "feature enabled"
if: '{{ eq .FEATURE_ENABLED "true" }}'
# Template with boolean-like variable (false)
template-bool-false:
cmds:
- cmd: echo "should not appear"
if: '{{ eq .FEATURE_DISABLED "true" }}'
- echo "feature was disabled"
# Direct true/false from template
template-direct-true:
cmds:
- cmd: echo "direct true works"
if: '{{ .FEATURE_ENABLED }}'
# Direct true/false from template (false case)
template-direct-false:
cmds:
- cmd: echo "should not appear"
if: '{{ .FEATURE_DISABLED }}'
- echo "direct false skipped correctly"
# Template with CLI variable override
template-cli-var:
cmds:
- cmd: echo "MY_VAR is yes"
if: '{{ eq .MY_VAR "yes" }}'
# Combined template conditions with and
template-and:
cmds:
- cmd: echo "both conditions met"
if: '{{ and (eq .ENV "prod") (eq .FEATURE_ENABLED "true") }}'
# Combined template conditions with or
template-or:
cmds:
- cmd: echo "at least one condition met"
if: '{{ or (eq .ENV "dev") (eq .ENV "prod") }}'
# Task-level if with template
task-level-template:
if: '{{ eq .ENV "prod" }}'
cmds:
- echo "task runs in prod"
# Task-level if with template (not met)
task-level-template-false:
if: '{{ eq .ENV "dev" }}'
cmds:
- echo "should not appear"
# Task-level if with dynamic variable (condition met)
task-if-dynamic-true:
vars:
ENABLE_FEATURE:
sh: 'echo "true"'
if: '{{ eq .ENABLE_FEATURE "true" }}'
cmds:
- echo "dynamic feature enabled"
# Task-level if with dynamic variable (condition not met)
task-if-dynamic-false:
vars:
ENABLE_FEATURE:
sh: 'echo "false"'
if: '{{ eq .ENABLE_FEATURE "true" }}'
cmds:
- echo "should not appear"

View File

@@ -1 +0,0 @@
this runs

View File

@@ -1 +0,0 @@
executed

View File

@@ -1,7 +0,0 @@
task: "if-in-for-loop" started
task: [if-in-for-loop] echo "processing a"
processing a
task: [if-in-for-loop] if condition not met - skipped
task: [if-in-for-loop] echo "processing c"
processing c
task: "if-in-for-loop" finished

View File

@@ -1,5 +0,0 @@
task: "task-call-if-false" started
task: [task-call-if-false] if condition not met - skipped
task: [task-call-if-false] echo "continues after skipped task"
continues after skipped task
task: "task-call-if-false" finished

View File

@@ -1,2 +0,0 @@
subtask ran
after task call

View File

@@ -1,2 +0,0 @@
task: dynamic variable: "echo \"false\"" result: "false"
task: if condition not met - skipped: "task-if-dynamic-false"

View File

@@ -1 +0,0 @@
dynamic feature enabled

View File

@@ -1 +0,0 @@
task: if condition not met - skipped: "task-if-false"

View File

@@ -1 +0,0 @@
task executed

View File

@@ -1 +0,0 @@
task: if condition not met - skipped: "task-level-template-false"

View File

@@ -1 +0,0 @@
task runs in prod

View File

@@ -1 +0,0 @@
both conditions met

View File

@@ -1 +0,0 @@
feature was disabled

View File

@@ -1 +0,0 @@
feature enabled

View File

@@ -1 +0,0 @@
MY_VAR is yes

View File

@@ -1 +0,0 @@
direct false skipped correctly

View File

@@ -1 +0,0 @@
direct true works

View File

@@ -1,5 +0,0 @@
task: "template-eq-false" started
task: [template-eq-false] if condition not met - skipped
task: [template-eq-false] echo "this runs"
this runs
task: "template-eq-false" finished

View File

@@ -1 +0,0 @@
env is prod

View File

@@ -1 +0,0 @@
env is not dev

View File

@@ -1 +0,0 @@
at least one condition met

View File

@@ -1,22 +0,0 @@
version: '3'
silent: true
tasks:
hello:
cmds:
- echo "Hello from include"
hello-silent:
silent: true
cmds:
- echo "Hello from include silent task"
hello-silent-not-set:
cmds:
- echo "Hello from include silent not set task"
hello-silent-set-false:
silent: false
cmds:
- echo "Hello from include silent false task"

View File

@@ -1,13 +0,0 @@
version: '3'
includes:
inc: Taskfile-inc.yml
tasks:
default:
cmds:
- echo "Hello from root Taskfile"
- task: inc:hello
- task: inc:hello-silent
- task: inc:hello-silent-not-set
- task: inc:hello-silent-set-false

View File

@@ -1,7 +0,0 @@
task: [default] echo "Hello from root Taskfile"
Hello from root Taskfile
Hello from include
Hello from include silent task
Hello from include silent not set task
task: [inc:hello-silent-set-false] echo "Hello from include silent false task"
Hello from include silent false task

View File

@@ -1 +0,0 @@
interactive: true

View File

@@ -1,108 +0,0 @@
version: '3'
tasks:
# Simple text input prompt
greet:
desc: Greet someone by name
requires:
vars:
- NAME
cmds:
- echo "Hello, {{.NAME}}!"
# Enum selection (dropdown menu)
deploy:
desc: Deploy to an environment
requires:
vars:
- name: ENVIRONMENT
enum: [dev, staging, prod]
cmds:
- echo "Deploying to {{.ENVIRONMENT}}..."
# Multiple variables at once
release:
desc: Create a release with version and environment
requires:
vars:
- VERSION
- name: ENVIRONMENT
enum: [dev, staging, prod]
cmds:
- echo "Releasing {{.VERSION}} to {{.ENVIRONMENT}}"
# Nested dependencies - all prompts happen upfront
full-deploy:
desc: Full deployment pipeline with nested deps
deps:
- task: build
- task: test
cmds:
- task: deploy
build:
requires:
vars:
- name: BUILD_MODE
enum: [debug, release]
cmds:
- echo "Building in {{.BUILD_MODE}} mode..."
test:
requires:
vars:
- name: TEST_SUITE
enum: [unit, integration, e2e, all]
cmds:
- echo "Running {{.TEST_SUITE}} tests..."
# Variable already set - no prompt shown
greet-world:
desc: Greet the world (no prompt needed)
vars:
NAME: World
requires:
vars:
- NAME
cmds:
- echo "Hello, {{.NAME}}!"
# Complex scenario with multiple levels
pipeline:
desc: Run the full CI/CD pipeline
cmds:
- task: setup
- task: build
- task: test
- task: deploy
setup:
requires:
vars:
- PROJECT_NAME
cmds:
- echo "Setting up project {{.PROJECT_NAME}}..."
# Docker example with multiple selections
docker-build:
desc: Build a Docker image
requires:
vars:
- IMAGE_NAME
- IMAGE_TAG
- name: PLATFORM
enum: [linux/amd64, linux/arm64, linux/arm/v7]
cmds:
- echo "Building {{.IMAGE_NAME}}:{{.IMAGE_TAG}} for {{.PLATFORM}}"
# Database migration example
db-migrate:
desc: Run database migrations
requires:
vars:
- name: DIRECTION
enum: [up, down]
- name: DATABASE
enum: [postgres, mysql, sqlite]
cmds:
- echo "Running {{.DIRECTION}} migrations on {{.DATABASE}}"

View File

@@ -1,7 +0,0 @@
version: '3'
tasks:
foo:
prefix: "foobar"
status:
- echo "I'm ok"

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ tasks:
cmds:
- echo {{.TASK}}
print-root-dir: echo {{.ROOT_DIR}}
print-root-taskfile: echo {{.ROOT_TASKFILE}}
print-taskfile: echo {{.TASKFILE}}
print-taskfile-dir: echo {{.TASKFILE_DIR}}
print-task-version: echo {{.TASK_VERSION}}

View File

@@ -1 +0,0 @@
{{.TEST_DIR}}/testdata/special_vars/Taskfile.yml

View File

@@ -1 +0,0 @@
{{.TEST_DIR}}/testdata/special_vars/Taskfile.yml

View File

@@ -49,10 +49,3 @@ tasks:
- echo "{{.MESSAGE}}"
from-dot-env: echo '{{.DOT_ENV_VAR}}'
# Test that CLI variables take priority over Taskfile defaults
cli-var-priority:
vars:
CLI_VAR: '{{.CLI_VAR | default "default_value"}}'
cmds:
- echo '{{.CLI_VAR}}'

View File

@@ -1 +0,0 @@
default_value

View File

@@ -1 +0,0 @@
from_cli

View File

@@ -19,8 +19,6 @@ tasks:
- "echo \"I don't consume matches: {{.MATCH}}\""
start-*:
aliases:
- s-*
vars:
SERVICE: "{{index .MATCH 0}}"
cmds:

View File

@@ -10,7 +10,6 @@ import (
"github.com/joho/godotenv"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
@@ -58,7 +57,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
Vars: vars,
Env: nil,
Dotenv: origTask.Dotenv,
Silent: deepcopy.Scalar(origTask.Silent),
Silent: origTask.Silent,
Interactive: origTask.Interactive,
Internal: origTask.Internal,
Method: origTask.Method,
@@ -114,7 +113,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
Vars: vars,
Env: nil,
Dotenv: templater.Replace(origTask.Dotenv, cache),
Silent: deepcopy.Scalar(origTask.Silent),
Silent: origTask.Silent,
Interactive: origTask.Interactive,
Internal: origTask.Internal,
Method: templater.Replace(origTask.Method, cache),
@@ -124,7 +123,6 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
IncludeVars: origTask.IncludeVars,
IncludedTaskfileVars: origTask.IncludedTaskfileVars,
Platforms: origTask.Platforms,
If: templater.Replace(origTask.If, cache),
Location: origTask.Location,
Requires: origTask.Requires,
Watch: origTask.Watch,
@@ -230,7 +228,6 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
newCmd := cmd.DeepCopy()
newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
newCmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
newCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
new.Cmds = append(new.Cmds, newCmd)
}
@@ -245,7 +242,6 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
newCmd := cmd.DeepCopy()
newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
newCmd.Task = templater.Replace(cmd.Task, cache)
newCmd.If = templater.Replace(cmd.If, cache)
newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache)
new.Cmds = append(new.Cmds, newCmd)
}

View File

@@ -1,4 +1,4 @@
import { defineConfig, HeadConfig } from 'vitepress';
import { defineConfig } from 'vitepress';
import githubLinksPlugin from './plugins/github-links';
import { readFileSync } from 'fs';
import { resolve } from 'path';
@@ -9,7 +9,7 @@ import {
localIconLoader
} from 'vitepress-plugin-group-icons';
import { team } from './team.ts';
import { taskDescription, taskName, ogUrl, ogImage } from './meta.ts';
import { taskDescription, taskName } from './meta.ts';
import { fileURLToPath, URL } from 'node:url';
import llmstxt, { copyOrDownloadAsMarkdownButtons } from 'vitepress-plugin-llms';
@@ -35,24 +35,35 @@ export default defineConfig({
description: taskDescription,
lang: 'en-US',
head: [
// Favicon ICO for legacy browsers (auto-discovery)
['link', { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }],
// Favicon SVG for modern browsers (scalable)
['link', { rel: 'icon', href: '/img/logo.svg', type: 'image/svg+xml' }],
// Apple Touch Icon for iOS devices
['link', { rel: 'apple-touch-icon', href: '/img/logo.png' }],
[
'link',
{
rel: 'icon',
type: 'image/x-icon',
href: '/img/favicon.icon',
sizes: '48x48'
}
],
[
'link',
{
rel: 'icon',
sizes: 'any',
type: 'image/svg+xml',
href: '/img/logo.svg'
}
],
[
'link',
{
rel: 'canonical',
href: 'https://taskfile.dev/'
}
],
[
'meta',
{ name: 'author', content: `${team.map((c) => c.name).join(', ')}` }
],
// Open Graph
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'Task' }],
['meta', { property: 'og:image', content: ogImage }],
// Twitter Card
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:site', content: '@taskfiledev' }],
['meta', { name: 'twitter:image', content: ogImage }],
[
'meta',
{
@@ -68,43 +79,8 @@ export default defineConfig({
src: "https://u.taskfile.dev/script.js",
"data-website-id": "084030b0-0e3f-4891-8d2a-0c12c40f5933"
}
],
[
"script",
{ type: "application/ld+json" },
JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Task",
"url": "https://taskfile.dev/"
})
]
],
transformHead({ pageData }) {
const head: HeadConfig[] = []
// Canonical URL dynamique
const canonicalUrl = `https://taskfile.dev/${pageData.relativePath
.replace(/\.md$/, '')
.replace(/index$/, '')}`
head.push(['link', { rel: 'canonical', href: canonicalUrl }])
// Dynamic Open Graph and Twitter meta tags
const pageTitle = pageData.frontmatter.title || pageData.title || taskName
const pageDescription = pageData.frontmatter.description || pageData.description || taskDescription
head.push(['meta', { property: 'og:title', content: `${pageTitle} | Task` }])
head.push(['meta', { property: 'og:description', content: pageDescription }])
head.push(['meta', { property: 'og:url', content: canonicalUrl }])
head.push(['meta', { name: 'twitter:title', content: `${pageTitle} | Task` }])
head.push(['meta', { name: 'twitter:description', content: pageDescription }])
// Noindex pour 404
if (pageData.relativePath === '404.md') {
head.push(['meta', { name: 'robots', content: 'noindex, nofollow' }])
}
return head
},
srcDir: 'src',
cleanUrls: true,
markdown: {
@@ -202,16 +178,6 @@ export default defineConfig({
sidebar: {
'/blog/': [
{
text: '2026',
collapsed: false,
items: [
{
text: 'New `if:` Control and Variable Prompt',
link: '/blog/if-and-variable-prompt'
}
]
},
{
text: '2025',
collapsed: false,
@@ -376,12 +342,6 @@ export default defineConfig({
}
},
sitemap: {
hostname: 'https://taskfile.dev',
transformItems: (items) => {
return items.map((item) => ({
...item,
lastmod: new Date().toISOString()
}));
}
hostname: 'https://taskfile.dev'
}
});

View File

@@ -3,4 +3,3 @@ export const taskDescription =
'A fast, cross-platform build tool inspired by Make, designed for modern workflows.';
export const ogUrl = 'https://taskfile.dev/';
export const ogImage = 'https://taskfile.dev/img/logo.png';

View File

@@ -20,8 +20,10 @@
"vitepress": "^1.6.3",
"vitepress-plugin-group-icons": "^1.6.1",
"vitepress-plugin-tabs": "^0.7.1",
"vitepress-plugin-llms": "^1.9.1",
"vue": "^3.5.18"
},
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
"dependencies": {
"vitepress-plugin-llms": "^1.9.1"
}
}

1569
website/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +0,0 @@
---
title: New `if:` Control and Variable Prompt
description: Introduction of the `if:` control and required variable prompts.
author: vmaerten
date: 2026-01-24
outline: deep
---
# New `if:` control and interactivity support
<AuthorCard :author="$frontmatter.author" />
The [v3.47.0][release] release is here, and it brings two exciting new features
to Task. Let's take a closer look at them!
## The New `if:` Control
This first feature is simply the second most upvoted issue of all time (!) with
58 :thumbsup:s (!!) at the time of writing.
It introduces the `if:` control, which allow you to conditionally skip the
execution of certain tasks and proceeding. `if:` can be set on a task-level or
command-level, and can be either a Bash command or a Go template expression.
Let me show a couple of examples.
Task-level with Bash expression:
```yaml
version: '3'
tasks:
deploy:
if: '[ "$CI" = "true" ]'
cmds:
- echo "Deploying..."
- ./deploy.sh
```
Command-level with Go template expression:
```yaml
version: '3'
tasks:
conditional:
vars:
ENABLE_FEATURE: "true"
cmds:
- cmd: echo "Feature is enabled"
if: '{{eq .ENABLE_FEATURE "true"}}'
- cmd: echo "Feature is disabled"
if: '{{ne .ENABLE_FEATURE "true"}}'
```
For more details, please check out the [documentation][if-docs].
The [examples][if-examples] from the test suite may be useful too.
::: info
We had similar functionality before, but nothing that perfectly fits this use
case. There were [`sources:`][sources] and [`status:`][status], but those were
meant to check if a task was up-to-date, and [`preconditions:`][preconditions],
but this would halt the execution of the task instead of skipping it.
:::
## Prompt for Required Variables
For backward-compatibility reasons, this feature is disabled by default.
To enable it, either pass `--interactive` flag or add `interactive: true` to
your `.taskrc.yml`.
Once you do that, Task will basically starting prompting you in runtime for any
required variables. In the example below, `NAME` will be prompted at runtime:
```yaml
version: '3'
tasks:
# Simple text input prompt
greet:
desc: Greet someone by name
requires:
vars:
- NAME
cmds:
- echo "Hello, {{.NAME}}!"
```
If a given variable has an enum, Task will actually show a selection menu so you
can choose the right option instead of typing:
```yaml
version: '3'
tasks:
# Enum selection (dropdown menu)
deploy:
desc: Deploy to an environment
requires:
vars:
- name: ENVIRONMENT
enum: [dev, staging, prod]
cmds:
- echo "Deploying to {{.ENVIRONMENT}}..."
```
Once again, check out the [documentation][prompt-docs] for more details, and the
[prompt examples][prompt-examples] from the test suite.
## Feedback
Let's us know if you have any feedback! You can find us on our
[Discord server][discord].
[release]: https://github.com/go-task/task/releases/tag/v3.47.0
[vmaerten]: https://github.com/vmaerten
[sources]: https://taskfile.dev/docs/guide#by-fingerprinting-locally-generated-files-and-their-sources
[status]: https://taskfile.dev/docs/guide#using-programmatic-checks-to-indicate-a-task-is-up-to-date
[preconditions]: https://taskfile.dev/docs/guide#using-programmatic-checks-to-cancel-the-execution-of-a-task-and-its-dependencies
[if-docs]: https://taskfile.dev/docs/guide#conditional-execution-with-if
[if-examples]: https://github.com/go-task/task/blob/main/testdata/if/Taskfile.yml
[prompt-docs]: https://taskfile.dev/docs/guide#prompting-for-missing-variables-interactively
[prompt-examples]: https://github.com/go-task/task/blob/main/testdata/interactive_vars/Taskfile.yml
[discord]: https://discord.com/invite/6TY36E39UK

View File

@@ -3,15 +3,6 @@ title: Blog
description: Latest news and updates from the Task team
---
<BlogPost
title="New `if:` Control and Variable Prompt"
url="/blog/if-and-variable-prompt"
date="2026-01-24"
author="vmaerten"
description="The v3.47.0 release is here, and it brings two exciting new features to Task. Let's take a closer look at them!"
:tags="['new-features', 'variables']"
/>
<BlogPost
title="Announcing Built-in Core Utilities for Windows"
url="/blog/windows-core-utils"

View File

@@ -7,119 +7,6 @@ outline: deep
::: v-pre
## v3.48.0 - 2026-01-26
- Fixed `if:` conditions when using to check dynamic variables. Also, skip
variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
@trulede).
- Included Taskfiles with `silent: true` now properly propagate silence to their
tasks, while still allowing individual tasks to override with `silent: false`
(#2640, #1319 by @trulede).
- Added TLS certificate options for Remote Taskfiles: use `--cacert` for
self-signed certificates and `--cert`/`--cert-key` for mTLS authentication
(#2537, #2242 by @vmaerten).
## v3.47.0 - 2026-01-24
- Fixed remote git Taskfiles: cloning now works without explicit ref, and
directory includes are properly resolved (#2602 by @vmaerten).
- For `output: prefixed`, print `prefix:` if set instead of task name (#1566,
#2633 by @trulede).
- Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by
@trulede).
- Task aliases can now contain wildcards and will match accordingly (e.g., `s-*`
as alias for `start-*`) (#1900, #2234 by @vmaerten).
- Added conditional execution with the `if` field: skip tasks, commands, or task
calls based on shell exit codes or template expressions like
`{{ eq .ENV "prod" }}` (#2564, #608 by @vmaerten).
- Task can now interactively prompt for missing required variables when running
in a TTY, with support for enum selection menus. Enable with `--interactive`
flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten).
## v3.46.4 - 2025-12-24
- Fixed regressions in completion script for Fish (#2591, #2604, #2592 by
@WinkelCode).
## v3.46.3 - 2025-12-19
- Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten).
## v3.46.2 - 2025-12-18
- Fixed a regression on previous release that affected variables passed via
command line (#2588, #2589 by @vmaerten).
## v3.46.1 - 2025-12-18
### ✨ Features
- A small behavior change was made to dependencies. Task will now wait for all
dependencies to finish running before continuing, even if any of them fail. To
opt for the previous behavior, set `failfast: true` either on your
`.taskrc.yml` or per task, or use the `--failfast` flag, which will also work
for `--parallel` (#1246, #2525 by @andreynering).
- The `--summary` flag now displays `vars:` (both global and task-level),
`env:`, and `requires:` sections. Dynamic variables show their shell command
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
@vmaerten).
- Improved performance of fuzzy task name matching by implementing lazy
initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option
to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).
- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`
and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).
- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to
skip confirmation prompts for specified hosts when using Remote Taskfiles
(#2491, #2473 by @maciejlech).
- When running in GitHub Actions, Task now automatically emits error annotations
on failure, improving visibility in workflow summaries (#2568 by @vmaerten).
- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`
variable (#2577, #2479 by @semihbkgr).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- Shell completion now works when Task is installed or aliased under a different
binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).
- Some small fixes and improvements were made to `task --init` and to the
default Taskfile it generates (#2433 by @andreynering).
- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to
customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).
- Zsh completion now supports zstyle verbose option to show or hide task
descriptions (#2571 by @vmaerten).
- Task now automatically enables colored output in CI environments (GitHub
Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by
@vmaerten).
- Added color taskrc option to explicitly enable or disable colored output
globally (#2569 by @vmaerten).
- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication
now works out of the box and `applyOf` is properly supported (#2512 by
@vmaerten).
### 🐛 Fixes
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or
`run: when_changed` settings (#2566, #1388 by @trulede).
- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in
root-level vars section (#2403, #2397 by @trulede, @vmaerten).
- Fixed a bug where `ignore_error` was ignored when using `task:` to call
another task (#2552, #363 by @trulede).
- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`
flag (#1574, #2574 by @vmaerten).
- Fixed Fish completion failing to parse task descriptions containing colons
(e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).
- Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when
using `for` loops in Taskfiles (#2576 by @vmaerten).
## v3.45.5 - 2025-11-11
- Fixed bug that made a generic message, instead of an useful one, appear when a
@@ -135,8 +22,7 @@ outline: deep
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).
- 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
@@ -144,8 +30,9 @@ outline: deep
- 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).
[mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199),
#2506 by @andreynering).
## v3.45.4 - 2025-09-17
@@ -296,8 +183,8 @@ Reverted the changes made in #2113 and #2186 that affected the
- The default taskfile (output when using the `--init` flag) is now an embedded
file in the binary instead of being stored in the code (#2112 by @pd93).
- Improved the way we report the Task version when using the `--version` flag or
`{{.TASK_VERSION}}` variable. This should now be more consistent and easier
for package maintainers to use (#2131 by @pd93).
`{{.TASK_VERSION}}` variable. This should now be more
consistent and easier for package maintainers to use (#2131 by @pd93).
- Fixed a bug where globstar (`**`) matching in `sources` only resolved the
first result (#2073, #2075 by @pd93).
- Fixed a bug where sorting tasks by "none" would use the default sorting
@@ -311,7 +198,7 @@ Reverted the changes made in #2113 and #2186 that affected the
- Fix Fish completions when `--global` (`-g`) is given (#2134 by @atusy).
- Fixed variables not available when using `defer:` (#1909, #2173 by @vmaerten).
#### Package API
### Package API
- The [`Executor`](https://pkg.go.dev/github.com/go-task/task/v3#Executor) now
uses the functional options pattern (#2085, #2147, #2148 by @pd93).
@@ -368,7 +255,7 @@ Reverted the changes made in #2113 and #2186 that affected the
used, all other variables become unavailable in the templating system within
the include (#2092 by @vmaerten).
#### Package API
### Package API
Unlike our CLI tool,
[Task's package API is not currently stable](https://taskfile.dev/reference/package).
@@ -758,8 +645,9 @@ stabilize the API in the future. #121 now tracks this piece of work.
@FilipSolich).
- Fix `defer` on JSON Schema (#1288 by @calvinmclean and @andreynering).
- Fix bug in usage of special variables like `{{.USER_WORKING_DIR}}` in
combination with `includes` (#1046, #1205, #1250, #1293, #1312, #1274 by
@andarto, #1309 by @andreynering).
combination with `includes`
(#1046, #1205, #1250, #1293, #1312, #1274 by @andarto, #1309 by
@andreynering).
- Fix bug on `--status` flag. Running this flag should not have side-effects: it
should not update the checksum on `.task`, only report its status (#1305,
#1307 by @visciang, #1313 by @andreynering).
@@ -864,9 +752,9 @@ it a go and let us know what you think via a
- Added task location data to the `--json` flag output (#1056 by @pd93)
- Change the name of the file generated by `task --init` from `Taskfile.yaml` to
`Taskfile.yml` (#1062 by @misitebao).
- Added new `splitArgs` template function
(`{{splitArgs "foo bar 'foo bar baz'"}}`) to ensure string is split as
arguments (#1040, #1059 by @dhanusaputra).
- Added new `splitArgs` template function (`{{splitArgs "foo bar 'foo bar
baz'"}}`) to ensure string is split as arguments (#1040, #1059 by
@dhanusaputra).
- Fix the value of `{{.CHECKSUM}}` variable in status (#1076, #1080 by @pd93).
- Fixed deep copy implementation (#1072 by @pd93)
- Created a tool to assist with releases (#1086 by @pd93).
@@ -1091,8 +979,8 @@ it a go and let us know what you think via a
## v3.9.0 - 2021-10-02
- A new `shellQuote` function was added to the template system
(`{{shellQuote "a string"}}`) to ensure a string is safe for use in shell
- A new `shellQuote` function was added to the template system (`{{shellQuote
"a string"}}`) to ensure a string is safe for use in shell
([mvdan/sh#727](https://github.com/mvdan/sh/pull/727),
[mvdan/sh#737](https://github.com/mvdan/sh/pull/737),
[Documentation](https://pkg.go.dev/mvdan.cc/sh/v3@v3.4.0/syntax#Quote))
@@ -1479,4 +1367,4 @@ document, since it describes in depth what changed for this version.
- Add LICENSE file
:::
:::

View File

@@ -263,38 +263,6 @@ Taskfile that is downloaded via an unencrypted connection. Sources that are not
protected by TLS are vulnerable to man-in-the-middle attacks and should be
avoided unless you know what you are doing.
#### Custom Certificates
If your remote Taskfiles are hosted on a server that uses a custom CA
certificate (e.g., a corporate internal server), you can specify the CA
certificate using the `--cacert` flag:
```shell
task --taskfile https://internal.example.com/Taskfile.yml --cacert /path/to/ca.crt
```
For servers that require client certificate authentication (mTLS), you can
provide a client certificate and key:
```shell
task --taskfile https://secure.example.com/Taskfile.yml \
--cert /path/to/client.crt \
--cert-key /path/to/client.key
```
::: warning
Encrypted private keys are not currently supported. If your key is encrypted,
you must decrypt it first:
```shell
openssl rsa -in encrypted.key -out decrypted.key
```
:::
These options can also be configured in the [configuration file](#configuration).
## Caching & Running Offline
Whenever you run a remote Taskfile, the latest copy will be downloaded from the
@@ -316,11 +284,10 @@ and look for a cached copy instead. This timeout can be configured by setting
the `--timeout` flag and specifying a duration. For example, `--timeout 5s` will
set the timeout to 5 seconds.
By default, the cache is stored in the Task temp directory (`.task`). You can
override the location of the cache by using the `--remote-cache-dir` flag, the
`remote.cache-dir` option in your [configuration file](#cache-dir), or the
`TASK_REMOTE_DIR` environment variable. This way, you can share the cache
between different projects.
By default, the cache is stored in the Task temp directory, represented by the
`TASK_TEMP_DIR` environment variable. You can override the location of the cache
by setting the `TASK_REMOTE_DIR` environment variable. This way, you can share
the cache between different projects.
You can force Task to ignore the cache and download the latest version by using
the `--download` flag.
@@ -341,13 +308,9 @@ remote:
offline: false
timeout: "30s"
cache-expiry: "24h"
cache-dir: ~/.task
trusted-hosts:
- github.com
- gitlab.com
cacert: ""
cert: ""
cert-key: ""
```
#### `insecure`
@@ -397,20 +360,6 @@ remote:
cache-expiry: "6h"
```
#### `cache-dir`
- **Type**: `string`
- **Default**: `.task`
- **Description**: Directory where remote Taskfiles are cached. Can be an
absolute path (e.g., `/var/cache/task`) or relative to the Taskfile directory.
- **CLI equivalent**: `--remote-cache-dir`
- **Environment variable**: `TASK_REMOTE_DIR` (lowest priority)
```yaml
remote:
cache-dir: ~/.task
```
#### `trusted-hosts`
- **Type**: `array of strings`
@@ -445,36 +394,3 @@ task --trusted-hosts github.com,gitlab.com -t https://github.com/user/repo.git//
# Trust a host with a specific port
task --trusted-hosts example.com:8080 -t https://example.com:8080/Taskfile.yml
```
#### `cacert`
- **Type**: `string`
- **Default**: `""`
- **Description**: Path to a custom CA certificate file for TLS verification
```yaml
remote:
cacert: "/path/to/ca.crt"
```
#### `cert`
- **Type**: `string`
- **Default**: `""`
- **Description**: Path to a client certificate file for mTLS authentication
```yaml
remote:
cert: "/path/to/client.crt"
```
#### `cert-key`
- **Type**: `string`
- **Default**: `""`
- **Description**: Path to the client certificate private key file
```yaml
remote:
cert-key: "/path/to/client.key"
```

View File

@@ -174,19 +174,6 @@ tasks:
- echo "Using $KEYNAME and endpoint $ENDPOINT"
```
When the same variable is defined in multiple dotenv files, the **first file in
the list takes precedence**. This allows you to set up override patterns by
placing higher-priority files first:
```yaml
version: '3'
dotenv:
- .env.local # Highest priority - local developer overrides
- .env.{{.ENV}} # Environment-specific settings
- .env # Base defaults (lowest priority)
```
Dotenv files can also be specified at the task level:
```yaml
@@ -1020,99 +1007,6 @@ tasks:
- echo "I will not run"
```
### Conditional execution with `if`
The `if` attribute allows you to conditionally skip tasks or commands based on a
shell command's exit code. Unlike `preconditions` which fail and stop execution,
`if` simply skips the task or command when the condition is not met and continues
with the rest of the Taskfile.
#### Task-level `if`
When `if` is set on a task, the entire task is skipped if the condition fails:
```yaml
version: '3'
tasks:
deploy:
if: '[ "$CI" = "true" ]'
cmds:
- echo "Deploying..."
- ./deploy.sh
```
#### Command-level `if`
When `if` is set on a command, only that specific command is skipped:
```yaml
version: '3'
tasks:
build:
cmds:
- cmd: echo "Building for production"
if: '[ "$ENV" = "production" ]'
- cmd: echo "Building for development"
if: '[ "$ENV" = "development" ]'
- go build ./...
```
#### Using templates in `if` conditions
You can use Go template expressions in `if` conditions. Template expressions like
<span v-pre>`{{eq .VAR "value"}}`</span> evaluate to `true` or `false`, which are valid shell
commands (`true` exits with 0, `false` exits with 1):
```yaml
version: '3'
tasks:
conditional:
vars:
ENABLE_FEATURE: "true"
cmds:
- cmd: echo "Feature is enabled"
if: '{{eq .ENABLE_FEATURE "true"}}'
- cmd: echo "Feature is disabled"
if: '{{ne .ENABLE_FEATURE "true"}}'
```
#### Using `if` with `for` loops
When used inside a `for` loop, the `if` condition is evaluated for each iteration:
```yaml
version: '3'
tasks:
process-items:
cmds:
- for: ['a', 'b', 'c']
cmd: echo "processing {{.ITEM}}"
if: '[ "{{.ITEM}}" != "b" ]'
```
This will output:
```
processing a
processing c
```
#### `if` vs `preconditions`
| Aspect | `if` | `preconditions` |
|--------|------|-----------------|
| On failure | Skips (continues) | Fails (stops) |
| Message | Only in verbose mode | Always shown |
| Use case | "Run if possible" | "Must be true" |
Use `if` when you want optional conditional execution that shouldn't stop the
workflow. Use `preconditions` when the condition must be met for the task to
make sense.
### Limiting when tasks run
If a task executed by multiple `cmds` or multiple `deps` you can control when it
@@ -1233,65 +1127,6 @@ This is supported only for string variables.
:::
### Prompting for missing variables interactively
If you want Task to prompt users for missing required variables instead of
failing, you can enable interactive mode in your `.taskrc.yml`:
```yaml
# ~/.taskrc.yml
interactive: true
```
When enabled, Task will display an interactive prompt for any missing required
variable. For variables with an `enum`, a selection menu is shown. For variables
without an enum, a text input is displayed.
```yaml
# Taskfile.yml
version: '3'
tasks:
deploy:
requires:
vars:
- name: ENVIRONMENT
enum: [dev, staging, prod]
- VERSION
cmds:
- echo "Deploying {{.VERSION}} to {{.ENVIRONMENT}}"
```
```shell
$ task deploy
? Select value for ENVIRONMENT:
dev
staging
prod
? Enter value for VERSION: 1.0.0
Deploying 1.0.0 to prod
```
If the variable is already set (via CLI, environment, or Taskfile), no prompt
is shown:
```shell
$ task deploy ENVIRONMENT=prod VERSION=1.0.0
Deploying 1.0.0 to prod
```
::: info
Interactive prompts require a TTY (terminal). Task automatically detects
non-interactive environments like GitHub Actions, GitLab CI, and other CI
pipelines where stdin/stdout are not connected to a terminal. In these cases,
prompts are skipped and missing variables will cause an error as usual.
You can enable prompts from the command line with `--interactive` or by setting
`interactive: true` in your `.taskrc.yml`.
:::
## Variables
Task allows you to set variables using the `vars` keyword. The following
@@ -1944,27 +1779,6 @@ $ task start:foo:3
Starting foo with 3 replicas
```
Using wildcards with aliases
Wildcards also work with aliases. If a task has an alias, you can use the alias name with wildcards to capture arguments. For example:
```yaml
version: '3'
tasks:
start:*:
aliases: [run:*]
vars:
SERVICE: "{{index .MATCH 0}}"
cmds:
- echo "Running {{.SERVICE}}"
```
In this example, you can call the task using the alias run:*:
```shell
$ task run:foo
Running foo
```
## Doing task cleanup with `defer`
With the `defer` keyword, it's possible to schedule cleanup to be run once the
@@ -2476,28 +2290,6 @@ The `output` option can also be specified by the `--output` or `-o` flags.
:::
## CI Integration
### Colored output
Task automatically enables colored output when running in CI environments
(`CI=true`). Most CI providers set this variable automatically.
You can also force colored output with `FORCE_COLOR=1` or disable it with
`NO_COLOR=1`.
### Error annotations
When running in GitHub Actions (`GITHUB_ACTIONS=true`), Task automatically emits
error annotations when a task fails. These annotations appear in the workflow
summary, making it easier to spot failures without scrolling through logs.
```shell
::error title=Task 'build' failed::exit status 1
```
This feature requires no configuration and works automatically.
## Interactive CLI application
When running interactive CLI applications inside Task they can sometimes behave

View File

@@ -384,13 +384,3 @@ task --completion fish > ~/.config/fish/completions/task.fish
```
:::
### Zsh customization
The Zsh completion supports the standard `verbose` zstyle to control whether task
descriptions are shown. By default, descriptions are displayed. To show only task
names without descriptions, add this to your `~/.zshrc` (after the completion is loaded):
```shell
zstyle ':completion:*:*:task:*' verbose false
```

View File

@@ -301,19 +301,6 @@ Automatically answer "yes" to all prompts.
task deploy --yes
```
#### `--interactive`
Enable interactive prompts for missing required variables. When a required
variable is not provided, Task will prompt for input instead of failing.
Task automatically detects non-TTY environments (like CI pipelines) and
skips prompts. This flag can also be set in `.taskrc.yml` to enable prompts
by default.
```bash
task deploy --interactive
```
## Exit Codes
Task uses specific exit codes to indicate different types of errors:

View File

@@ -91,17 +91,6 @@ experiments:
verbose: true
```
### `color`
- **Type**: `boolean`
- **Default**: `true`
- **Description**: Enable colored output. Colors are automatically enabled in CI environments (`CI=true`).
- **CLI equivalent**: [`-c, --color`](./cli.md#-c---color)
```yaml
color: false
```
### `disable-fuzzy`
- **Type**: `boolean`
@@ -135,20 +124,6 @@ concurrency: 4
failfast: true
```
### `interactive`
- **Type**: `boolean`
- **Default**: `false`
- **Description**: Prompt for missing required variables instead of failing.
When enabled, Task will display an interactive prompt for any missing required
variable. Requires a TTY. Task automatically detects non-TTY environments
(CI pipelines, etc.) and skips prompts.
- **CLI equivalent**: [`--interactive`](./cli.md#--interactive)
```yaml
interactive: true
```
## Example Configuration
Here's a complete example of a `.taskrc.yml` file with all available options:
@@ -156,7 +131,6 @@ Here's a complete example of a `.taskrc.yml` file with all available options:
```yaml
# Global settings
verbose: true
color: true
disable-fuzzy: false
concurrency: 2

View File

@@ -155,14 +155,12 @@ silent: true
### `dotenv`
- **Type**: `[]string`
- **Description**: Load environment variables from .env files. When the same
variable is defined in multiple files, the first file in the list takes
precedence.
- **Description**: Load environment variables from .env files
```yaml
dotenv:
- .env.local # Highest priority
- .env # Lowest priority
- .env
- .env.local
```
### `run`
@@ -616,27 +614,6 @@ tasks:
- ./deploy.sh
```
#### `if`
- **Type**: `string`
- **Description**: Shell command to conditionally execute the task. If the
command exits with a non-zero code, the task is skipped (not failed).
```yaml
tasks:
# Task only runs in CI environment
deploy:
if: '[ "$CI" = "true" ]'
cmds:
- ./deploy.sh
# Using Go template expressions
build-prod:
if: '{{eq .ENV "production"}}'
cmds:
- go build -ldflags="-s -w" ./...
```
### `dir`
- **Type**: `string`
@@ -655,7 +632,7 @@ tasks:
#### `requires`
- **Type**: `Requires`
- **Description**: Required variables with optional enum validation
- **Description**: Required variables with optional enums
```yaml
tasks:
@@ -680,9 +657,6 @@ tasks:
- ./deploy.sh
```
See [Prompting for missing variables interactively](/docs/guide#prompting-for-missing-variables-interactively)
for information on enabling interactive prompts for missing required variables.
#### `watch`
- **Type**: `bool`
@@ -836,27 +810,6 @@ tasks:
SERVICE: '{{.ITEM}}'
```
### Conditional Commands
Use `if` to conditionally execute a command. If the shell command exits with a
non-zero code, the command is skipped.
```yaml
tasks:
build:
cmds:
# Only run in production
- cmd: echo "Optimizing for production"
if: '[ "$ENV" = "production" ]'
# Using Go templates
- cmd: echo "Feature enabled"
if: '{{eq .ENABLE_FEATURE "true"}}'
# Inside for loops (evaluated per iteration)
- for: [a, b, c]
cmd: echo "processing {{.ITEM}}"
if: '[ "{{.ITEM}}" != "b" ]'
```
## Shell Options
### Set Options

View File

@@ -172,11 +172,6 @@ tasks:
- **Type**: `bool`
- **Description**: Whether `--offline` flag was set
#### `CLI_ASSUME_YES`
- **Type**: `bool`
- **Description**: Whether `--yes` flag was set
### Task
#### `TASK`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -1,4 +0,0 @@
User-agent: *
Allow: /
Sitemap: https://taskfile.dev/sitemap.xml

View File

@@ -43,10 +43,6 @@
"description": "Expiry duration for cached remote Taskfiles (e.g., '1h', '24h')",
"pattern": "^[0-9]+(ns|us|µs|ms|s|m|h)$"
},
"cache-dir": {
"type": "string",
"description": "Directory to cache remote Taskfiles"
},
"trusted-hosts": {
"type": "array",
"description": "List of trusted hosts for remote Taskfiles (e.g., 'github.com', 'gitlab.com', 'example.com:8080').",
@@ -61,10 +57,6 @@
"type": "boolean",
"description": "Enable verbose output"
},
"color": {
"type": "boolean",
"description": "Enable colored output"
},
"disable-fuzzy": {
"type": "boolean",
"description": "Disable fuzzy matching for task names"
@@ -78,11 +70,6 @@
"description": "When running tasks in parallel, stop all tasks if one fails.",
"type": "boolean",
"default": false
},
"interactive": {
"description": "Prompt for missing required variables instead of failing. Requires a TTY.",
"type": "boolean",
"default": false
}
},
"additionalProperties": false

View File

@@ -193,10 +193,6 @@
"description": "Specifies which platforms the task should be run on.",
"$ref": "#/definitions/platforms"
},
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the task is skipped.",
"type": "string"
},
"requires": {
"description": "A list of variables which should be set if this task is to run, if any of these variables are unset the task will error and not run",
"$ref": "#/definitions/requires_obj"
@@ -237,10 +233,7 @@
"$ref": "#/definitions/defer_cmd_call"
},
{
"$ref": "#/definitions/for_cmd_call"
},
{
"$ref": "#/definitions/for_task_call"
"$ref": "#/definitions/for_cmds_call"
}
]
},
@@ -336,10 +329,6 @@
"silent": {
"description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.",
"type": "boolean"
},
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
}
},
"additionalProperties": false,
@@ -377,10 +366,6 @@
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"$ref": "#/definitions/platforms"
},
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
}
},
"additionalProperties": false,
@@ -416,7 +401,7 @@
"additionalProperties": false,
"required": ["defer"]
},
"for_cmd_call": {
"for_cmds_call": {
"type": "object",
"properties": {
"for": {
@@ -430,20 +415,6 @@
"description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"$ref": "#/definitions/platforms"
}
},
"additionalProperties": false,
"required": ["for", "cmd"]
},
"for_task_call": {
"type": "object",
"properties": {
"for": {
"$ref": "#/definitions/for"
},
"task": {
"description": "Task to run",
"type": "string"
@@ -452,21 +423,14 @@
"description": "Values passed to the task called",
"$ref": "#/definitions/vars"
},
"silent": {
"description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"$ref": "#/definitions/platforms"
},
"if": {
"description": "A shell command to evaluate. If the exit code is non-zero, the command is skipped.",
"type": "string"
}
},
"oneOf": [{ "required": ["cmd"] }, { "required": ["task"] }],
"additionalProperties": false,
"required": ["for", "task"]
"required": ["for"]
},
"for_deps_call": {
"type": "object",
@@ -487,8 +451,9 @@
"$ref": "#/definitions/vars"
}
},
"oneOf": [{ "required": ["cmd"] }, { "required": ["task"] }],
"additionalProperties": false,
"required": ["for", "task"]
"required": ["for"]
},
"for": {
"anyOf": [
@@ -510,7 +475,7 @@
"description": "A list of values to iterate over",
"type": "array",
"items": {
"type": ["string", "number"]
"type": "string"
}
},
"for_attribute": {
@@ -635,7 +600,7 @@
"name": { "type": "string" },
"enum": { "type": "array", "items": { "type": "string" } }
},
"required": ["name"],
"required": ["name", "enum"],
"additionalProperties": false
}
]