Compare commits

..

25 Commits

Author SHA1 Message Date
Pete Davison
985280e580 feat: update completions 2026-07-03 13:00:25 +00:00
Pete Davison
5b92215af7 feat: remove REMOTE_TASKFILES from config and schema 2026-07-02 23:05:18 +00:00
Pete Davison
060ec4b454 fix: dead links 2026-07-02 23:04:41 +00:00
Pete Davison
9f6dfa7fbf feat: add remote taskfile snippets to guide 2026-07-02 17:22:26 +00:00
Pete Davison
09a9aec407 feat: delete old remote taskfiles experiment doc 2026-07-02 17:22:26 +00:00
Pete Davison
9571430fed feat: add remote taskfiles documentation 2026-07-02 17:22:26 +00:00
Pete Davison
63abdd080c feat: add configuration values for remote taskfiles 2026-07-02 17:22:26 +00:00
Pete Davison
0467e0d603 feat: guide formatting 2026-07-02 17:22:26 +00:00
Pete Davison
ff9c4f24a9 feat: released not stable 2026-07-02 17:22:26 +00:00
Pete Davison
468cacd4ca feat: improvement to warning when experiments are stable 2026-07-02 17:22:26 +00:00
Pete Davison
f2f0d62723 feat: remove experiment check for remote taskfiles 2026-07-02 17:22:26 +00:00
qingyingliu
b5861b1e27 fix: reject include without taskfile or dir (#2892)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-07-01 19:48:47 +00:00
Pete Davison
b5f671e098 feat: changelog for #2905 2026-07-01 14:46:48 +00:00
Pete Davison
c7cb5e1aaa feat: readd example hosted remote taskfile (#2905) 2026-07-01 15:44:33 +01:00
Valentin Maerten
11010d0d81 chore: changelog for #2904 2026-07-01 14:40:32 +02:00
Pete Davison
a6cedbe732 fix: remote taskfiles from ADO git path (#2904) 2026-07-01 14:38:38 +02:00
renovate[bot]
6cf0303ab8 chore(deps): update all non-major dependencies (#2889) 2026-06-30 23:19:27 +02:00
renovate[bot]
b779f852e8 chore(deps): update module golang.org/x/crypto to v0.52.0 [security] (#2903) 2026-06-30 23:11:06 +02:00
Valentin Maerten
48b215db0a ci: enable indirect Go deps and group action digest updates (#2902) 2026-06-30 21:01:24 +00:00
Valentin Maerten
0aa0496f85 ci: fix golangci-lint custom manager and enable OSV alerts in Renovate (#2901) 2026-06-30 20:50:31 +00:00
otjdiepluong
d601746d4b fix: handle nil Vars in ToCacheMap (#2842) 2026-06-30 22:32:07 +02:00
Valentin Maerten
e415d7135e fix(docs): wrap join example in v-pre to fix VitePress build
The inline `{{join " " .WORDS}}` example was parsed as a Vue
interpolation, which broke the website build. Wrap it in <span v-pre>
so it renders literally, matching the existing escaped example in the
same file.
2026-06-30 22:25:27 +02:00
Pete Davison
a61f8ade36 feat: update changelog for #2847 2026-06-29 18:27:59 +00:00
Valentin Maerten
9910c33557 fix(remote): define special variables behavior (#2847) 2026-06-29 19:25:25 +01:00
Markus
b93897a6c3 docs: clarify join argument order in templating reference (#2887)
Co-authored-by: Markus Stark <markusstark@MacBook-Air-von-Markus.local>
2026-06-29 17:08:44 +02:00
59 changed files with 1911 additions and 2784 deletions

17
.github/renovate.json vendored
View File

@@ -6,20 +6,31 @@
"schedule:weekly",
":semanticCommitTypeAll(chore)"
],
"mode": "full",
"addLabels":["area: dependencies"],
"osvVulnerabilityAlerts": true,
"postUpdateOptions": ["gomodTidy"],
"customManagers": [
{
"customType": "regex",
"fileMatch": ["^\\.github/workflows/.*\\.ya?ml$"],
"managerFilePatterns": ["/^\\.github/workflows/.*\\.ya?ml$/"],
"matchStrings": [
"uses:\\s*golangci/golangci-lint-action@\\S+\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
"uses:\\s*golangci/golangci-lint-action@\\S+(?:\\s*#[^\\n]*)?\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
],
"datasourceTemplate": "github-releases",
"depNameTemplate": "golangci/golangci-lint"
}
],
"packageRules": [
{
"matchManagers": ["gomod"],
"matchDepTypes": ["indirect"],
"enabled": true
},
{
"matchUpdateTypes": ["digest", "pinDigest"],
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch"
},
{
"matchManagers": ["github-actions"],
"addLabels": ["area: github actions"]

View File

@@ -19,21 +19,21 @@ jobs:
go-version: [1.25.10, 1.26.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: golangci-lint
uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.12.2
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: 3.14

View File

@@ -18,12 +18,12 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: 1.26.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with:
distribution: goreleaser-pro
version: latest

View File

@@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: 1.26.x
@@ -41,7 +41,7 @@ jobs:
run_install: "true"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with:
distribution: goreleaser-pro
version: latest

View File

@@ -25,42 +25,9 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version: ${{matrix.go-version}}
- name: Test
run: go run ./cmd/task test
completion:
name: Completion
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest]
runs-on: ${{matrix.platform}}
steps:
- name: Check out code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.x
# zsh and pwsh are preinstalled on the runners; only fish is missing
# (plus zsh on the Linux image).
- name: Install shells (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y zsh fish
- name: Install shells (macOS)
if: runner.os == 'macOS'
run: brew install fish
- name: Test completion
# Strict mode fails the run if any shell is missing, so we never get a
# false pass when a runner image stops shipping one (e.g. pwsh).
env:
TASK_COMPLETION_STRICT: "1"
run: go run ./cmd/task test:completion

View File

@@ -1,4 +1,3 @@
experiments:
GENTLE_FORCE: 0
REMOTE_TASKFILES: 0
ENV_PRECEDENCE: 0

View File

@@ -10,11 +10,11 @@
by @Legimity).
- PowerShell completions now work with aliases of the `task` command, not just
the `task` binary itself (#2852 by @kojiishi).
- Fixed task and namespace aliases not being completed by the Zsh completion.
A `show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
- Fixed task and namespace aliases not being completed by the Zsh completion. A
`show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
- Fixed task names containing certain characters (e.g. `\`, `_`, `^`) leaking
into checksum/timestamp filenames, breaking `sources:`/`generates:`
up-to-date detection (#2886 by @s3onghyun).
into checksum/timestamp filenames, breaking `sources:`/`generates:` up-to-date
detection (#2886 by @s3onghyun).
- Fixed `for: matrix:` loops using `ref:` rows producing wrong values when the
same task was run concurrently (e.g. by parallel `deps`) with different vars
(#2890, #2894 by @amitmishra11).
@@ -23,18 +23,20 @@
- Added the `use_gitignore` setting (global or per-task) to skip files matched
by your `.gitignore` when fingerprinting `sources`/`generates` and when
watching (#2773 by @vmaerten).
- Added support for configuring output flags (`--output`, `--output-group-begin`,
`--output-group-end`, `--output-group-error-only`) via the `TASK_OUTPUT*`
environment variables (#2873 by @liiight).
- Added support for configuring output flags (`--output`,
`--output-group-begin`, `--output-group-end`, `--output-group-error-only`) via
the `TASK_OUTPUT*` environment variables (#2873 by @liiight).
- Added a `--temp-dir` flag (with `TASK_TEMP_DIR` env var and `temp-dir` taskrc
config) to customise the directory where Task stores temporary files such as
checksums. Relative paths are resolved against the root Taskfile (#2891 by
@kjasn).
- Unified Bash, Fish, Zsh and PowerShell completions behind a single `task
__complete` engine, so every shell offers the same suggestions: task names,
aliases, flags, flag values and per-task CLI variables. The Zsh `show-aliases`
and `verbose` zstyles are preserved, now backed by the `--no-aliases` and
`--no-descriptions` completion flags (#2897 by @vmaerten).
- Defined environment variable behavior for remote taskfiles (#2267, #2847 by
@vmaerten).
- Added support for remote Taskfiles hosted on Azure DevOps, whose git URLs use
a `/_git/` path segment rather than a `.git` suffix (#2904 by @pd93).
- Re-added the example remote taskfile at
[taskfile.dev/Taskfile.yml](https://taskfile.dev/Taskfile.yml) (#2905 by
@pd93).
## v3.51.1 - 2026-05-16

View File

@@ -152,15 +152,6 @@ tasks:
cmds:
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
test:completion:
desc: Tests the shell completion engine and wrappers (bash, zsh, fish, powershell)
sources:
- internal/complete/**/*.go
- cmd/task/**/*.go
- completion/**/*
cmds:
- bash completion/tests/run.sh
goreleaser:test:
desc: Tests release process without publishing
cmds:

View File

@@ -1,55 +0,0 @@
package main
import (
"io"
"os"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/complete"
)
func runComplete(args []string) error {
// Strip the completion-control flags the wrapper prepends; the rest is the
// user's command line to complete.
opts, args := complete.ParseOptions(args)
dir, entrypoint, global := extractTaskfileFlags(args)
e := task.NewExecutor(
task.WithDir(dir),
task.WithEntrypoint(entrypoint),
task.WithStdout(io.Discard),
task.WithStderr(io.Discard),
task.WithVersionCheck(false),
)
if global {
if home, err := os.UserHomeDir(); err == nil {
e.Options(task.WithDir(home))
}
}
// Loading the Taskfile parses YAML (and may hit the network for remote
// Taskfiles), so skip it entirely when completing flags or their values.
// Best-effort: a missing or broken Taskfile must not break completion.
if complete.NeedsTaskfile(args, pflag.CommandLine) {
_ = e.Setup()
}
suggs, dirv := complete.Complete(e, pflag.CommandLine, args, opts)
complete.Write(os.Stdout, suggs, dirv)
return nil
}
func extractTaskfileFlags(args []string) (dir, entrypoint string, global bool) {
fs := pflag.NewFlagSet("complete", pflag.ContinueOnError)
fs.SetOutput(io.Discard)
fs.ParseErrorsAllowlist.UnknownFlags = true
fs.Usage = func() {}
fs.StringVarP(&dir, "dir", "d", "", "")
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
fs.BoolVarP(&global, "global", "g", false, "")
_ = fs.Parse(args)
return
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/complete"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
@@ -59,12 +58,6 @@ func emitCIErrorAnnotation(err error) {
}
func run() error {
// Dispatched before flag validation: the args after __complete are the
// user's command line, not Task's own flags.
if complete.IsActive() {
return runComplete(os.Args[2:])
}
log := &logger.Logger{
Stdout: os.Stdout,
Stderr: os.Stderr,

View File

@@ -15,6 +15,7 @@ import (
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -206,10 +207,19 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
// across platforms. This prevents issues with backslashes being interpreted
// as escape sequences when paths are used in shell commands on Windows.
var rootTaskfile, rootDir string
if taskfile.IsRemoteEntrypoint(c.Entrypoint) {
rootTaskfile = c.Entrypoint
rootDir = ""
} else {
rootTaskfile = filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint))
rootDir = filepath.ToSlash(c.Dir)
}
allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
"ROOT_DIR": filepath.ToSlash(c.Dir),
"ROOT_TASKFILE": rootTaskfile,
"ROOT_DIR": rootDir,
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
"TASK_VERSION": version.GetVersion(),
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
@@ -217,9 +227,22 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
}
if t != nil {
allVars["TASK"] = t.Task
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
if taskfile.IsRemoteEntrypoint(t.Location.Taskfile) {
allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = ""
switch {
case t.Dir == "":
allVars["TASK_DIR"] = filepath.ToSlash(c.UserWorkingDir)
case filepath.IsAbs(t.Dir):
allVars["TASK_DIR"] = filepath.ToSlash(t.Dir)
default:
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.UserWorkingDir, t.Dir))
}
} else {
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
}
} else {
allVars["TASK"] = ""
allVars["TASK_DIR"] = ""

135
compiler_internal_test.go Normal file
View File

@@ -0,0 +1,135 @@
package task
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
func TestGetSpecialVarsRemote(t *testing.T) {
t.Parallel()
uwd := t.TempDir()
uwdSlash := filepath.ToSlash(uwd)
localProj := filepath.Join(uwd, "proj")
localProjSlash := filepath.ToSlash(localProj)
localTaskfile := filepath.Join(localProj, "Taskfile.yml")
localTaskfileSlash := filepath.ToSlash(localTaskfile)
absTaskDir := filepath.Join(uwd, "opt", "work")
absTaskDirSlash := filepath.ToSlash(absTaskDir)
tests := []struct {
name string
entrypoint string
compilerDir string
taskDir string
taskfileLocation string
wantRootTaskfile string
wantRootDir string
wantTaskfile string
wantTaskfileDir string
wantTaskDir string
}{
{
name: "local entrypoint, local task",
entrypoint: localTaskfile,
compilerDir: localProj,
taskDir: "",
taskfileLocation: localTaskfile,
wantRootTaskfile: localTaskfileSlash,
wantRootDir: localProjSlash,
wantTaskfile: localTaskfileSlash,
wantTaskfileDir: localProjSlash,
wantTaskDir: localProjSlash,
},
{
name: "https entrypoint, empty task.dir",
entrypoint: "https://taskfile.dev/Taskfile.yml",
compilerDir: "",
taskDir: "",
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
wantRootDir: "",
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
wantTaskfileDir: "",
wantTaskDir: uwdSlash,
},
{
name: "https entrypoint, relative task.dir",
entrypoint: "https://taskfile.dev/Taskfile.yml",
compilerDir: "",
taskDir: "subdir",
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
wantRootDir: "",
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
wantTaskfileDir: "",
wantTaskDir: filepath.ToSlash(filepathext.SmartJoin(uwd, "subdir")),
},
{
name: "https entrypoint, absolute task.dir",
entrypoint: "https://taskfile.dev/Taskfile.yml",
compilerDir: "",
taskDir: absTaskDir,
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
wantRootDir: "",
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
wantTaskfileDir: "",
wantTaskDir: absTaskDirSlash,
},
{
name: "git entrypoint",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
compilerDir: "",
taskDir: "",
taskfileLocation: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
wantRootTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
wantRootDir: "",
wantTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
wantTaskfileDir: "",
wantTaskDir: uwdSlash,
},
{
name: "local root, remote included task",
entrypoint: localTaskfile,
compilerDir: localProj,
taskDir: "",
taskfileLocation: "https://taskfile.dev/included.yml",
wantRootTaskfile: localTaskfileSlash,
wantRootDir: localProjSlash,
wantTaskfile: "https://taskfile.dev/included.yml",
wantTaskfileDir: "",
wantTaskDir: uwdSlash,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := &Compiler{
Dir: tt.compilerDir,
Entrypoint: tt.entrypoint,
UserWorkingDir: uwd,
}
task := &ast.Task{
Task: "mytask",
Dir: tt.taskDir,
Location: &ast.Location{Taskfile: tt.taskfileLocation},
}
vars, err := c.getSpecialVars(task, nil)
assert.NoError(t, err)
assert.Equal(t, tt.wantRootTaskfile, vars["ROOT_TASKFILE"], "ROOT_TASKFILE")
assert.Equal(t, tt.wantRootDir, vars["ROOT_DIR"], "ROOT_DIR")
assert.Equal(t, tt.wantTaskfile, vars["TASKFILE"], "TASKFILE")
assert.Equal(t, tt.wantTaskfileDir, vars["TASKFILE_DIR"], "TASKFILE_DIR")
assert.Equal(t, tt.wantTaskDir, vars["TASK_DIR"], "TASK_DIR")
})
}
}

View File

@@ -1,81 +1,60 @@
# vim: set tabstop=2 shiftwidth=2 expandtab:
#
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
_GO_TASK_COMPLETION_LIST_OPTION='--list-all'
TASK_CMD="${TASK_EXE:-task}"
_task() {
function _task()
{
local cur prev words cword
_init_completion -n : || return
# Completion directives, mirroring internal/complete/complete.go.
local -ri NO_SPACE=2 NO_FILE_COMP=4 FILTER_FILE_EXT=8 FILTER_DIRS=16
# Exclude both `=` and `:` from the word breaks so `--output=` and
# `docs:serve` reach the engine as single tokens.
_init_completion -n =: || return
local -a args=()
if (( cword > 0 )); then
args=( "${words[@]:1:cword}" )
fi
if (( ${#args[@]} == 0 )); then
args=( "" )
fi
local output
output=$("$TASK_CMD" __complete "${args[@]}" 2>/dev/null)
if [[ -z "$output" ]]; then
_filedir
return
fi
local -a lines=()
local line
while IFS= read -r line; do
lines+=( "$line" )
done <<< "$output"
local last_idx=$(( ${#lines[@]} - 1 ))
local directive="${lines[$last_idx]#:}"
unset 'lines[$last_idx]'
if (( directive & FILTER_FILE_EXT )); then
local exts=""
# ${arr[@]+…} guards against "unbound variable" on an empty array under
# `set -u` in bash 3.2 (macOS).
for line in ${lines[@]+"${lines[@]}"}; do
exts+="${exts:+|}$line"
done
_filedir "@($exts)"
return
fi
if (( directive & FILTER_DIRS )); then
_filedir -d
return
fi
# Prefix-filter by hand instead of `compgen -W`: the latter joins/splits the
# word list on IFS, which mangles any suggestion value containing a space.
local value
COMPREPLY=()
for line in ${lines[@]+"${lines[@]}"}; do
value="${line%%$'\t'*}"
if [[ -z "$cur" || "$value" == "$cur"* ]]; then
COMPREPLY+=( "$value" )
# Check for `--` within command-line and quit or strip suffix.
local i
for i in "${!words[@]}"; do
if [ "${words[$i]}" == "--" ]; then
# Do not complete words following `--` passed to CLI_ARGS.
[ $cword -gt $i ] && return
# Remove the words following `--` to not put --list in CLI_ARGS.
words=( "${words[@]:0:$i}" )
break
fi
done
if (( directive & NO_SPACE )); then
compopt -o nospace 2>/dev/null
fi
# Handle special arguments of options.
case "$prev" in
-d|--dir|--remote-cache-dir)
_filedir -d
return $?
;;
--cacert|--cert|--cert-key)
_filedir
return $?
;;
-t|--taskfile)
_filedir yaml || return $?
_filedir yml
return $?
;;
-o|--output)
COMPREPLY=( $( compgen -W "interleaved group prefixed" -- $cur ) )
return 0
;;
esac
# Handle normal options.
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "$(_parse_help $1)" -- $cur ) )
return 0
;;
esac
# Prepare task name completions.
local tasks=( $( "${words[@]}" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) )
COMPREPLY=( $( compgen -W "${tasks[*]}" -- "$cur" ) )
# Post-process because task names might contain colons.
__ltrim_colon_completions "$cur"
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & NO_FILE_COMP )); then
_filedir
fi
}
complete -F _task "$TASK_CMD"

View File

@@ -1,91 +1,116 @@
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
# Completion directives, mirroring internal/complete/complete.go. fish's `math`
# has no bitwise operators, so bits are stored as their power-of-two value and
# tested with integer division + modulo via __task_test_bit.
set -g __task_directive_no_space 2
set -g __task_directive_no_file_comp 4
set -g __task_directive_filter_file_ext 8
set -g __task_directive_filter_dirs 16
set -g __task_directive_keep_order 32
# Cache variables for experiments (global)
set -g __task_experiments_cache ""
set -g __task_experiments_cache_time 0
function __task_test_bit --argument-names value bit
test (math "floor($value / $bit) % 2") -eq 1
end
# Helper function to get experiments with 1-second cache
function __task_get_experiments --inherit-variable GO_TASK_PROGNAME
set -l now (date +%s)
set -l ttl 1 # Cache for 1 second only
function __task_complete --inherit-variable GO_TASK_PROGNAME
set -l tokens (commandline -opc)
set -l current (commandline -ct)
set -l args
if test (count $tokens) -gt 1
set args $tokens[2..-1]
end
set args $args $current
set -l output ($GO_TASK_PROGNAME __complete $args 2>/dev/null)
set -l count (count $output)
if test $count -eq 0
return
end
set -l last $output[$count]
if not string match -q ':*' -- $last
# Protocol violation: emit raw lines as a fallback.
printf '%s\n' $output
return
end
set -l directive (string replace -r '^:' '' -- $last)
set -l data
if test $count -gt 1
set data $output[1..(math $count - 1)]
end
# The main completion is registered with `--no-files`, which disables fish's
# native file fallback. Every file-completion directive must therefore be
# served here, otherwise nothing is offered (e.g. `--cacert`, after `--`).
# __fish_complete_suffix only *prioritizes* the extension rather than
# filtering, so filter the file list ourselves (keeping dirs to descend into).
if __task_test_bit $directive $__task_directive_filter_file_ext
for entry in (__fish_complete_path $current)
set -l name (string split -f1 \t -- $entry)
if string match -qr '/$' -- $name
printf '%s\n' $entry
continue
end
for ext in $data
if string match -qr "\.$ext\$" -- $name
printf '%s\n' $entry
break
end
end
# Return cached value if still valid
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
printf '%s\n' $__task_experiments_cache
return
end
# Refresh cache
set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null)
set -g __task_experiments_cache_time $now
printf '%s\n' $__task_experiments_cache
end
# Helper function to check if an experiment is enabled
function __task_is_experiment_enabled
set -l experiment $argv[1]
__task_get_experiments | string match -qr "^\* $experiment:.*on"
end
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
# Check if the global task is requested
set -l global_task false
commandline --current-process | read --tokenize --list --local cmd_args
for arg in $cmd_args
if test "_$arg" = "_--"
break # ignore arguments to be passed to the task
end
if test "_$arg" = "_--global" -o "_$arg" = "_-g"
set global_task true
break
end
end
# Read the list of tasks (and potential errors)
if $global_task
$GO_TASK_PROGNAME --global --list-all
else
$GO_TASK_PROGNAME --list-all
end 2>&1 | read -lz rawOutput
# Return on non-zero exit code (for cases when there is no Taskfile found or etc.)
if test $status -ne 0
return
end
if __task_test_bit $directive $__task_directive_filter_dirs
__fish_complete_directories $current
return
end
# Emit the candidates verbatim; fish reads the tab as the value/description
# separator.
for line in $data
printf '%s\n' $line
end
# NoFileComp unset → also offer files, since `--no-files` suppressed the
# native fallback. Covers DirectiveDefault (e.g. `--cacert`, after `--`).
if not __task_test_bit $directive $__task_directive_no_file_comp
__fish_complete_path $current
# 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)
if test $output
echo $output
end
end
# Single registration: all task names, flags, flag values and file completion
# flow through the engine. `--no-files` prevents fish from mixing in files when
# the engine says not to (NoFileComp); `__task_complete` re-adds them otherwise.
complete -c $GO_TASK_PROGNAME --no-files -a "(__task_complete)"
complete -c $GO_TASK_PROGNAME \
-d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \
-xa "(__task_get_tasks)" \
-n "not __fish_seen_subcommand_from --"
# Standard flags
complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks'
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks'
complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell"
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution'
complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names'
complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing'
complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command'
complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments'
complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails'
complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date'
complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help'
complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile'
complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads'
complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes'
complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON'
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks'
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing'
complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none"
complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date'
complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary'
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run'
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output'
complete -c $GO_TASK_PROGNAME -l version -d 'show version'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes'
complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts'
complete -c $GO_TASK_PROGNAME -l offline -d 'use only local or cached Taskfiles'
complete -c $GO_TASK_PROGNAME -l timeout -d 'timeout for remote Taskfile downloads'
complete -c $GO_TASK_PROGNAME -l expiry -d 'cache expiry duration'
complete -c $GO_TASK_PROGNAME -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
complete -c $GO_TASK_PROGNAME -l cacert -d 'custom CA certificate for TLS' -r
complete -c $GO_TASK_PROGNAME -l cert -d 'client certificate for mTLS' -r
complete -c $GO_TASK_PROGNAME -l cert-key -d 'client certificate private key' -r
complete -c $GO_TASK_PROGNAME -l download -d 'download remote Taskfile'
complete -c $GO_TASK_PROGNAME -l clear-cache -d 'clear remote Taskfile cache'
# Experimental flags (dynamically checked at completion time via -n condition)
# GentleForce experiment
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'

View File

@@ -1,171 +0,0 @@
// Package completion_test black-box tests the `task __complete` wire protocol:
// the candidates and directive the real binary emits for a command line. The
// shell wrappers only need to be smoke-tested for how they interpret the
// directive (see completion/tests/wrapper.*).
package completion_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/complete"
)
var taskBin string
func TestMain(m *testing.M) {
dir, err := os.MkdirTemp("", "task-completion-test")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
taskBin = filepath.Join(dir, "task")
if runtime.GOOS == "windows" {
taskBin += ".exe"
}
if out, err := exec.Command("go", "build", "-o", taskBin, "github.com/go-task/task/v3/cmd/task").CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "failed to build task binary: %v\n%s", err, out)
os.RemoveAll(dir)
os.Exit(1)
}
code := m.Run()
os.RemoveAll(dir)
os.Exit(code)
}
const fixtureTaskfile = `version: '3'
tasks:
build:
desc: Build it
deploy:
desc: Deploy the application
aliases: [dep, ship]
requires:
vars:
- name: ENV
enum: [dev, staging, prod]
- REGION
docs:serve:
desc: Serve docs locally
`
// completeArgs runs `task __complete <args>` in a fresh fixture directory and
// returns the offered candidate values plus the emitted directive.
func completeArgs(t *testing.T, args ...string) ([]string, complete.Directive) {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(fixtureTaskfile), 0o644))
cmd := exec.Command(taskBin, append([]string{complete.CommandName}, args...)...)
cmd.Dir = dir
out, err := cmd.Output()
require.NoError(t, err)
lines := strings.Split(strings.TrimRight(string(out), "\n"), "\n")
require.NotEmpty(t, lines, "protocol output must end with a directive line")
last := lines[len(lines)-1]
require.True(t, strings.HasPrefix(last, ":"), "last line must be the :<directive> line, got %q", last)
n, err := strconv.Atoi(strings.TrimPrefix(last, ":"))
require.NoError(t, err)
values := make([]string, 0, len(lines)-1)
for _, line := range lines[:len(lines)-1] {
values = append(values, strings.SplitN(line, "\t", 2)[0])
}
return values, complete.Directive(n)
}
func TestProtocol(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
want []string // candidate values that must be offered
absent []string // candidate values that must NOT be offered
directive complete.Directive
}{
{
name: "task names and aliases",
args: []string{""},
want: []string{"build", "deploy", "dep", "ship", "docs:serve"},
directive: complete.DirectiveNoFileComp,
},
{
name: "no-aliases drops aliases",
args: []string{"--no-aliases", ""},
want: []string{"build", "deploy"},
absent: []string{"dep", "ship"},
directive: complete.DirectiveNoFileComp,
},
{
name: "flag names",
args: []string{"-"},
want: []string{"--taskfile", "--dir", "--output"},
directive: complete.DirectiveNoFileComp,
},
{
name: "separate flag value is bare",
args: []string{"--output", ""},
want: []string{"interleaved", "group", "prefixed"},
directive: complete.DirectiveNoFileComp,
},
{
name: "inline flag value is full form",
args: []string{"--output="},
want: []string{"--output=interleaved", "--output=group", "--output=prefixed"},
directive: complete.DirectiveNoFileComp,
},
{
name: "sort enum values",
args: []string{"--sort", ""},
want: []string{"default", "alphanumeric", "none"},
directive: complete.DirectiveNoFileComp,
},
{
name: "taskfile filters by extension",
args: []string{"--taskfile", ""},
want: []string{"yml", "yaml"},
directive: complete.DirectiveFilterFileExt,
},
{
name: "dir filters to directories",
args: []string{"--dir", ""},
directive: complete.DirectiveFilterDirs,
},
{
name: "task variables keep order and suppress the space",
args: []string{"deploy", ""},
want: []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="},
directive: complete.DirectiveNoSpace | complete.DirectiveNoFileComp | complete.DirectiveKeepOrder,
},
{
name: "after -- yields default file completion",
args: []string{"deploy", "--", ""},
directive: complete.DirectiveDefault,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
values, directive := completeArgs(t, tt.args...)
require.Equal(t, tt.directive, directive)
require.Subset(t, values, tt.want)
for _, a := range tt.absent {
require.NotContains(t, values, a)
}
})
}
}

View File

@@ -1,88 +1,89 @@
using namespace System.Management.Automation
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique
Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
$TaskExe = if ($env:TASK_EXE) { $env:TASK_EXE } else { 'task' }
if ($commandName.StartsWith('-')) {
$completions = @(
# Standard flags (alphabetical order)
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),
[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),
[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),
[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),
[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),
[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),
[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes'),
[CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles'),
[CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout'),
[CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry'),
[CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory'),
[CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate'),
[CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate'),
[CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key'),
[CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile'),
[CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')
)
# Words after the program name, truncated to the cursor.
$argsToPass = @()
$elements = $commandAst.CommandElements
if ($elements.Count -gt 1) {
for ($i = 1; $i -lt $elements.Count; $i++) {
$el = $elements[$i]
if ($el.Extent.StartOffset -ge $cursorPosition) { break }
$argsToPass += $el.ToString()
# Experimental flags (dynamically added based on enabled experiments)
$experiments = & task --experiments 2>$null | Out-String
if ($experiments -match '\* GENTLE_FORCE:.*on') {
$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')
}
}
# The trailing word (possibly empty) must reach the engine so it knows
# the cursor sits on a fresh word. It is already present when it coincides
# with the last command element captured above.
if ($argsToPass.Count -eq 0 -or $argsToPass[-1] -ne $wordToComplete) {
$argsToPass += $wordToComplete
return $completions.Where{ $_.CompletionText.StartsWith($commandName) }
}
$output = & $TaskExe __complete @argsToPass 2>$null
if (-not $output) { return }
$lines = @($output)
if ($lines.Count -eq 0) { return }
$last = $lines[-1]
if (-not $last.StartsWith(':')) { return }
$directive = [int]($last.Substring(1))
$data = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
# Completion directives, mirroring internal/complete/complete.go.
$NoFileComp = 4
$FilterFileExt = 8
$FilterDirs = 16
# Note: DirectiveNoSpace (bit 2) cannot be honored here — PowerShell's
# CompletionResult API has no per-item "no trailing space" option, so a
# suggestion like `VAR=` gets a trailing space. This is a PowerShell limit.
# FilterFileExt: keep files whose extension matches, plus directories so the
# user can still descend into them. `-Include` is unreliable without
# `-Recurse`, so filter with Where-Object instead.
if ($directive -band $FilterFileExt) {
$exts = $data | ForEach-Object { ".$_" }
return Get-ChildItem -Path "$wordToComplete*" -ErrorAction SilentlyContinue |
Where-Object { $_.PSIsContainer -or $exts -contains $_.Extension } |
ForEach-Object {
$type = if ($_.PSIsContainer) { [CompletionResultType]::ProviderContainer } else { [CompletionResultType]::ProviderItem }
[CompletionResult]::new($_.Name, $_.Name, $type, $_.Name)
}
}
# FilterDirs
if ($directive -band $FilterDirs) {
return Get-ChildItem -Path "$wordToComplete*" -Directory -ErrorAction SilentlyContinue |
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderContainer, $_.Name) }
}
# Build candidates, filtering by the current word. PowerShell does not filter
# native argument-completer results itself, so without this every suggestion
# would be offered regardless of what the user typed.
$results = @($data | ForEach-Object {
$parts = $_ -split "`t", 2
$value = $parts[0]
if ($wordToComplete -and -not $value.StartsWith($wordToComplete)) { return }
$desc = if ($parts.Count -gt 1 -and $parts[1]) { $parts[1] } else { $value }
[CompletionResult]::new($value, $value, [CompletionResultType]::ParameterValue, $desc)
})
# NoFileComp (bit 4) unset and nothing matched → fall back to file completion,
# since the engine returned DirectiveDefault (e.g. --cacert, after `--`).
if ($results.Count -eq 0 -and -not ($directive -band $NoFileComp)) {
return Get-ChildItem -Path . -ErrorAction SilentlyContinue |
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
}
return $results
return $(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + " " }
}

View File

@@ -1,97 +0,0 @@
#!/usr/bin/env bash
# Runs the completion test suite: builds the task binary, creates a fixture
# Taskfile with sample files and directories, then exercises the engine and
# every installed shell wrapper. Skips shells that are not installed.
set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
root=$(cd "$here/../.." && pwd)
# Temp dirs for the binary and the fixture; removed on exit (including on early
# failure via the trap).
bindir=$(mktemp -d)
fixture=$(mktemp -d)
trap 'rm -rf "$bindir" "$fixture"' EXIT
# Build the binary under test.
if ! go build -o "$bindir/task" "$root/cmd/task"; then
echo "failed to build task binary" >&2
exit 1
fi
export TASK_BIN="$bindir/task"
# fish and PowerShell register completion for the command name `task`, so make
# `task` on PATH resolve to the binary under test.
export PATH="$bindir:$PATH"
# Fixture: a Taskfile plus files/dirs so file/dir completion has real entries.
cat > "$fixture/Taskfile.yml" <<'YML'
version: '3'
tasks:
build:
desc: Build it
deploy:
desc: Deploy it
aliases: [dep]
requires:
vars:
- name: ENV
enum: [dev, prod]
- REGION
docs:serve:
desc: Serve docs
YML
touch "$fixture/extra.yaml" "$fixture/notes.txt"
mkdir -p "$fixture/sub" "$fixture/other"
export TASK_FIXTURE="$fixture"
# In strict mode (set TASK_COMPLETION_STRICT=1, used in CI) a missing shell is
# a failure instead of a skip, so we never get a false pass when a shell the
# environment was expected to provide (e.g. pwsh on CI runners) is absent.
strict=${TASK_COMPLETION_STRICT:-}
fails=0
run() { # LABEL COMMAND...
echo "== $1 =="
if "${@:2}"; then :; else fails=$((fails + 1)); fi
echo
}
skip() { # LABEL
if [[ -n "$strict" ]]; then
echo "== $1 == (MISSING — required under TASK_COMPLETION_STRICT)"
fails=$((fails + 1))
else
echo "== $1 == (skipped: not installed)"
fi
echo
}
# The engine/protocol itself is covered by the Go tests (completion/protocol_test.go
# and internal/complete); these smokes only check how each shell wrapper
# interprets the directive.
run "bash wrapper" bash "$here/wrapper.bash"
if command -v zsh >/dev/null 2>&1; then
run "zsh wrapper" zsh "$here/wrapper.zsh"
else
skip "zsh wrapper"
fi
if command -v fish >/dev/null 2>&1; then
run "fish wrapper" fish "$here/wrapper.fish"
else
skip "fish wrapper"
fi
pwsh_bin=$(command -v pwsh || command -v pwsh-preview || true)
if [[ -n "$pwsh_bin" ]]; then
run "powershell wrapper" "$pwsh_bin" -NoProfile -File "$here/wrapper.ps1"
else
skip "powershell wrapper"
fi
if ((fails)); then
echo "completion tests: $fails suite(s) failed"
exit 1
fi
echo "completion tests: all suites passed"

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env bash
# Smoke-tests how the bash wrapper routes each directive by stubbing the
# bash-completion helpers (_filedir / compopt / …) and asserting what it calls.
# Suggestion logic lives in the Go tests. Requires TASK_BIN and TASK_FIXTURE.
set -u
: "${TASK_BIN:?}"; : "${TASK_FIXTURE:?}"
export TASK_EXE="$TASK_BIN"
cd "$TASK_FIXTURE" || exit 1
fails=0
CAP=""
# Stubs standing in for the bash-completion runtime.
_init_completion() {
words=("${TEST_WORDS[@]}")
cword=$TEST_CWORD
cur="${TEST_WORDS[$TEST_CWORD]}"
prev="${TEST_WORDS[$((TEST_CWORD - 1))]}"
return 0
}
_filedir() { CAP+="filedir:$*"$'\n'; }
compopt() { CAP+="compopt:$*"$'\n'; }
__ltrim_colon_completions() { :; }
source "$(dirname "${BASH_SOURCE[0]}")/../bash/task.bash"
run() {
CAP=""
TEST_WORDS=("$@")
TEST_CWORD=$((${#TEST_WORDS[@]} - 1))
COMPREPLY=()
_task
}
reply_has() { # LABEL VALUE
local v
for v in "${COMPREPLY[@]}"; do [[ "$v" == "$2" ]] && { echo " ok $1"; return; }; done
echo " FAIL $1 — '$2' missing from COMPREPLY: ${COMPREPLY[*]}"
fails=$((fails + 1))
}
cap_has() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then echo " ok $1"; else
echo " FAIL $1 — expected '$2' in: $CAP"; fails=$((fails + 1)); fi
}
cap_hasnot() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then
echo " FAIL $1 — '$2' should be absent in: $CAP"; fails=$((fails + 1)); else
echo " ok $1"; fi
}
echo "bash: :4 (NoFileComp) forwards candidates, no file fallback"
run task ''
reply_has "candidate forwarded" build
cap_hasnot "no file fallback" "filedir:"
echo "bash: :2 (NoSpace) disables the trailing space"
run task deploy ''
cap_has "nospace applied" "compopt:-o nospace"
echo "bash: :8 (FilterFileExt) routes to extension-filtered files"
run task --taskfile ''
cap_has "filedir ext glob" "filedir:@(yml|yaml)"
echo "bash: :16 (FilterDirs) routes to directory completion"
run task --dir ''
cap_has "filedir -d" "filedir:-d"
echo "bash: :0 (Default) falls back to files"
run task build -- ''
cap_has "filedir default" "filedir:"
if ((fails)); then
echo "bash: $fails failure(s)"
exit 1
fi
echo "bash: all passed"

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env fish
# Smoke-tests how the fish wrapper routes each directive, via `complete -C`
# (real completions, no TTY). Suggestion logic lives in the Go tests.
# Set up by run.sh: TASK_FIXTURE, and `task` on PATH = the binary under test.
cd $TASK_FIXTURE
source (dirname (status -f))/../fish/task.fish
set -g fails 0
function cands
complete -C $argv[1] | string split -f1 \t
end
function has # LABEL LINE VALUE
if contains -- $argv[3] (cands $argv[2])
echo " ok $argv[1]"
else
echo " FAIL $argv[1] — '$argv[3]' missing from: "(cands $argv[2])
set fails (math $fails + 1)
end
end
function hasnot # LABEL LINE VALUE
if contains -- $argv[3] (cands $argv[2])
echo " FAIL $argv[1] — '$argv[3]' should be absent"
set fails (math $fails + 1)
else
echo " ok $argv[1]"
end
end
echo "fish: :4 (NoFileComp) forwards candidates, offers no files"
has "candidate forwarded" 'task ' build
hasnot "no file fallback" 'task ' notes.txt
echo "fish: :16 (FilterDirs) offers directories only"
has "dir offered" 'task --dir ' sub/
hasnot "no plain file" 'task --dir ' notes.txt
echo "fish: :8 (FilterFileExt) filters by extension"
has "matching file" 'task --taskfile ' Taskfile.yml
hasnot "non-matching file" 'task --taskfile ' notes.txt
echo "fish: :0 (Default) falls back to files"
has "file offered" 'task build -- ' notes.txt
if test $fails -ne 0
echo "fish: $fails failure(s)"
exit 1
end
echo "fish: all passed"

View File

@@ -1,54 +0,0 @@
# Smoke-tests how the PowerShell wrapper routes each directive (plus its own
# prefix filtering), via the completion API (real completions, no TTY).
# Suggestion logic lives in the Go tests. Set up by run.sh: $env:TASK_FIXTURE,
# and `task` on PATH = the binary under test.
Set-Location $env:TASK_FIXTURE
. "$PSScriptRoot/../ps/task.ps1"
$fails = 0
function Cands($line) {
([System.Management.Automation.CommandCompletion]::CompleteInput($line, $line.Length, $null)).CompletionMatches |
ForEach-Object { $_.CompletionText }
}
function Has($label, $line, $value) {
if ((Cands $line) -contains $value) {
Write-Output " ok $label"
} else {
Write-Output " FAIL $label — '$value' missing from: $((Cands $line) -join ' ')"
$script:fails++
}
}
function HasNot($label, $line, $value) {
if ((Cands $line) -contains $value) {
Write-Output " FAIL $label — '$value' should be absent"
$script:fails++
} else {
Write-Output " ok $label"
}
}
Write-Output "powershell: :4 (NoFileComp) forwards candidates, offers no files"
Has "candidate forwarded" 'task ' 'build'
HasNot "no file fallback" 'task ' 'notes.txt'
Write-Output "powershell: filters candidates by the current word"
Has "prefix keeps match" 'task b' 'build'
HasNot "prefix drops others" 'task b' 'deploy'
Write-Output "powershell: :16 (FilterDirs) offers directories only"
Has "dir offered" 'task --dir ' 'sub'
HasNot "no plain file" 'task --dir ' 'notes.txt'
Write-Output "powershell: :8 (FilterFileExt) filters by extension"
Has "matching file" 'task --taskfile ' 'Taskfile.yml'
HasNot "non-matching file" 'task --taskfile ' 'notes.txt'
if ($fails -ne 0) {
Write-Output "powershell: $fails failure(s)"
exit 1
}
Write-Output "powershell: all passed"

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env zsh
# Smoke-tests how the zsh wrapper routes each directive by stubbing the
# completion functions (_describe / _files / _path_files) and asserting what it
# calls. Suggestion logic lives in the Go tests. Requires TASK_BIN, TASK_FIXTURE.
export TASK_EXE=$TASK_BIN
cd $TASK_FIXTURE
integer fails=0
local CAP
compdef() { } # no-op: we call _task directly, not through compinit
_describe() {
local arr=$4
CAP+="describe_opts:${@[5,-1]}"$'\n'
local c; for c in ${(P)arr}; do CAP+="cand:$c"$'\n'; done
}
_files() { CAP+="files:$*"$'\n' }
_path_files() { CAP+="path_files:$*"$'\n' }
# Sourcing (not autoloading) defines _task and avoids the autoload first-call
# quirk; the trailing `compdef` call is stubbed above.
source ${0:A:h}/../zsh/_task
run() {
CAP=""
local -a words=("$@")
integer CURRENT=$#words
local curcontext=":completion:complete:task:"
_task
}
has() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then
echo " ok $1"
else
echo " FAIL $1 — expected '$2' in:"$'\n'"$CAP"
(( fails++ ))
fi
}
hasnot() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then
echo " FAIL $1 — '$2' should be absent in:"$'\n'"$CAP"
(( fails++ ))
else
echo " ok $1"
fi
}
echo "zsh: :4 (NoFileComp) forwards candidates, no file fallback"
run task ''
has "candidate forwarded" "cand:build"
hasnot "no file fallback" "files:"
echo "zsh: :2|:32 (NoSpace|KeepOrder) map to -S and -V"
run task deploy ''
has "NoSpace -> -S" "describe_opts:-S"
has "KeepOrder -> -V" "-V"
echo "zsh: :8 (FilterFileExt) routes to extension-filtered files"
run task --taskfile ''
has "files glob" "files:"
has "yml in glob" "yml"
echo "zsh: :16 (FilterDirs) routes to directory completion"
run task --dir ''
has "path_files -/" "path_files:-/"
echo "zsh: :0 (Default) falls back to files"
run task build -- ''
has "files default" "files:"
if (( fails )); then
echo "zsh: $fails failure(s)"
exit 1
fi
echo "zsh: all passed"

View File

@@ -1,74 +1,158 @@
#compdef task
#
# Thin wrapper around `task __complete`. All suggestion logic lives in the
# Go engine — do not add completion logic here.
typeset -A opt_args
TASK_CMD="${TASK_EXE:-task}"
compdef _task "$TASK_CMD"
_task() {
local -a args lines completions opts ctl
local output directive line
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
# Completion directives, mirroring internal/complete/complete.go.
local -ri NO_SPACE=2 NO_FILE_COMP=4 FILTER_FILE_EXT=8 FILTER_DIRS=16 KEEP_ORDER=32
# Map the zsh completion zstyles to engine flags. `-T` is true when the
# style is unset (its default) or explicitly true, so a flag is only passed
# when the user turned the style off.
zstyle -T ":completion:${curcontext}:" show-aliases || ctl+=(--no-aliases)
zstyle -T ":completion:${curcontext}:" verbose || ctl+=(--no-descriptions)
# (@) preserves a trailing empty string, which the engine relies on to
# know the cursor is on a fresh word.
args=("${(@)words[2,CURRENT]}")
(( ${#args} == 0 )) && args=("")
output=$("$TASK_CMD" __complete "${ctl[@]}" "${args[@]}" 2>/dev/null)
if [[ -z "$output" ]]; then
_files
return
fi
lines=("${(f)output}")
directive="${lines[-1]#:}"
lines=("${(@)lines[1,-2]}")
if (( directive & FILTER_FILE_EXT )); then
local -a globs
for line in "${lines[@]}"; do
globs+=("*.${line}")
done
_files -g "(${(j:|:)globs})"
return
fi
if (( directive & FILTER_DIRS )); then
_path_files -/
return
fi
# `:` inside the value must be escaped: _describe splits on the first
# unescaped colon (e.g. "docs:serve" would otherwise become value "docs").
local value desc
for line in "${lines[@]}"; do
if [[ "$line" == *$'\t'* ]]; then
value="${line%%$'\t'*}"
desc="${line#*$'\t'}"
completions+=("${value//:/\\:}:$desc")
else
completions+=("${line//:/\\:}")
fi
done
(( directive & NO_SPACE )) && opts+=(-S '')
(( directive & KEEP_ORDER )) && opts+=(-V)
if (( ${#completions} > 0 )); then
_describe -t tasks 'task' completions "${opts[@]}"
fi
(( directive & NO_FILE_COMP )) && return
_files
# Check if an experiment is enabled
function __task_is_experiment_enabled() {
local experiment=$1
task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on"
}
compdef _task "$TASK_CMD"
# Listing commands from Taskfile.yml
function __task_list() {
local -a scripts cmd task_aliases match mbegin mend
local -i enabled=0
local taskfile item task desc task_alias
cmd=($TASK_CMD)
taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
taskfile=${taskfile//\~/$HOME}
for arg in "${words[@]:0:$CURRENT}"; do
if [[ "$arg" = "--" ]]; then
# Use default completion for words after `--` as they are CLI_ARGS.
_default
return 0
fi
done
if [[ -n "$taskfile" && -f "$taskfile" ]]; then
cmd+=(--taskfile "$taskfile")
fi
# Check if global flag is set
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
cmd+=(--global)
fi
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
enabled=1
fi
(( 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
# Read zstyle show-aliases option (default = true via -T)
local show_aliases
zstyle -T ":completion:${curcontext}:" show-aliases && show_aliases=true || show_aliases=false
for item in "${(@)${(f)output}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
# Extract the aliases listed in the trailing "(aliases: a, b)" column.
# NB: `aliases` is a reserved zsh parameter, so use a different name.
task_aliases=()
if [[ "$show_aliases" == "true" && "$item" == (#b)*'(aliases: '(*)')' ]]; then
task_aliases=( "${(@s:, :)match[1]}" )
fi
if [[ "$show_desc" == "true" ]]; then
local desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
for task_alias in $task_aliases; do
scripts+=( "${task_alias//:/\\:}:$desc (alias of $task)" )
done
else
scripts+=( "$task" )
for task_alias in $task_aliases; do
scripts+=( "$task_alias" )
done
fi
done
if [[ "$show_desc" == "true" ]]; then
_describe 'Task to run' scripts
else
compadd -Q -a scripts
fi
}
_task() {
local -a standard_args operation_args
standard_args=(
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'
'(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'
'(-f --force)'{-f,--force}'[run even if task is up-to-date]'
'(-c --color)'{-c,--color}'[colored output]'
'(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'
'(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'
'(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'
'(--dry)--dry[dry-run mode, compile and print tasks only]'
'(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'
'(--experiments)--experiments[list available experiments]'
'(-g --global)'{-g,--global}'[run global Taskfile from home directory]'
'(--insecure)--insecure[allow insecure Taskfile downloads]'
'(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '
'(-j --json)'{-j,--json}'[format task list as JSON]'
'(--nested)--nested[nest namespaces when listing as JSON]'
'(--no-status)--no-status[ignore status when listing as JSON]'
'(--interactive)--interactive[prompt for missing required variables]'
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
'(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'
'(-s --silent)'{-s,--silent}'[disable echoing]'
'(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'
'(--status)--status[exit non-zero if supplied tasks not up-to-date]'
'(--summary)--summary[show summary\: field from tasks instead of running them]'
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'
'(-v --verbose)'{-v,--verbose}'[verbose mode]'
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'
'(-y --yes)'{-y,--yes}'[assume yes to all prompts]'
'(--offline --clear-cache)--download[download remote Taskfile]'
'(--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'
)
# Experimental flags (dynamically added based on enabled experiments)
# Options (modify behavior)
if __task_is_experiment_enabled "GENTLE_FORCE"; then
standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')
fi
operation_args=(
# Task names completion (can be specified multiple times)
'(operation)*: :__task_list'
# Operational args completion (mutually exclusive)
+ '(operation)'
'(*)'{-l,--list}'[list describable tasks]'
'(*)'{-a,--list-all}'[list all tasks]'
'(*)'{-i,--init}'[create new Taskfile.yml]'
'(- *)'{-h,--help}'[show help]'
'(- *)--version[show version and exit]'
'(* --download)--clear-cache[clear remote Taskfile cache]'
)
_arguments -S $standard_args $operation_args
}
# don't run the completion function when being source-ed or eval-ed
if [ "$funcstack[1]" = "_task" ]; then
_task "$@"
fi

View File

@@ -1,6 +1,7 @@
package experiments
import (
"cmp"
"fmt"
"strconv"
"strings"
@@ -24,12 +25,15 @@ func (err InvalidValueError) Error() string {
}
type InactiveError struct {
Name string
Name string
Reason string
}
func (err InactiveError) Error() string {
reason := cmp.Or(err.Reason, "is inactive and cannot be enabled")
return fmt.Sprintf(
"task: Experiment %q is inactive and cannot be enabled",
"task: Experiment %q %s",
err.Name,
reason,
)
}

View File

@@ -9,27 +9,54 @@ import (
)
type Experiment struct {
Name string // The name of the experiment.
AllowedValues []int // The values that can enable this experiment.
Value int // The version of the experiment that is enabled.
Name string // The name of the experiment.
AllowedValues []int // The values that can enable this experiment.
Value int // The version of the experiment that is enabled.
InactiveReason string // If not active, the reason why it is inactive.
}
func getValue(xName string, config *ast.TaskRC) int {
var value int
if config != nil {
value = config.Experiments[xName]
}
if value == 0 {
value, _ = strconv.Atoi(getEnv(xName))
}
return value
}
// New creates a new experiment with the given name and sets the values that can
// enable it.
func New(xName string, config *ast.TaskRC, allowedValues ...int) Experiment {
var value int
if config != nil {
value = config.Experiments[xName]
}
if value == 0 {
value, _ = strconv.Atoi(getEnv(xName))
}
x := Experiment{
Name: xName,
AllowedValues: allowedValues,
Value: value,
Value: getValue(xName, config),
}
xList = append(xList, x)
return x
}
// NewReleased creates a new experiment that is released and no longer needs to
// be enabled. It will always be inactive and cannot be enabled.
func NewReleased(xName string, config *ast.TaskRC) Experiment {
x := Experiment{
Name: xName,
Value: getValue(xName, config),
InactiveReason: "is released and no longer needs to be enabled",
}
xList = append(xList, x)
return x
}
// NewAbandoned creates a new experiment that has been abandoned and is no
// longer supported. It will always be inactive and cannot be enabled.
func NewAbandoned(xName string, config *ast.TaskRC) Experiment {
x := Experiment{
Name: xName,
Value: getValue(xName, config),
InactiveReason: "has been abandoned and is no longer supported",
}
xList = append(xList, x)
return x
@@ -46,7 +73,8 @@ func (x Experiment) Active() bool {
func (x Experiment) Valid() error {
if !x.Active() && x.Value != 0 {
return &InactiveError{
Name: x.Name,
Name: x.Name,
Reason: x.InactiveReason,
}
}
if !x.Enabled() && x.Value != 0 {

View File

@@ -16,16 +16,16 @@ const envPrefix = "TASK_X_"
// Active experiments.
var (
GentleForce Experiment
RemoteTaskfiles Experiment
EnvPrecedence Experiment
GentleForce Experiment
EnvPrecedence Experiment
)
// Inactive experiments. These are experiments that cannot be enabled, but are
// preserved for error handling.
var (
AnyVariables Experiment
MapVariables Experiment
AnyVariables Experiment
MapVariables Experiment
RemoteTaskfiles Experiment
)
// An internal list of all the initialized experiments used for iterating.
@@ -41,10 +41,11 @@ func ParseWithConfig(dir string, config *ast.TaskRC) {
readDotEnv(dir)
// Initialize the experiments
GentleForce = New("GENTLE_FORCE", config, 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)
EnvPrecedence = New("ENV_PRECEDENCE", config, 1)
AnyVariables = New("ANY_VARIABLES", config)
MapVariables = New("MAP_VARIABLES", config)
// Inactive experiments
AnyVariables = NewReleased("ANY_VARIABLES", config)
MapVariables = NewReleased("MAP_VARIABLES", config)
RemoteTaskfiles = NewReleased("REMOTE_TASKFILES", config)
}
// Validate checks if any experiments have been enabled while being inactive.

131
go.mod
View File

@@ -31,107 +31,106 @@ require (
golang.org/x/sync v0.21.0
golang.org/x/term v0.44.0
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117
mvdan.cc/sh/v3 v3.13.2-0.20260613075524-2255122b577b
)
require (
cel.dev/expr v0.25.1 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth v0.20.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.3 // indirect
cloud.google.com/go/storage v1.61.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
cloud.google.com/go/iam v1.11.0 // indirect
cloud.google.com/go/monitoring v1.29.0 // indirect
cloud.google.com/go/storage v1.63.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.42.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.26 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.25 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 // indirect
github.com/aws/smithy-go v1.27.3 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260622092850-f39628c8a989 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/dlclark/regexp2/v2 v2.2.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.17 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.73 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // 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/pierrec/lz4/v4 v4.1.27 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/u-root/u-root v0.16.0 // 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
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.55.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/api v0.287.0 // indirect
google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect
google.golang.org/grpc v1.82.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

305
go.sum
View File

@@ -1,95 +1,87 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
charm.land/bubbletea/v2 v2.0.7 h1:7qw2tTAVar7m7klOPBYfTB0mniv/RuexsYwMRNxSeL0=
charm.land/bubbletea/v2 v2.0.7/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q=
charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
cloud.google.com/go/logging v1.18.0 h1:KhzZq+1cSkPH9YUaKLLhLtQxIHitVayBmk0sGfoM9+k=
cloud.google.com/go/logging v1.18.0/go.mod h1:ZGKnpBaURITh+g/uom2VhbiFoFWvejcrHPDhxFtU/gI=
cloud.google.com/go/longrunning v1.1.0 h1:qJ0R0IA8ONaRCNWTRPAS0iAmt1bj3TVgJ40z7ZGRslE=
cloud.google.com/go/longrunning v1.1.0/go.mod h1:tH+A/6UvNypiPJWAQaKCsh+xiGbB23wUO8egwUXlD2E=
cloud.google.com/go/monitoring v1.29.0 h1:AHhDsFaSax1/4k+qlIDX/SDGe6hggnfXJ9dkgD9qBPY=
cloud.google.com/go/monitoring v1.29.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM=
cloud.google.com/go/storage v1.63.0 h1:hvXF2xfg9I32bjujggxgkEZn/Ej6sJ9pieFgeueBLrQ=
cloud.google.com/go/storage v1.63.0/go.mod h1:tirWVptrFNo5GEX2DQ47JooF7yaweJdAJ1hYAVMvKzE=
cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E=
cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk=
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.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
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.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls=
github.com/aws/aws-sdk-go-v2/config v1.32.26 h1:JI+W5B3jUA8UBz2ggbICGd9UCR6/+SB21G8EFl0SFTQ=
github.com/aws/aws-sdk-go-v2/config v1.32.26/go.mod h1:RLE2Ls/wRstvdSz1GPrIWNnXcKZ/znDdWyMuiQxdBoY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.25 h1:TzPVjfUZ1hsKafvYE+DIzKXIik2KufQxsPHanlkttbo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.25/go.mod h1:K4hw0buguVvtC74HnVfTRr0LzQQHAWPqJbBU9QGk2Pg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30 h1:4HbXxyipSYxexU0juMIpdS05dilL6dbB2VQHxxN2vGU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 h1:yb03KevaOAG5e8suo79Af74vjIQvoeKmjl79WQchLrs=
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1/go.mod h1:mreYODw0Y4yv7xeczvqC6vciwFao8lPE9k1l1ulfY6E=
github.com/aws/aws-sdk-go-v2/service/signin v1.2.1 h1:BeJmkm5YOZs6lGRGcNoIuLSoTTtGLLCEqlSiRKYodfM=
github.com/aws/aws-sdk-go-v2/service/signin v1.2.1/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 h1:i465b/3c7xJd++pobNIDOggouekCuiWOnB0goQJy+94=
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 h1:xbmJAnBbyYPkTzoCNCF/bpJ6ymQHRdXX1vquYfDIGYk=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 h1:Np0vmL7op0Zs5xGacYMMX3v5O5pvZ46xhb5LwDgPj8M=
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
@@ -100,10 +92,8 @@ github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiw
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek=
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY=
github.com/charmbracelet/ultraviolet v0.0.0-20260622092850-f39628c8a989 h1:aLA9AmFNKnFr86XM3/Jm9g4xLOVjEgRuttBWUFujdVw=
github.com/charmbracelet/ultraviolet v0.0.0-20260622092850-f39628c8a989/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
@@ -118,18 +108,16 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0=
github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
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=
@@ -138,16 +126,16 @@ github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
@@ -173,26 +161,26 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
github.com/googleapis/enterprise-certificate-proxy v0.3.17 h1:73NfMHdiqo9JFU9+7a5ExpVa10/R29pXfZIaW559nrg=
github.com/googleapis/enterprise-certificate-proxy v0.3.17/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.73 h1:LXhjywNxHsex3qFY2p2iOaHK4nFvdqVp9T9QLdZfpjQ=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.73/go.mod h1:AsbUhwFfdK9ipM8G0i8WVHS0IesKck6M0M9NcuMQTJ8=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY=
github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.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/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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -204,23 +192,23 @@ 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.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/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/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/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
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/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg=
github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4=
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=
@@ -235,21 +223,21 @@ github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E=
github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
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/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
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.16.0 h1:wY40O83MBVks97+Is0WlFlOPSwKQMIrWP9R1IsrExg8=
github.com/u-root/u-root v0.16.0/go.mod h1:yL/XdSSW27PdGLgUh4MNRBy54mKM+TBLzpwiB4nwj90=
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=
@@ -262,63 +250,58 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.287.0 h1:CQDMqUiqZZ0U/Yge3zyjAhNQ0OSYEH0PaA7l4xtEen4=
google.golang.org/api v0.287.0/go.mod h1:pPW85yt3Iuc3unkpaMhFtMmOqnTdCwCqEOaUlnuxRlQ=
google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7 h1:lQG76ePMKmtujel4VIVMiFoHVWVNtJdawbCZJtWlVXU=
google.golang.org/genproto v0.0.0-20260630182238-925bb5da69e7/go.mod h1:LwlOWYBU335L+sR55UuR5fbbU8KmEX+3tUHf3SwMmhM=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -331,5 +314,5 @@ 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.13.2-0.20260510185049-f5c6e2779117 h1:BfzdGSjcnJBb8sPNLudpzTml8zFUxS1N0N/v9IIS6tQ=
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
mvdan.cc/sh/v3 v3.13.2-0.20260613075524-2255122b577b h1:NREoadYF42Gu7127VIccx/SRia+Bz8wpKBaqmXKiGXE=
mvdan.cc/sh/v3 v3.13.2-0.20260613075524-2255122b577b/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=

View File

@@ -1,89 +0,0 @@
// Package complete implements the `task __complete` protocol consumed by the
// shell completion wrappers. The protocol mirrors cobra v2 so a future
// migration stays cheap.
package complete
import "os"
// CommandName is the hidden subcommand the shell wrappers invoke to drive
// completion: `task __complete <words...>`.
const CommandName = "__complete"
// IsActive reports whether the process was invoked in completion mode, i.e.
// the first argument is the __complete subcommand.
func IsActive() bool {
return len(os.Args) >= 2 && os.Args[1] == CommandName
}
// Directive mirrors cobra's ShellCompDirective bitfield. It is emitted on the
// final output line as `:<directive>` and tells the shell wrapper how to treat
// the suggestions (file fallback, trailing space, ordering, …).
type Directive int
const (
// DirectiveDefault leaves the shell to perform its default file completion.
DirectiveDefault Directive = 0
// DirectiveError signals an error; the shell should not offer completion.
DirectiveError Directive = 1 << 0
// DirectiveNoSpace prevents the shell from appending a space after the
// suggestion (e.g. so `VAR=` can be followed by a value).
DirectiveNoSpace Directive = 1 << 1
// DirectiveNoFileComp disables the shell's fallback file completion.
DirectiveNoFileComp Directive = 1 << 2
// DirectiveFilterFileExt restricts file completion to the emitted extensions.
DirectiveFilterFileExt Directive = 1 << 3
// DirectiveFilterDirs restricts completion to directories.
DirectiveFilterDirs Directive = 1 << 4
// DirectiveKeepOrder tells the shell to preserve the emitted order instead
// of sorting alphabetically.
DirectiveKeepOrder Directive = 1 << 5
)
// Suggestion is a single completion candidate: the Value inserted on the
// command line and an optional human-readable Description.
type Suggestion struct {
Value string
Description string
}
// Options tunes what the engine emits. The zero value shows everything; use
// DefaultOptions for the default and flip fields off from the __complete flags.
type Options struct {
ShowAliases bool
ShowDescriptions bool
}
// DefaultOptions returns the options used when no completion-control flag is
// passed: aliases and descriptions are both shown.
func DefaultOptions() Options {
return Options{ShowAliases: true, ShowDescriptions: true}
}
// Completion-control flags. Shell wrappers prepend these to the __complete
// invocation to tune the output (e.g. zsh maps its show-aliases / verbose
// zstyles to them). They are consumed by ParseOptions before the remaining
// args are treated as the user's command line.
const (
FlagNoAliases = "--no-aliases"
FlagNoDescriptions = "--no-descriptions"
)
// ParseOptions peels the leading completion-control flags off args and returns
// the resulting Options together with the remaining args (the user's command
// line to complete). Only leading flags are consumed, so a `--no-aliases` typed
// by the user further down the line is left untouched.
func ParseOptions(args []string) (Options, []string) {
opts := DefaultOptions()
for len(args) > 0 {
switch args[0] {
case FlagNoAliases:
opts.ShowAliases = false
case FlagNoDescriptions:
opts.ShowDescriptions = false
default:
return opts, args
}
args = args[1:]
}
return opts, args
}

View File

@@ -1,367 +0,0 @@
package complete_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/complete"
)
func newTestFlagSet() *pflag.FlagSet {
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
var b bool
var s string
fs.BoolVarP(&b, "list-all", "a", false, "Lists all tasks")
fs.BoolVarP(&b, "list", "l", false, "Lists tasks with descriptions")
fs.BoolVarP(&b, "verbose", "v", false, "Verbose mode")
fs.StringVarP(&s, "taskfile", "t", "", "Taskfile path")
fs.StringVarP(&s, "dir", "d", "", "Run dir")
fs.StringVarP(&s, "output", "o", "", "Output style")
fs.StringVar(&s, "sort", "", "Sort order")
fs.StringVar(&s, "cacert", "", "CA cert path")
return fs
}
const testTaskfile = `version: '3'
vars:
ALLOWED_ENVS:
- dev
- staging
- prod
tasks:
deploy:
desc: Deploy the application
aliases: [dep, ship]
requires:
vars:
- name: ENV
enum:
- dev
- staging
- prod
- REGION
cmds:
- 'echo {{.ENV}} {{.REGION}}'
build:
desc: Build it
cmds:
- 'echo build'
dynenum:
desc: Dynamic enum
requires:
vars:
- name: ENV
enum:
ref: .ALLOWED_ENVS
cmds:
- 'echo {{.ENV}}'
docs:serve:
desc: Serve docs locally
cmds:
- 'echo serving'
`
func setupExecutor(t *testing.T) *task.Executor {
t.Helper()
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(testTaskfile), 0o644))
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(io.Discard),
task.WithStderr(io.Discard),
task.WithVersionCheck(false),
)
require.NoError(t, e.Setup())
return e
}
func TestComplete_TaskNames(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{""}, complete.DefaultOptions())
require.ElementsMatch(t,
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
values(suggs),
)
require.Equal(t, complete.DirectiveNoFileComp, dir)
require.Contains(t, descriptions(suggs), "Deploy the application")
}
func TestComplete_AliasResolvesToTaskVars(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"dep", ""}, complete.DefaultOptions())
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="}, values(suggs))
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp|complete.DirectiveKeepOrder, dir)
}
func TestComplete_StaticEnum(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"deploy", ""}, complete.DefaultOptions())
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="}, values(suggs))
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp|complete.DirectiveKeepOrder, dir)
}
func TestComplete_EnumRef(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{"dynenum", ""}, complete.DefaultOptions())
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod"}, values(suggs))
}
func TestComplete_NoRequires(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"build", ""}, complete.DefaultOptions())
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_FlagValueNotConfusedWithTaskName(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--dir", "deploy", ""}, complete.DefaultOptions())
require.ElementsMatch(t,
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
values(suggs),
)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_NamespacedTaskName(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"docs:serve", ""}, complete.DefaultOptions())
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_FlagValueInlineEquals(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--output="}, complete.DefaultOptions())
// Inline form returns full `--output=value` tokens so the shell can match
// against the whole current word.
require.Equal(t, []string{"--output=interleaved", "--output=group", "--output=prefixed"}, values(suggs))
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_AfterDash(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"deploy", "--", ""}, complete.DefaultOptions())
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveDefault, dir)
}
func TestComplete_FlagNames(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"-"}, complete.DefaultOptions())
require.NotEmpty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
vals := values(suggs)
require.Contains(t, vals, "--list-all")
require.Contains(t, vals, "--taskfile")
require.Contains(t, vals, "-a")
}
func TestComplete_EnumFlagValue_Output(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--output", ""}, complete.DefaultOptions())
require.Equal(t, []string{"interleaved", "group", "prefixed"}, values(suggs))
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_EnumFlagValue_Sort(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{"--sort", ""}, complete.DefaultOptions())
require.Equal(t, []string{"default", "alphanumeric", "none"}, values(suggs))
}
func TestComplete_PathFlag_Taskfile(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--taskfile", ""}, complete.DefaultOptions())
require.Equal(t, []string{"yml", "yaml"}, values(suggs))
require.Equal(t, complete.DirectiveFilterFileExt, dir)
}
func TestComplete_PathFlag_Dir(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--dir", ""}, complete.DefaultOptions())
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveFilterDirs, dir)
}
func TestComplete_PathFlag_Cacert(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--cacert", ""}, complete.DefaultOptions())
require.Empty(t, suggs)
require.Equal(t, complete.DirectiveDefault, dir)
}
func TestComplete_NilExecutor(t *testing.T) {
t.Parallel()
suggs, dir := complete.Complete(nil, newTestFlagSet(), []string{"-"}, complete.DefaultOptions())
require.NotEmpty(t, suggs)
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_NoAliases(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
opts := complete.Options{ShowAliases: false, ShowDescriptions: true}
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{""}, opts)
require.ElementsMatch(t,
[]string{"build", "deploy", "dynenum", "docs:serve"},
values(suggs),
)
require.NotContains(t, values(suggs), "dep")
require.NotContains(t, values(suggs), "ship")
require.Equal(t, complete.DirectiveNoFileComp, dir)
}
func TestComplete_NoDescriptions(t *testing.T) {
t.Parallel()
e := setupExecutor(t)
opts := complete.Options{ShowAliases: true, ShowDescriptions: false}
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{""}, opts)
require.ElementsMatch(t,
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
values(suggs),
)
for _, d := range descriptions(suggs) {
require.Empty(t, d)
}
}
func TestParseOptions(t *testing.T) {
t.Parallel()
t.Run("defaults", func(t *testing.T) {
t.Parallel()
opts, rest := complete.ParseOptions([]string{"deploy", ""})
require.Equal(t, complete.DefaultOptions(), opts)
require.Equal(t, []string{"deploy", ""}, rest)
})
t.Run("both flags", func(t *testing.T) {
t.Parallel()
opts, rest := complete.ParseOptions([]string{"--no-aliases", "--no-descriptions", "deploy", ""})
require.False(t, opts.ShowAliases)
require.False(t, opts.ShowDescriptions)
require.Equal(t, []string{"deploy", ""}, rest)
})
t.Run("only leading flags consumed", func(t *testing.T) {
t.Parallel()
// A flag appearing after the user's words is left in the command line.
opts, rest := complete.ParseOptions([]string{"deploy", "--no-aliases"})
require.True(t, opts.ShowAliases)
require.Equal(t, []string{"deploy", "--no-aliases"}, rest)
})
}
func TestNeedsTaskfile(t *testing.T) {
t.Parallel()
tests := map[string]struct {
args []string
want bool
}{
"task name": {[]string{""}, true},
"partial task name": {[]string{"bui"}, true},
"task var": {[]string{"deploy", ""}, true},
"value flag then name": {[]string{"--dir", "/tmp", ""}, true},
"flag name": {[]string{"-"}, false},
"long flag name": {[]string{"--li"}, false},
"inline flag value": {[]string{"--output="}, false},
"flag value": {[]string{"--output", ""}, false},
"path flag value": {[]string{"--taskfile", ""}, false},
"after dash": {[]string{"deploy", "--", ""}, false},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, complete.NeedsTaskfile(tt.args, newTestFlagSet()))
})
}
}
func TestWrite_Format(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
complete.Write(&buf, []complete.Suggestion{
{Value: "deploy", Description: "Deploy the app"},
{Value: "build"},
}, complete.DirectiveNoSpace|complete.DirectiveNoFileComp)
require.Equal(t, "deploy\tDeploy the app\nbuild\n:6\n", buf.String())
}
func TestWrite_EmptyWithDirective(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
complete.Write(&buf, nil, complete.DirectiveFilterDirs)
require.Equal(t, ":16\n", buf.String())
}
func values(suggs []complete.Suggestion) []string {
out := make([]string, 0, len(suggs))
for _, s := range suggs {
out = append(out, s.Value)
}
return out
}
func descriptions(suggs []complete.Suggestion) []string {
out := make([]string, 0, len(suggs))
for _, s := range suggs {
out = append(out, s.Description)
}
return out
}

View File

@@ -1,80 +0,0 @@
package complete
import (
"strings"
"github.com/spf13/pflag"
)
type completionContext struct {
toComplete string
prev string
afterDash bool
}
// parseContext infers the cursor position from args alone. It deliberately
// avoids the task list so flag completion never pays to load it; the task word
// is resolved separately by detectTaskName only once a task context is reached.
func parseContext(args []string) completionContext {
ctx := completionContext{}
if len(args) == 0 {
return ctx
}
ctx.toComplete = args[len(args)-1]
if len(args) >= 2 {
ctx.prev = args[len(args)-2]
}
for _, w := range args[:len(args)-1] {
if w == "--" {
ctx.afterDash = true
return ctx
}
}
return ctx
}
// detectTaskName scans args for the task word the cursor is completing under
// (e.g. "deploy" in `task deploy ENV=<tab>`). fs is needed to skip the word
// following a value-taking flag, otherwise `task --dir deploy` would mistake
// "deploy" (the directory) for a task name.
func detectTaskName(args []string, knownTasks []string, fs *pflag.FlagSet) string {
if len(args) <= 1 {
return ""
}
known := make(map[string]struct{}, len(knownTasks))
for _, t := range knownTasks {
known[t] = struct{}{}
}
taskName := ""
skipNext := false
for _, w := range args[:len(args)-1] {
if skipNext {
skipNext = false
continue
}
if w == "--" {
return taskName
}
if strings.HasPrefix(w, "-") {
if !strings.Contains(w, "=") {
if f := matchFlagName(fs, w); f != nil && flagTakesValue(f) {
skipNext = true
}
}
continue
}
if strings.Contains(w, "=") {
continue
}
if _, ok := known[w]; ok {
taskName = w
}
}
return taskName
}

View File

@@ -1,201 +0,0 @@
package complete
import (
"strings"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
)
// Complete is the single entry point used by `task __complete`. e may be nil
// when the Taskfile failed to load; flag completion still works in that case.
func Complete(e *task.Executor, fs *pflag.FlagSet, args []string, opts Options) ([]Suggestion, Directive) {
ctx := parseContext(args)
if ctx.afterDash {
return nil, DirectiveDefault
}
if ctx.prev != "" {
if flag := matchFlagName(fs, ctx.prev); flag != nil && flagTakesValue(flag) {
return completeFlagValue(flag.Name, "")
}
}
if strings.HasPrefix(ctx.toComplete, "-") {
if eqIdx := strings.Index(ctx.toComplete, "="); eqIdx != -1 {
flagWord := ctx.toComplete[:eqIdx]
if f := matchFlagName(fs, flagWord); f != nil && flagTakesValue(f) {
// Return full `--flag=value` candidates: shells match/insert
// against the whole current token, so bare values never match.
return completeFlagValue(f.Name, flagWord+"=")
}
}
return listFlags(fs), DirectiveNoFileComp
}
// Only a task context needs the task list, so it is loaded lazily here.
if e != nil && e.Taskfile != nil {
if taskName := detectTaskName(args, taskNames(e), fs); taskName != "" {
return completeTaskVars(e, taskName)
}
}
return completeTaskNames(e, opts), DirectiveNoFileComp
}
// NeedsTaskfile reports whether completing args requires a loaded Taskfile.
// Flag-name and flag-value completion (and words after `--`) do not, so the
// caller can skip the potentially expensive Taskfile parse for those keystrokes.
func NeedsTaskfile(args []string, fs *pflag.FlagSet) bool {
ctx := parseContext(args)
if ctx.afterDash {
return false
}
if ctx.prev != "" {
if flag := matchFlagName(fs, ctx.prev); flag != nil && flagTakesValue(flag) {
return false
}
}
return !strings.HasPrefix(ctx.toComplete, "-")
}
func taskNames(e *task.Executor) []string {
if e == nil || e.Taskfile == nil {
return nil
}
var out []string
for t := range e.Taskfile.Tasks.Values(nil) {
if t.Internal {
continue
}
out = append(out, strings.TrimSuffix(t.Task, ":"))
for _, alias := range t.Aliases {
out = append(out, strings.TrimSuffix(alias, ":"))
}
}
return out
}
func completeTaskNames(e *task.Executor, opts Options) []Suggestion {
if e == nil || e.Taskfile == nil {
return nil
}
tasks, err := e.GetTaskList(task.FilterOutInternal)
if err != nil {
return nil
}
desc := func(t *ast.Task) string {
if !opts.ShowDescriptions {
return ""
}
return t.Desc
}
out := make([]Suggestion, 0, len(tasks))
for _, t := range tasks {
out = append(out, Suggestion{
Value: strings.TrimSuffix(t.Task, ":"),
Description: desc(t),
})
if !opts.ShowAliases {
continue
}
for _, alias := range t.Aliases {
out = append(out, Suggestion{
Value: strings.TrimSuffix(alias, ":"),
Description: desc(t),
})
}
}
return out
}
// completeFlagValue completes the value of a value-taking flag. prefix is empty
// for the separate-argument form (`--output <TAB>`) and `<flag>=` for the inline
// form (`--output=<TAB>`), so enum candidates come back as full `--output=value`
// tokens the shell can match against the current word.
func completeFlagValue(flagName, prefix string) ([]Suggestion, Directive) {
// Absent keys yield the zero value (DirectiveDefault), which falls through
// to the enum lookup below.
switch flagDirective[flagName] {
case DirectiveFilterFileExt:
suggs := make([]Suggestion, 0, len(taskfileExtensions))
for _, ext := range taskfileExtensions {
suggs = append(suggs, Suggestion{Value: ext})
}
return suggs, DirectiveFilterFileExt
case DirectiveFilterDirs:
return nil, DirectiveFilterDirs
}
if values, ok := flagEnums[flagName]; ok {
out := make([]Suggestion, 0, len(values))
for _, v := range values {
out = append(out, Suggestion{Value: prefix + v})
}
return out, DirectiveNoFileComp
}
return nil, DirectiveDefault
}
func completeTaskVars(e *task.Executor, taskName string) ([]Suggestion, Directive) {
compiled, err := e.FastCompiledTask(&task.Call{Task: taskName})
if err != nil || compiled == nil || compiled.Requires == nil {
return nil, DirectiveNoFileComp
}
cache := &templater.Cache{Vars: compiled.Vars}
out := make([]Suggestion, 0, 8)
for _, v := range compiled.Requires.Vars {
if v == nil || v.Name == "" {
continue
}
values := enumValues(v.Enum, cache)
if len(values) == 0 {
out = append(out, Suggestion{Value: v.Name + "="})
continue
}
for _, val := range values {
out = append(out, Suggestion{Value: v.Name + "=" + val})
}
}
if len(out) == 0 {
return nil, DirectiveNoFileComp
}
// KeepOrder preserves the declaration order of the `requires` block instead
// of letting the shell sort the variables alphabetically.
return out, DirectiveNoSpace | DirectiveNoFileComp | DirectiveKeepOrder
}
func enumValues(enum *ast.Enum, cache *templater.Cache) []string {
if enum == nil {
return nil
}
if len(enum.Value) > 0 {
return enum.Value
}
if enum.Ref == "" {
return nil
}
resolved := templater.ResolveRef(enum.Ref, cache)
if cache.Err() != nil {
return nil
}
arr, ok := resolved.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, item := range arr {
s, ok := item.(string)
if !ok {
return nil
}
out = append(out, s)
}
return out
}

View File

@@ -1,74 +0,0 @@
package complete
import (
"sort"
"strings"
"github.com/spf13/pflag"
)
// flagEnums lists allowed values for enum-style flags. Keep in sync with the
// help strings in internal/flags/flags.go.
var flagEnums = map[string][]string{
"output": {"interleaved", "group", "prefixed"},
"sort": {"default", "alphanumeric", "none"},
"completion": {"bash", "zsh", "fish", "powershell"},
}
// flagDirective maps value-taking flags to a file-completion directive.
// DirectiveDefault entries (and any flag absent here) fall back to the shell's
// default file completion.
var flagDirective = map[string]Directive{
"taskfile": DirectiveFilterFileExt,
"dir": DirectiveFilterDirs,
"remote-cache-dir": DirectiveFilterDirs,
"cacert": DirectiveDefault,
"cert": DirectiveDefault,
"cert-key": DirectiveDefault,
}
var taskfileExtensions = []string{"yml", "yaml"}
// flagTakesValue is false for boolean switches (NoOptDefVal == "true").
func flagTakesValue(f *pflag.Flag) bool {
return f.NoOptDefVal == ""
}
// listFlags walks fs at call time so experiment-gated flags appear or
// disappear based on the active experiments.
func listFlags(fs *pflag.FlagSet) []Suggestion {
if fs == nil {
return nil
}
out := make([]Suggestion, 0, 64)
fs.VisitAll(func(f *pflag.Flag) {
if f.Hidden || f.Deprecated != "" {
return
}
out = append(out, Suggestion{
Value: "--" + f.Name,
Description: f.Usage,
})
if f.Shorthand != "" {
out = append(out, Suggestion{
Value: "-" + f.Shorthand,
Description: f.Usage,
})
}
})
sort.Slice(out, func(i, j int) bool { return out[i].Value < out[j].Value })
return out
}
func matchFlagName(fs *pflag.FlagSet, word string) *pflag.Flag {
if fs == nil {
return nil
}
switch {
case strings.HasPrefix(word, "--"):
return fs.Lookup(strings.TrimPrefix(word, "--"))
case strings.HasPrefix(word, "-") && len(word) == 2:
return fs.ShorthandLookup(word[1:])
}
return nil
}

View File

@@ -1,28 +0,0 @@
package complete
import (
"fmt"
"io"
"strings"
)
// Write emits the cobra-v2 completion protocol: one `value\tdescription` (or
// bare `value`) per suggestion, followed by a trailing `:<directive>` line
// that shell wrappers split off even when there are zero suggestions.
func Write(w io.Writer, suggs []Suggestion, dir Directive) {
for _, s := range suggs {
value := sanitize(s.Value)
desc := sanitize(s.Description)
if desc == "" {
fmt.Fprintln(w, value)
continue
}
fmt.Fprintf(w, "%s\t%s\n", value, desc)
}
fmt.Fprintf(w, ":%d\n", dir)
}
func sanitize(s string) string {
r := strings.NewReplacer("\n", " ", "\r", " ", "\t", " ")
return r.Replace(s)
}

View File

@@ -13,18 +13,13 @@ type (
}
// Task describes a single task
Task struct {
Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"`
Summary string `json:"summary"`
Aliases []string `json:"aliases"`
UpToDate *bool `json:"up_to_date,omitempty"`
Location *Location `json:"location"`
Requires []RequiredVar `json:"requires,omitempty"`
}
RequiredVar struct {
Name string `json:"name"`
Enum []string `json:"enum,omitempty"`
Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"`
Summary string `json:"summary"`
Aliases []string `json:"aliases"`
UpToDate *bool `json:"up_to_date,omitempty"`
Location *Location `json:"location"`
}
// Location describes a task's location in a taskfile
Location struct {
@@ -50,28 +45,9 @@ func NewTask(task *ast.Task) Task {
Column: task.Location.Column,
Taskfile: task.Location.Taskfile,
},
Requires: newRequiredVars(task.Requires),
}
}
func newRequiredVars(requires *ast.Requires) []RequiredVar {
if requires == nil || len(requires.Vars) == 0 {
return nil
}
out := make([]RequiredVar, 0, len(requires.Vars))
for _, v := range requires.Vars {
if v == nil {
continue
}
rv := RequiredVar{Name: v.Name}
if v.Enum != nil && len(v.Enum.Value) > 0 {
rv.Enum = append([]string{}, v.Enum.Value...)
}
out = append(out, rv)
}
return out
}
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
if len(namespacePath) == 0 {
return

View File

@@ -14,7 +14,6 @@ import (
"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/complete"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
@@ -156,6 +155,16 @@ func init() {
pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, "FAILFAST", func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", getConfig(config, "REMOTE_OFFLINE", func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, "REMOTE_TRUSTED_HOSTS", func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, "REMOTE_TIMEOUT", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, "REMOTE_CACHE_DIR", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
pflag.StringVar(&CACert, "cacert", getConfig(config, "REMOTE_CACERT", func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
// Gentle force experiment will override the force flag and add a new force-all flag
if experiments.GentleForce.Enabled() {
@@ -165,26 +174,6 @@ func init() {
pflag.BoolVarP(&ForceAll, "force", "f", false, "Forces execution even when the task is up-to-date.")
}
// Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", getConfig(config, "REMOTE_OFFLINE", func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, "REMOTE_TRUSTED_HOSTS", func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, "REMOTE_TIMEOUT", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, "REMOTE_CACHE_DIR", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
pflag.StringVar(&CACert, "cacert", getConfig(config, "REMOTE_CACERT", func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
}
// In completion mode the user's `--flag` words must reach the engine
// untouched. The BoolVar/StringVar calls above already populated
// pflag.CommandLine, which is all the engine needs.
if complete.IsActive() {
return
}
pflag.Parse()
// Auto-detect color based on environment when not explicitly configured

View File

@@ -2,7 +2,7 @@
# Runtimes
go = "1.26.4"
node = "24"
pnpm = "11.8.0"
pnpm = "11.9.0"
# Dev tools
golangci-lint = "2.12.2"

View File

@@ -1073,8 +1073,6 @@ func TestIncludesMultiLevel(t *testing.T) {
}
func TestIncludesRemote(t *testing.T) {
enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)
dir := "testdata/includes_remote"
os.RemoveAll(filepath.Join(dir, ".task", "remote"))
@@ -1240,6 +1238,25 @@ func TestIncludesIncorrect(t *testing.T) {
assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error())
}
func TestIncludesMissingTaskfile(t *testing.T) {
t.Parallel()
const dir = "testdata/includes_missing_taskfile"
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithSilent(true),
)
err := e.Setup()
require.Error(t, err)
assert.Contains(t, err.Error(), "include must specify taskfile or dir")
assert.NotContains(t, err.Error(), "include cycle detected")
}
func TestIncludesEmptyMain(t *testing.T) {
t.Parallel()
@@ -1258,8 +1275,6 @@ func TestIncludesEmptyMain(t *testing.T) {
}
func TestIncludesHttp(t *testing.T) {
enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)
dir, err := filepath.Abs("testdata/includes_http")
require.NoError(t, err)

View File

@@ -2,6 +2,7 @@ package ast
import (
"iter"
"strings"
"sync"
"github.com/elliotchance/orderedmap/v3"
@@ -171,6 +172,9 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
if err := node.Decode(&includedTaskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if strings.TrimSpace(includedTaskfile.Taskfile) == "" && strings.TrimSpace(includedTaskfile.Dir) == "" {
return errors.NewTaskfileDecodeError(nil, node).WithMessage("include must specify taskfile or dir")
}
include.Taskfile = includedTaskfile.Taskfile
include.Dir = includedTaskfile.Dir
include.Optional = includedTaskfile.Optional

View File

@@ -98,6 +98,9 @@ func (vars *Vars) Values() iter.Seq[Var] {
// ToCacheMap converts Vars to an unordered map containing only the static
// variables
func (vars *Vars) ToCacheMap() (m map[string]any) {
if vars == nil || vars.om == nil {
return nil
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
m = make(map[string]any, vars.Len())

55
taskfile/ast/vars_test.go Normal file
View File

@@ -0,0 +1,55 @@
package ast
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestVars_ToCacheMap(t *testing.T) {
t.Parallel()
t.Run("nil receiver returns nil", func(t *testing.T) {
t.Parallel()
var vars *Vars
assert.Nil(t, vars.ToCacheMap())
})
t.Run("empty vars returns empty map", func(t *testing.T) {
t.Parallel()
vars := NewVars()
m := vars.ToCacheMap()
assert.NotNil(t, m)
assert.Empty(t, m)
})
t.Run("static values are included", func(t *testing.T) {
t.Parallel()
vars := NewVars(
&VarElement{Key: "FOO", Value: Var{Value: "bar"}},
&VarElement{Key: "NUM", Value: Var{Value: 42}},
)
m := vars.ToCacheMap()
assert.Equal(t, map[string]any{"FOO": "bar", "NUM": 42}, m)
})
t.Run("live values take precedence over static values", func(t *testing.T) {
t.Parallel()
vars := NewVars(
&VarElement{Key: "FOO", Value: Var{Value: "bar", Live: "live-bar"}},
)
m := vars.ToCacheMap()
assert.Equal(t, map[string]any{"FOO": "live-bar"}, m)
})
t.Run("dynamic variables are excluded", func(t *testing.T) {
t.Parallel()
sh := "echo hello"
vars := NewVars(
&VarElement{Key: "STATIC", Value: Var{Value: "ok"}},
&VarElement{Key: "DYNAMIC", Value: Var{Sh: &sh}},
)
m := vars.ToCacheMap()
assert.Equal(t, map[string]any{"STATIC": "ok"}, m)
})
}

View File

@@ -2,13 +2,12 @@ package taskfile
import (
"context"
"slices"
"strings"
"time"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/fsext"
)
@@ -66,14 +65,11 @@ func NewNode(
default:
node, err = NewFileNode(entrypoint, dir, opts...)
}
if _, isRemote := node.(RemoteNode); isRemote && !experiments.RemoteTaskfiles.Enabled() {
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
}
return node, err
}
func isRemoteEntrypoint(entrypoint string) bool {
func IsRemoteEntrypoint(entrypoint string) bool {
scheme, _ := getScheme(entrypoint)
switch scheme {
case "git", "http", "https":
@@ -89,7 +85,10 @@ func getScheme(uri string) (string, error) {
return "", err
}
if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
isDotGit := strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git")
isUnderscoreGit := strings.Contains(strings.Split(u.Path, "//")[0], "/_git/")
schemeIsGitCompatible := slices.Contains([]string{"git", "ssh", "https", "http"}, u.Scheme)
if (isDotGit || isUnderscoreGit) && schemeIsGitCompatible {
return "git", nil
}

View File

@@ -60,7 +60,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
if IsRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -188,7 +188,7 @@ func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
if IsRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -42,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
if IsRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -21,4 +21,7 @@ func TestScheme(t *testing.T) {
scheme, err = getScheme("https://github.com/foo/common.yml")
assert.NoError(t, err)
assert.Equal(t, "https", scheme)
scheme, err = getScheme("https://some-azure-host.com/org/project/_git/repo")
assert.NoError(t, err)
assert.Equal(t, "git", scheme)
}

View File

@@ -0,0 +1,5 @@
version: '3'
includes:
GOBIN:
sh: echo $(go env GOPATH)/bin

View File

@@ -370,6 +370,10 @@ export default defineConfig({
text: 'Guide',
link: '/docs/guide'
},
{
text: 'Remote Taskfiles',
link: '/docs/remote-taskfiles'
},
{
text: 'Reference',
collapsed: true,

View File

@@ -23,5 +23,5 @@
"vitepress-plugin-llms": "^1.9.1",
"vue": "^3.5.18"
},
"packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b"
}

275
website/pnpm-lock.yaml generated
View File

@@ -16,10 +16,10 @@ importers:
version: 24.13.2
netlify-cli:
specifier: ^26.0.0
version: 26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)
version: 26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)(supports-color@10.2.2)
prettier:
specifier: ^3.6.2
version: 3.8.4
version: 3.9.4
vitepress:
specifier: ^1.6.3
version: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3)
@@ -28,13 +28,13 @@ importers:
version: 1.7.5(vite@5.4.21(@types/node@24.13.2))
vitepress-plugin-llms:
specifier: ^1.9.1
version: 1.13.1
version: 1.13.2
vitepress-plugin-tabs:
specifier: ^0.9.0
version: 0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.38(typescript@5.9.3))
version: 0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.39(typescript@5.9.3))
vue:
specifier: ^3.5.18
version: 3.5.38(typescript@5.9.3)
version: 3.5.39(typescript@5.9.3)
packages:
@@ -1480,17 +1480,17 @@ packages:
vite: ^5.0.0 || ^6.0.0
vue: ^3.2.25
'@vue/compiler-core@3.5.38':
resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==}
'@vue/compiler-core@3.5.39':
resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==}
'@vue/compiler-dom@3.5.38':
resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==}
'@vue/compiler-dom@3.5.39':
resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==}
'@vue/compiler-sfc@3.5.38':
resolution: {integrity: sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==}
'@vue/compiler-sfc@3.5.39':
resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==}
'@vue/compiler-ssr@3.5.38':
resolution: {integrity: sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==}
'@vue/compiler-ssr@3.5.39':
resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==}
'@vue/devtools-api@7.7.7':
resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==}
@@ -1501,25 +1501,25 @@ packages:
'@vue/devtools-shared@7.7.7':
resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==}
'@vue/reactivity@3.5.38':
resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
'@vue/reactivity@3.5.39':
resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==}
'@vue/runtime-core@3.5.38':
resolution: {integrity: sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==}
'@vue/runtime-core@3.5.39':
resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==}
'@vue/runtime-dom@3.5.38':
resolution: {integrity: sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==}
'@vue/runtime-dom@3.5.39':
resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==}
'@vue/server-renderer@3.5.38':
resolution: {integrity: sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==}
'@vue/server-renderer@3.5.39':
resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==}
peerDependencies:
vue: 3.5.38
vue: 3.5.39
'@vue/shared@3.5.18':
resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==}
'@vue/shared@3.5.38':
resolution: {integrity: sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==}
'@vue/shared@3.5.39':
resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==}
'@vueuse/core@12.8.2':
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
@@ -1853,8 +1853,8 @@ packages:
brace-expansion@2.1.1:
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
brace-expansion@5.0.7:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
@@ -3232,8 +3232,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
js-yaml@3.15.0:
resolution: {integrity: sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==}
hasBin: true
json-schema-ref-resolver@3.0.0:
@@ -3629,6 +3629,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.15:
resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanospinner@1.2.2:
resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==}
@@ -4010,6 +4015,10 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
@@ -4042,8 +4051,8 @@ packages:
resolution: {integrity: sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==}
engines: {node: '>= 0.6'}
prettier@3.8.4:
resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==}
prettier@3.9.4:
resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==}
engines: {node: '>=14'}
hasBin: true
@@ -4971,8 +4980,8 @@ packages:
vite:
optional: true
vitepress-plugin-llms@1.13.1:
resolution: {integrity: sha512-m+rxyghF5INi8hBw0huFPx6+VvaX1tDGvw1H7FdXowaZJ3dcRY5ShgbmK1AQlmeOFMdd16H8WarhSHLPXF/2OA==}
vitepress-plugin-llms@1.13.2:
resolution: {integrity: sha512-2O4s0I5pjEZzgnoWgBPCZCyhah9FH5uQB6lGADazMoyF1URJshtG04ZnmX+cbmQmniN3T5JzdJO9B4q8JHDKOQ==}
engines: {node: '>=18'}
vitepress-plugin-tabs@0.9.0:
@@ -4993,8 +5002,8 @@ packages:
postcss:
optional: true
vue@3.5.38:
resolution: {integrity: sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==}
vue@3.5.39:
resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -5131,6 +5140,10 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
@@ -5899,7 +5912,7 @@ snapshots:
semver: 7.8.4
tmp-promise: 3.0.3
'@netlify/dev@4.18.7(rollup@4.46.2)':
'@netlify/dev@4.18.7(rollup@4.46.2)(supports-color@10.2.2)':
dependencies:
'@netlify/ai': 0.4.1
'@netlify/blobs': 10.7.9(supports-color@10.2.2)
@@ -5907,9 +5920,9 @@ snapshots:
'@netlify/database-dev': 0.10.1
'@netlify/dev-utils': 4.4.6
'@netlify/edge-functions-dev': 1.0.20
'@netlify/functions-dev': 1.3.0(rollup@4.46.2)
'@netlify/functions-dev': 1.3.0(rollup@4.46.2)(supports-color@10.2.2)
'@netlify/headers': 2.1.11
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9)
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))
'@netlify/redirects': 3.1.13
'@netlify/runtime': 4.1.25
'@netlify/static': 3.1.10
@@ -5982,7 +5995,7 @@ snapshots:
dependencies:
'@netlify/types': 2.8.0
'@netlify/functions-dev@1.3.0(rollup@4.46.2)':
'@netlify/functions-dev@1.3.0(rollup@4.46.2)(supports-color@10.2.2)':
dependencies:
'@netlify/blobs': 10.7.9(supports-color@10.2.2)
'@netlify/dev-utils': 4.4.6
@@ -5990,7 +6003,7 @@ snapshots:
'@netlify/zip-it-and-ship-it': 14.7.1(rollup@4.46.2)(supports-color@10.2.2)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
extract-zip: 2.0.1(supports-color@10.2.2)
is-stream: 4.0.1
jwt-decode: 4.0.0
lambda-local: 2.2.0
@@ -6042,9 +6055,9 @@ snapshots:
dependencies:
'@netlify/headers-parser': 9.0.3
'@netlify/images@1.3.10(@netlify/blobs@10.7.9)':
'@netlify/images@1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))':
dependencies:
ipx: 3.1.1(@netlify/blobs@10.7.9)
ipx: 3.1.1(@netlify/blobs@10.7.9(supports-color@10.2.2))
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -6423,7 +6436,7 @@ snapshots:
'@pnpm/network.ca-file': 1.0.2
config-chain: 1.1.13
'@pnpm/tabtab@0.5.4':
'@pnpm/tabtab@0.5.4(supports-color@10.2.2)':
dependencies:
debug: 4.4.3(supports-color@10.2.2)
enquirer: 2.4.1
@@ -6676,40 +6689,40 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.38(typescript@5.9.3))':
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.39(typescript@5.9.3))':
dependencies:
vite: 5.4.21(@types/node@24.13.2)
vue: 3.5.38(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
'@vue/compiler-core@3.5.38':
'@vue/compiler-core@3.5.39':
dependencies:
'@babel/parser': 7.29.7
'@vue/shared': 3.5.38
'@vue/shared': 3.5.39
entities: 7.0.1
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.38':
'@vue/compiler-dom@3.5.39':
dependencies:
'@vue/compiler-core': 3.5.38
'@vue/shared': 3.5.38
'@vue/compiler-core': 3.5.39
'@vue/shared': 3.5.39
'@vue/compiler-sfc@3.5.38':
'@vue/compiler-sfc@3.5.39':
dependencies:
'@babel/parser': 7.29.7
'@vue/compiler-core': 3.5.38
'@vue/compiler-dom': 3.5.38
'@vue/compiler-ssr': 3.5.38
'@vue/shared': 3.5.38
'@vue/compiler-core': 3.5.39
'@vue/compiler-dom': 3.5.39
'@vue/compiler-ssr': 3.5.39
'@vue/shared': 3.5.39
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.15
postcss: 8.5.16
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.38':
'@vue/compiler-ssr@3.5.39':
dependencies:
'@vue/compiler-dom': 3.5.38
'@vue/shared': 3.5.38
'@vue/compiler-dom': 3.5.39
'@vue/shared': 3.5.39
'@vue/devtools-api@7.7.7':
dependencies:
@@ -6729,38 +6742,38 @@ snapshots:
dependencies:
rfdc: 1.4.1
'@vue/reactivity@3.5.38':
'@vue/reactivity@3.5.39':
dependencies:
'@vue/shared': 3.5.38
'@vue/shared': 3.5.39
'@vue/runtime-core@3.5.38':
'@vue/runtime-core@3.5.39':
dependencies:
'@vue/reactivity': 3.5.38
'@vue/shared': 3.5.38
'@vue/reactivity': 3.5.39
'@vue/shared': 3.5.39
'@vue/runtime-dom@3.5.38':
'@vue/runtime-dom@3.5.39':
dependencies:
'@vue/reactivity': 3.5.38
'@vue/runtime-core': 3.5.38
'@vue/shared': 3.5.38
'@vue/reactivity': 3.5.39
'@vue/runtime-core': 3.5.39
'@vue/shared': 3.5.39
csstype: 3.2.3
'@vue/server-renderer@3.5.38(vue@3.5.38(typescript@5.9.3))':
'@vue/server-renderer@3.5.39(vue@3.5.39(typescript@5.9.3))':
dependencies:
'@vue/compiler-ssr': 3.5.38
'@vue/shared': 3.5.38
vue: 3.5.38(typescript@5.9.3)
'@vue/compiler-ssr': 3.5.39
'@vue/shared': 3.5.39
vue: 3.5.39(typescript@5.9.3)
'@vue/shared@3.5.18': {}
'@vue/shared@3.5.38': {}
'@vue/shared@3.5.39': {}
'@vueuse/core@12.8.2(typescript@5.9.3)':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 12.8.2
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -6768,7 +6781,7 @@ snapshots:
dependencies:
'@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
optionalDependencies:
focus-trap: 7.6.5
jwt-decode: 4.0.0
@@ -6779,7 +6792,7 @@ snapshots:
'@vueuse/shared@12.8.2(typescript@5.9.3)':
dependencies:
vue: 3.5.38(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
transitivePeerDependencies:
- typescript
@@ -7058,7 +7071,7 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
body-parser@2.3.0:
body-parser@2.3.0(supports-color@10.2.2):
dependencies:
bytes: 3.1.2
content-type: 2.0.0
@@ -7089,7 +7102,7 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.6:
brace-expansion@5.0.7:
dependencies:
balanced-match: 4.0.4
@@ -7485,7 +7498,7 @@ snapshots:
detective-vue2@2.3.0(supports-color@10.2.2)(typescript@5.9.3):
dependencies:
'@dependents/detective-less': 5.0.3
'@vue/compiler-sfc': 3.5.38
'@vue/compiler-sfc': 3.5.39
detective-es6: 5.0.2
detective-sass: 6.0.2
detective-scss: 5.0.2
@@ -7808,10 +7821,10 @@ snapshots:
dependencies:
on-headers: 1.1.0
express@5.2.1:
express@5.2.1(supports-color@10.2.2):
dependencies:
accepts: 2.0.0
body-parser: 2.3.0
body-parser: 2.3.0(supports-color@10.2.2)
content-disposition: 1.1.0
content-type: 1.0.5
cookie: 0.7.2
@@ -7821,7 +7834,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1
finalhandler: 2.1.1(supports-color@10.2.2)
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
@@ -7832,8 +7845,8 @@ snapshots:
proxy-addr: 2.0.7
qs: 6.15.2
range-parser: 1.2.1
router: 2.2.0
send: 1.2.1
router: 2.2.0(supports-color@10.2.2)
send: 1.2.1(supports-color@10.2.2)
serve-static: 2.2.1
statuses: 2.0.2
type-is: 2.1.0
@@ -7847,7 +7860,7 @@ snapshots:
extend@3.0.2: {}
extract-zip@2.0.1:
extract-zip@2.0.1(supports-color@10.2.2):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
get-stream: 5.2.0
@@ -7953,7 +7966,7 @@ snapshots:
filter-obj@6.1.0: {}
finalhandler@2.1.1:
finalhandler@2.1.1(supports-color@10.2.2):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
@@ -7993,7 +8006,7 @@ snapshots:
dependencies:
from2: 2.3.0
follow-redirects@1.16.0(debug@4.4.3):
follow-redirects@1.16.0(debug@4.4.3(supports-color@10.2.2)):
optionalDependencies:
debug: 4.4.3(supports-color@10.2.2)
@@ -8151,7 +8164,7 @@ snapshots:
gray-matter@4.0.3:
dependencies:
js-yaml: 3.14.2
js-yaml: 3.15.0
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
@@ -8246,21 +8259,21 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
http-proxy-middleware@3.0.7:
http-proxy-middleware@3.0.7(supports-color@10.2.2):
dependencies:
'@types/http-proxy': 1.17.17
debug: 4.4.3(supports-color@10.2.2)
http-proxy: 1.18.1(debug@4.4.3)
http-proxy: 1.18.1(debug@4.4.3(supports-color@10.2.2))
is-glob: 4.0.3
is-plain-object: 5.0.0
micromatch: 4.0.8
transitivePeerDependencies:
- supports-color
http-proxy@1.18.1(debug@4.4.3):
http-proxy@1.18.1(debug@4.4.3(supports-color@10.2.2)):
dependencies:
eventemitter3: 4.0.7
follow-redirects: 1.16.0(debug@4.4.3)
follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.2.2))
requires-port: 1.0.0
transitivePeerDependencies:
- debug
@@ -8274,7 +8287,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@8.0.0:
https-proxy-agent@8.0.0(supports-color@10.2.2):
dependencies:
agent-base: 8.0.0
debug: 4.4.3(supports-color@10.2.2)
@@ -8355,7 +8368,7 @@ snapshots:
ipaddr.js@2.4.0: {}
ipx@3.1.1(@netlify/blobs@10.7.9):
ipx@3.1.1(@netlify/blobs@10.7.9(supports-color@10.2.2)):
dependencies:
'@fastify/accept-negotiator': 2.0.1
citty: 0.1.6
@@ -8371,7 +8384,7 @@ snapshots:
sharp: 0.34.5
svgo: 4.0.1
ufo: 1.6.4
unstorage: 1.17.5(@netlify/blobs@10.7.9)
unstorage: 1.17.5(@netlify/blobs@10.7.9(supports-color@10.2.2))
xss: 1.0.15
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -8598,7 +8611,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.2:
js-yaml@3.15.0:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
@@ -9032,7 +9045,7 @@ snapshots:
millify@6.1.0:
dependencies:
yargs: 17.7.2
yargs: 17.7.3
mime-db@1.54.0: {}
@@ -9050,7 +9063,7 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.6
brace-expansion: 5.0.7
minimatch@5.1.9:
dependencies:
@@ -9107,13 +9120,15 @@ snapshots:
nanoid@3.3.12: {}
nanoid@3.3.15: {}
nanospinner@1.2.2:
dependencies:
picocolors: 1.1.1
negotiator@1.0.0: {}
netlify-cli@26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2):
netlify-cli@26.1.0(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)(supports-color@10.2.2):
dependencies:
'@fastify/static': 9.1.3
'@netlify/ai': 0.4.1
@@ -9122,19 +9137,19 @@ snapshots:
'@netlify/build': 35.15.0(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(picomatch@4.0.4)(rollup@4.46.2)
'@netlify/build-info': 10.5.1
'@netlify/config': 24.6.0
'@netlify/dev': 4.18.7(rollup@4.46.2)
'@netlify/dev': 4.18.7(rollup@4.46.2)(supports-color@10.2.2)
'@netlify/dev-utils': 4.4.6
'@netlify/edge-bundler': 14.10.3
'@netlify/edge-functions': 3.0.8
'@netlify/edge-functions-bootstrap': 2.17.1
'@netlify/headers-parser': 9.0.3
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9)
'@netlify/images': 1.3.10(@netlify/blobs@10.7.9(supports-color@10.2.2))
'@netlify/local-functions-proxy': 2.0.3
'@netlify/redirect-parser': 15.0.4
'@netlify/zip-it-and-ship-it': 14.7.1(rollup@4.46.2)(supports-color@10.2.2)
'@octokit/rest': 22.0.1
'@opentelemetry/api': 1.9.1
'@pnpm/tabtab': 0.5.4
'@pnpm/tabtab': 0.5.4(supports-color@10.2.2)
ansi-escapes: 7.3.0
ansi-to-html: 0.7.2
ascii-table: 0.0.9
@@ -9157,7 +9172,7 @@ snapshots:
envinfo: 7.21.0
etag: 1.8.1
execa: 5.1.1
express: 5.2.1
express: 5.2.1(supports-color@10.2.2)
express-logging: 1.1.1
fastest-levenshtein: 1.0.16
fastify: 5.8.5
@@ -9167,9 +9182,9 @@ snapshots:
get-port: 5.1.1
git-repo-info: 2.1.1
gitconfiglocal: 2.1.0
http-proxy: 1.18.1(debug@4.4.3)
http-proxy-middleware: 3.0.7
https-proxy-agent: 8.0.0
http-proxy: 1.18.1(debug@4.4.3(supports-color@10.2.2))
http-proxy-middleware: 3.0.7(supports-color@10.2.2)
https-proxy-agent: 8.0.0(supports-color@10.2.2)
inquirer: 8.2.7(@types/node@24.13.2)
inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.7(@types/node@24.13.2))
is-docker: 4.0.0
@@ -9617,6 +9632,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.16:
dependencies:
nanoid: 3.3.15
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.1: {}
@@ -9653,7 +9674,7 @@ snapshots:
precond@0.2.3: {}
prettier@3.8.4: {}
prettier@3.9.4: {}
pretty-bytes@7.1.0: {}
@@ -9938,7 +9959,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.46.2
fsevents: 2.3.3
router@2.2.0:
router@2.2.0(supports-color@10.2.2):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
depd: 2.0.0
@@ -10012,7 +10033,7 @@ snapshots:
semver@7.8.4: {}
send@1.2.1:
send@1.2.1(supports-color@10.2.2):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
encodeurl: 2.0.0
@@ -10033,7 +10054,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1
send: 1.2.1(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
@@ -10590,7 +10611,7 @@ snapshots:
unpipe@1.0.0: {}
unstorage@1.17.5(@netlify/blobs@10.7.9):
unstorage@1.17.5(@netlify/blobs@10.7.9(supports-color@10.2.2)):
dependencies:
anymatch: 3.1.3
chokidar: 5.0.0
@@ -10656,7 +10677,7 @@ snapshots:
vite@5.4.21(@types/node@24.13.2):
dependencies:
esbuild: 0.21.5
postcss: 8.5.15
postcss: 8.5.16
rollup: 4.46.2
optionalDependencies:
'@types/node': 24.13.2
@@ -10670,7 +10691,7 @@ snapshots:
optionalDependencies:
vite: 5.4.21(@types/node@24.13.2)
vitepress-plugin-llms@1.13.1:
vitepress-plugin-llms@1.13.2:
dependencies:
gray-matter: 4.0.3
markdown-it: 14.2.0
@@ -10689,10 +10710,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
vitepress-plugin-tabs@0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.38(typescript@5.9.3)):
vitepress-plugin-tabs@0.9.0(vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3))(vue@3.5.39(typescript@5.9.3)):
dependencies:
vitepress: 1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3)
vue: 3.5.38(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
vitepress@1.6.4(@algolia/client-search@5.35.0)(@types/node@24.13.2)(jwt-decode@4.0.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@5.9.3):
dependencies:
@@ -10703,7 +10724,7 @@ snapshots:
'@shikijs/transformers': 2.5.0
'@shikijs/types': 2.5.0
'@types/markdown-it': 14.1.2
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.38(typescript@5.9.3))
'@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@24.13.2))(vue@3.5.39(typescript@5.9.3))
'@vue/devtools-api': 7.7.7
'@vue/shared': 3.5.18
'@vueuse/core': 12.8.2(typescript@5.9.3)
@@ -10713,7 +10734,7 @@ snapshots:
minisearch: 7.1.2
shiki: 2.5.0
vite: 5.4.21(@types/node@24.13.2)
vue: 3.5.38(typescript@5.9.3)
vue: 3.5.39(typescript@5.9.3)
optionalDependencies:
postcss: 8.5.15
transitivePeerDependencies:
@@ -10743,13 +10764,13 @@ snapshots:
- typescript
- universal-cookie
vue@3.5.38(typescript@5.9.3):
vue@3.5.39(typescript@5.9.3):
dependencies:
'@vue/compiler-dom': 3.5.38
'@vue/compiler-sfc': 3.5.38
'@vue/runtime-dom': 3.5.38
'@vue/server-renderer': 3.5.38(vue@3.5.38(typescript@5.9.3))
'@vue/shared': 3.5.38
'@vue/compiler-dom': 3.5.39
'@vue/compiler-sfc': 3.5.39
'@vue/runtime-dom': 3.5.39
'@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@5.9.3))
'@vue/shared': 3.5.39
optionalDependencies:
typescript: 5.9.3
@@ -10912,6 +10933,16 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
yargs@17.7.3:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yauzl@2.10.0:
dependencies:
buffer-crc32: 0.2.13

View File

@@ -1,489 +1,12 @@
---
title: 'Remote Taskfiles (#1317)'
description: Experimentation for using Taskfiles stored in remote locations
outline: deep
---
# Remote Taskfiles (#1317)
::: warning
The Remote Taskfiles experiment has now [been released][changelog] :tada:. To
learn more, you can read the [remote Taskfile docs][remote-taskfile-docs].
All experimental features are subject to breaking changes and/or removal _at any
time_. We strongly recommend that you do not use these features in a production
environment. They are intended for testing and feedback only.
:::
::: info
To enable this experiment, set the environment variable:
`TASK_X_REMOTE_TASKFILES=1`. Check out
[our guide to enabling experiments](./index.md#enabling-experiments) for more
information.
:::
::: danger
Never run remote Taskfiles from sources that you do not trust.
:::
This experiment allows you to use Taskfiles which are stored in remote
locations. This applies to both the root Taskfile (aka. Entrypoint) and also
when including Taskfiles.
Task uses "nodes" to reference remote Taskfiles. There are a few different types
of node which you can use:
::: code-group
```text [HTTP/HTTPS]
https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
```
```text [Git over HTTP]
https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
```text [Git over SSH]
git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
:::
## Node Types
### HTTP/HTTPS
`https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml`
This is the most basic type of remote node and works by downloading the file
from the specified URL. The file must be a valid Taskfile and can be of any
name. If a file is not found at the specified URL, Task will append each of the
supported file names in turn until it finds a valid file. If it still does not
find a valid Taskfile, an error is returned.
### Git over HTTP
`https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main`
This type of node works by downloading the file from a Git repository over
HTTP/HTTPS. The first part of the URL is the base URL of the Git repository.
This is the same URL that you would use to clone the repo over HTTP.
- You can optionally add the path to the Taskfile in the repository by appending
`//<path>` to the URL.
- You can also optionally specify a branch or tag to use by appending
`?ref=<ref>` to the end of the URL. If you omit a reference, the default
branch will be used.
### Git over SSH
`git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main`
This type of node works by downloading the file from a Git repository over SSH.
The first part of the URL is the user and base URL of the Git repository. This
is the same URL that you would use to clone the repo over SSH.
To use Git over SSH, you need to make sure that your SSH agent has your private
SSH keys added so that they can be used during authentication.
- You can optionally add the path to the Taskfile in the repository by appending
`//<path>` to the URL.
- You can also optionally specify a branch or tag to use by appending
`?ref=<ref>` to the end of the URL. If you omit a reference, the default
branch will be used.
Task has an example remote Taskfile in our repository that you can use for
testing and that we will use throughout this document:
```yaml
version: '3'
tasks:
default:
cmds:
- task: hello
hello:
cmds:
- echo "Hello Task!"
```
## Specifying a remote entrypoint
By default, Task will look for one of the supported file names on your local
filesystem. If you want to use a remote file instead, you can pass its URI into
the `--taskfile`/`-t` flag just like you would to specify a different local
file. For example:
::: code-group
```shell [HTTP/HTTPS]
$ task --taskfile https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
task: [hello] echo "Hello Task!"
Hello Task!
```
```shell [Git over HTTP]
$ task --taskfile https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
task: [hello] echo "Hello Task!"
Hello Task!
```
```shell [Git over SSH]
$ task --taskfile git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
task: [hello] echo "Hello Task!"
Hello Task!
```
:::
## Including remote Taskfiles
Including a remote file works exactly the same way that including a local file
does. You just need to replace the local path with a remote URI. Any tasks in
the remote Taskfile will be available to run from your main Taskfile.
::: code-group
```yaml [HTTP/HTTPS]
version: '3'
includes:
my-remote-namespace: https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
```
```yaml [Git over HTTP]
version: '3'
includes:
my-remote-namespace: https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
```yaml [Git over SSH]
version: '3'
includes:
my-remote-namespace: git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
:::
```shell
$ task my-remote-namespace:hello
task: [hello] echo "Hello Task!"
Hello Task!
```
### Authenticating using environment variables
The Taskfile location is processed by the templating system, so you can
reference environment variables in your URL if you need to add authentication.
For example:
```yaml
version: '3'
includes:
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
```
## Security
### Automatic checksums
Running commands from sources that you do not control is always a potential
security risk. For this reason, we have added some automatic checks when using
remote Taskfiles:
1. When running a task from a remote Taskfile for the first time, Task will
print a warning to the console asking you to check that you are sure that you
trust the source of the Taskfile. If you do not accept the prompt, then Task
will exit with code `104` (not trusted) and nothing will run. If you accept
the prompt, the remote Taskfile will run and further calls to the remote
Taskfile will not prompt you again.
2. Whenever you run a remote Taskfile, Task will create and store a checksum of
the file that you are running. If the checksum changes, then Task will print
another warning to the console to inform you that the contents of the remote
file has changed. If you do not accept the prompt, then Task will exit with
code `104` (not trusted) and nothing will run. If you accept the prompt, the
checksum will be updated and the remote Taskfile will run.
Sometimes you need to run Task in an environment that does not have an
interactive terminal, so you are not able to accept a prompt. In these cases you
are able to tell task to accept these prompts automatically by using the `--yes`
flag or the `--trust` flag. The `--trust` flag allows you to specify trusted
hosts for remote Taskfiles, while `--yes` applies to all prompts in Task. You
can also configure trusted hosts in your [taskrc configuration](#trusted-hosts) using
`remote.trusted-hosts`. Before enabling automatic trust, you should:
1. Be sure that you trust the source and contents of the remote Taskfile.
2. Consider using a pinned version of the remote Taskfile (e.g. A link
containing a commit hash) to prevent Task from automatically accepting a
prompt that says a remote Taskfile has changed.
### Manual checksum pinning
Alternatively, if you expect the contents of your remote files to be a constant
value, you can pin the checksum of the included file instead:
```yaml
version: '3'
includes:
included:
taskfile: https://taskfile.dev
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
```
This will disable the automatic checksum prompts discussed above. However, if
the checksums do not match, Task will exit immediately with an error. When
setting this up for the first time, you may not know the correct value of the
checksum. There are a couple of ways you can obtain this:
1. Add the include normally without the `checksum` key. The first time you run
the included Taskfile, a `.task/remote` temporary directory is created. Find
the correct set of files for your included Taskfile and open the file that
ends with `.checksum`. You can copy the contents of this file and paste it
into the `checksum` key of your include. This method is safest as it allows
you to inspect the downloaded Taskfile before you pin it.
2. Alternatively, add the include with a temporary random value in the
`checksum` key. When you try to run the Taskfile, you will get an error that
will report the incorrect expected checksum and the actual checksum. You can
copy the actual checksum and replace your temporary random value.
### TLS
Task currently supports both `http` and `https` URLs. However, the `http`
requests will not execute by default unless you run the task with the
`--insecure` flag. This is to protect you from accidentally running a remote
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
internet and cached locally. This cached file will be used for all future
invocations of the Taskfile until the cache expires. Once it expires, Task will
download the latest copy of the file and update the cache. By default, the cache
is set to expire immediately. This means that Task will always fetch the latest
version. However, the cache expiry duration can be modified by setting the
`--expiry` flag.
If for any reason you lose access to the internet or you are running Task in
offline mode (via the `--offline` flag or `TASK_OFFLINE` environment variable),
Task will run the any available cached files _even if they are expired_. This
means that you should never be stuck without the ability to run your tasks as
long as you have downloaded a remote Taskfile at least once.
By default, Task will timeout requests to download remote files after 10 seconds
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.
You can force Task to ignore the cache and download the latest version by using
the `--download` flag.
You can use the `--clear-cache` flag to clear all cached remote files.
## Configuration
This experiment adds a new `remote` section to the
[configuration file](../reference/config.md).
- **Type**: `object`
- **Description**: Remote configuration settings for handling remote Taskfiles
```yaml
remote:
insecure: false
offline: false
timeout: "30s"
cache-expiry: "24h"
cache-dir: ~/.task
trusted-hosts:
- github.com
- gitlab.com
cacert: ""
cert: ""
cert-key: ""
```
#### `insecure`
- **Type**: `boolean`
- **Default**: `false`
- **Description**: Allow insecure connections when fetching remote Taskfiles
- **CLI equivalent**: `--insecure`
- **Environment variable**: `TASK_REMOTE_INSECURE`
```yaml
remote:
insecure: true
```
#### `offline`
- **Type**: `boolean`
- **Default**: `false`
- **Description**: Work in offline mode, preventing remote Taskfile fetching
- **CLI equivalent**: `--offline`
- **Environment variable**: `TASK_REMOTE_OFFLINE`
```yaml
remote:
offline: true
```
#### `timeout`
- **Type**: `string`
- **Default**: 10s
- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$`
- **Description**: Timeout duration for remote operations (e.g., '30s', '5m')
- **CLI equivalent**: `--timeout`
- **Environment variable**: `TASK_REMOTE_TIMEOUT`
```yaml
remote:
timeout: "1m"
```
#### `cache-expiry`
- **Type**: `string`
- **Default**: 0s (no cache)
- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$`
- **Description**: Cache expiry duration for remote Taskfiles (e.g., '1h',
'24h')
- **CLI equivalent**: `--expiry`
- **Environment variable**: `TASK_REMOTE_CACHE_EXPIRY`
```yaml
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_CACHE_DIR`
```yaml
remote:
cache-dir: ~/.task
```
#### `trusted-hosts`
- **Type**: `array of strings`
- **Default**: `[]` (empty list)
- **Description**: List of trusted hosts for remote Taskfiles. Hosts in this
list will not prompt for confirmation when downloading Taskfiles
- **CLI equivalent**: `--trusted-hosts`
- **Environment variable**: `TASK_REMOTE_TRUSTED_HOSTS` (comma-separated)
```yaml
remote:
trusted-hosts:
- github.com
- gitlab.com
- raw.githubusercontent.com
- example.com:8080
```
Hosts in the trusted hosts list will automatically be trusted without prompting for
confirmation when they are first downloaded or when their checksums change. The
host matching includes the port if specified in the URL. Use with caution and
only add hosts you fully trust.
You can also specify trusted hosts via the command line:
```shell
# Trust specific host for this execution
task --trusted-hosts github.com -t https://github.com/user/repo.git//Taskfile.yml
# Trust multiple hosts (comma-separated)
task --trusted-hosts github.com,gitlab.com -t https://github.com/user/repo.git//Taskfile.yml
# 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"
```
[changelog]: ../changelog.md#v3511---2026-05-16
[remote-taskfile-docs]: ../remote-taskfiles.md

View File

@@ -91,7 +91,7 @@ tasks:
:::
### Reading a Taskfile from stdin
### Running a Taskfile from stdin
Taskfile also supports reading from stdin. This is useful if you are generating
Taskfiles dynamically and don't want write them to disk. To tell task to read
@@ -104,6 +104,41 @@ task -t - < ./Taskfile.yml
cat ./Taskfile.yml | task -t -
```
### Running a remote Taskfile
::: danger
Never run remote Taskfiles from sources that you do not trust.
:::
It is possible to directly run a Taskfile from a remote source via HTTP(S) or
Git by using the `--taskfile`/`-t` flag. This is useful if you want to reuse a
set of tasks in multiple projects. For more information, take a look at our
[remote Taskfiles documentation](./remote-taskfiles.md).
::: code-group
```shell [HTTP/HTTPS]
$ task --taskfile https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
task: [hello] echo "Hello Task!"
Hello Task!
```
```shell [Git over HTTP]
$ task --taskfile https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
task: [hello] echo "Hello Task!"
Hello Task!
```
```shell [Git over SSH]
$ task --taskfile git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
task: [hello] echo "Hello Task!"
Hello Task!
```
:::
## Environment variables
### Task
@@ -248,6 +283,26 @@ the `DockerTasks.yml` file.
Relative paths are resolved relative to the directory containing the including
Taskfile.
### Remote Taskfiles
::: danger
Never run remote Taskfiles from sources that you do not trust.
:::
It is possible to include a Taskfile from a remote source via HTTP(S) or Git.
This is useful if you want to reuse a set of tasks in multiple projects. For
more information, take a look at our
[remote Taskfiles documentation](./remote-taskfiles.md).
```yaml
version: '3'
includes:
my-remote-namespace: https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
```
### OS-specific Taskfiles
You can include OS-specific Taskfiles by using a templating function:
@@ -940,7 +995,8 @@ You can use `--force` or `-f` if you want to force a task to run even when
up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero
[exit code](/docs/reference/cli#exit-codes) if any of the tasks are not up-to-date.
[exit code](/docs/reference/cli#exit-codes) if any of the tasks are not
up-to-date.
`status` can be combined with the
[fingerprinting](#by-fingerprinting-locally-generated-files-and-their-sources)
@@ -1024,8 +1080,8 @@ tasks:
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.
`if` simply skips the task or command when the condition is not met and
continues with the rest of the Taskfile.
#### Task-level `if`
@@ -1061,9 +1117,9 @@ tasks:
#### 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):
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'
@@ -1071,7 +1127,7 @@ version: '3'
tasks:
conditional:
vars:
ENABLE_FEATURE: "true"
ENABLE_FEATURE: 'true'
cmds:
- cmd: echo "Feature is enabled"
if: '{{eq .ENABLE_FEATURE "true"}}'
@@ -1081,7 +1137,8 @@ tasks:
#### Using `if` with `for` loops
When used inside a `for` loop, the `if` condition is evaluated for each iteration:
When used inside a `for` loop, the `if` condition is evaluated for each
iteration:
```yaml
version: '3'
@@ -1103,11 +1160,11 @@ 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" |
| 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
@@ -1337,8 +1394,8 @@ $ task deploy
Deploying 1.0.0 to prod
```
If the variable is already set (via CLI, environment, or Taskfile), no prompt
is shown:
If the variable is already set (via CLI, environment, or Taskfile), no prompt is
shown:
```shell
$ task deploy ENVIRONMENT=prod VERSION=1.0.0
@@ -1638,8 +1695,8 @@ in logs, but is **not a substitute** for proper secret management practices.
- ❌ Secrets in command output (stdout/stderr)
- ❌ Secret values copied into derived (non-secret) variables
Always use proper secret management tools (HashiCorp Vault, AWS Secrets
Manager, etc.) for production environments.
Always use proper secret management tools (HashiCorp Vault, AWS Secrets Manager,
etc.) for production environments.
:::
@@ -1767,7 +1824,7 @@ tasks:
If you use dotenv files, add them to `.gitignore`:
```yaml
dotenv: ['.env.local'] # Load from .env.local (in .gitignore)
dotenv: ['.env.local'] # Load from .env.local (in .gitignore)
```
:::
@@ -1844,8 +1901,7 @@ tasks:
matrix:
OS: ['windows', 'linux', 'darwin']
ARCH: ['amd64', 'arm64']
cmd:
echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
```
This will output:
@@ -1877,8 +1933,7 @@ tasks:
ref: .OS_VAR
ARCH:
ref: .ARCH_VAR
cmd:
echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
```
### Looping over your task's sources or generated files
@@ -1923,8 +1978,8 @@ files that match that glob.
Paths will always be returned as paths relative to the task directory. If you
need to convert this to an absolute path, you can use the built-in `joinPath`
function. There are some
[special variables](/docs/reference/templating#special-variables) that you may find
useful for this.
[special variables](/docs/reference/templating#special-variables) that you may
find useful for this.
::: code-group
@@ -2201,8 +2256,9 @@ $ 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:
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'
@@ -2211,11 +2267,12 @@ tasks:
start:*:
aliases: [run:*]
vars:
SERVICE: "{{index .MATCH 0}}"
SERVICE: '{{index .MATCH 0}}'
cmds:
- echo "Running {{.SERVICE}}"
```
In this example, you can call the task using the alias run:*:
In this example, you can call the task using the alias run:\*:
```shell
$ task run:foo
@@ -2266,8 +2323,8 @@ commands are executed in the reverse order if you schedule multiple of them.
:::
A special variable `.EXIT_CODE` is exposed when a command exited with a non-zero
[exit code](/docs/reference/cli#exit-codes). You can check its presence to know if
the task completed successfully or not:
[exit code](/docs/reference/cli#exit-codes). You can check its presence to know
if the task completed successfully or not:
```yaml
version: '3'
@@ -2276,7 +2333,8 @@ tasks:
default:
cmds:
- defer:
echo '{{if .EXIT_CODE}}Failed with {{.EXIT_CODE}}!{{else}}Success!{{end}}'
echo '{{if .EXIT_CODE}}Failed with
{{.EXIT_CODE}}!{{else}}Success!{{end}}'
- exit 1
```
@@ -2468,8 +2526,8 @@ tasks:
```
Warning prompts are called before executing a task. If a prompt is denied Task
will exit with [exit code](/docs/reference/cli#exit-codes) 205. If approved, Task
will continue as normal.
will exit with [exit code](/docs/reference/cli#exit-codes) 205. If approved,
Task will continue as normal.
```shell
task example
@@ -2863,8 +2921,8 @@ if called by another task, either directly or as a dependency.
The watcher can misbehave in certain scenarios, in particular for long-running
servers. There is a [known bug](https://github.com/go-task/task/issues/160)
where child processes of the running might not be killed appropriately. It's
advised to avoid running commands as `go run` and prefer `go build [...] &&
./binary` instead.
advised to avoid running commands as `go run` and prefer
`go build [...] && ./binary` instead.
If you are having issues, you might want to try tools specifically designed for
live-reloading, like [Air](https://github.com/air-verse/air/). Also, be sure to

View File

@@ -128,7 +128,8 @@ Disable fuzzy matching for task names. When enabled, Task will not suggest
similar task names when you mistype a task name.
- **Config equivalent**: [`disable-fuzzy`](./config.md#disable-fuzzy)
- **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy)
- **Environment variable**:
[`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy)
```bash
task buidl --disable-fuzzy
@@ -180,7 +181,8 @@ task test lint --parallel
Limit the number of concurrent tasks. Zero means unlimited.
- **Config equivalent**: [`concurrency`](./config.md#concurrency)
- **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency)
- **Environment variable**:
[`TASK_CONCURRENCY`](./environment.md#task-concurrency)
```bash
task test --concurrency 4
@@ -248,7 +250,8 @@ task test --output group
Message template to print before grouped output.
- **Environment variable**: [`TASK_OUTPUT_GROUP_BEGIN`](./environment.md#task-output-group-begin)
- **Environment variable**:
[`TASK_OUTPUT_GROUP_BEGIN`](./environment.md#task-output-group-begin)
```bash
task test --output group --output-group-begin "::group::{{.TASK}}"
@@ -258,7 +261,8 @@ task test --output group --output-group-begin "::group::{{.TASK}}"
Message template to print after grouped output.
- **Environment variable**: [`TASK_OUTPUT_GROUP_END`](./environment.md#task-output-group-end)
- **Environment variable**:
[`TASK_OUTPUT_GROUP_END`](./environment.md#task-output-group-end)
```bash
task test --output group --output-group-end "::endgroup::"
@@ -268,7 +272,8 @@ task test --output group --output-group-end "::endgroup::"
Only show command output on non-zero exit codes.
- **Environment variable**: [`TASK_OUTPUT_GROUP_ERROR_ONLY`](./environment.md#task-output-group-error-only)
- **Environment variable**:
[`TASK_OUTPUT_GROUP_ERROR_ONLY`](./environment.md#task-output-group-error-only)
```bash
task test --output group --output-group-error-only
@@ -351,7 +356,8 @@ task build --watch --interval 1s
Automatically answer "yes" to all prompts.
- **Environment variable**: [`TASK_ASSUME_YES`](./environment.md#task-assume-yes)
- **Environment variable**:
[`TASK_ASSUME_YES`](./environment.md#task-assume-yes)
```bash
task deploy --yes
@@ -366,12 +372,69 @@ 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.
- **Environment variable**: [`TASK_INTERACTIVE`](./environment.md#task-interactive)
- **Environment variable**:
[`TASK_INTERACTIVE`](./environment.md#task-interactive)
```bash
task deploy --interactive
```
### Remote
The following flags are used to control the behavior of
[remote Taskfiles](../remote-taskfiles.md).
#### `--insecure`
Allow insecure connections when fetching remote Taskfiles.
#### `--offline`
Work in offline mode, preventing remote Taskfile fetching.
#### `--download`
Forces task to download remote Taskfiles and ignore any cached versions.
#### `--timeout`
Timeout duration for remote operations (e.g., '30s', '5m').
#### `--clear-cache`
Wipe the cache of remote Taskfiles and checksums.
#### `--expiry`
Cache expiry duration for remote Taskfiles (e.g., '1h', '24h').
#### `--remote-cache-dir`
Directory where remote Taskfiles are cached. Can be an absolute path (e.g.,
`/var/cache/task`) or relative to the Taskfile directory.
#### `--trusted-hosts`
List of (comma-separated) trusted hosts for remote Taskfiles. Hosts in this list
will not prompt for confirmation when downloading Taskfiles.
Hosts in the trusted hosts list will automatically be trusted without prompting
for confirmation when they are first downloaded or when their checksums change.
The host matching includes the port if specified in the URL. Use with caution
and only add hosts you fully trust.
#### `--cacert`
Path to a custom CA certificate file for TLS verification.
#### `--cert`
Path to a client certificate file for mTLS authentication.
#### `--cert-key`
Path to the client certificate private key file.
## Exit Codes
Task uses specific exit codes to indicate different types of errors:

View File

@@ -108,7 +108,8 @@ silent: true
- **Type**: `boolean`
- **Default**: `true`
- **Description**: Enable colored output. Colors are automatically enabled in CI environments (`CI=true`).
- **Description**: Enable colored output. Colors are automatically enabled in CI
environments (`CI=true`).
- **CLI equivalent**: [`-c, --color`](./cli.md#-c---color)
- **Environment variable**: [`TASK_COLOR`](./environment.md#task-color)
@@ -120,9 +121,11 @@ color: false
- **Type**: `boolean`
- **Default**: `false`
- **Description**: Disable fuzzy matching for task names. When enabled, Task will not suggest similar task names when you mistype a task name.
- **Description**: Disable fuzzy matching for task names. When enabled, Task
will not suggest similar task names when you mistype a task name.
- **CLI equivalent**: [`--disable-fuzzy`](./cli.md#--disable-fuzzy)
- **Environment variable**: [`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy)
- **Environment variable**:
[`TASK_DISABLE_FUZZY`](./environment.md#task-disable-fuzzy)
```yaml
disable-fuzzy: true
@@ -134,7 +137,8 @@ disable-fuzzy: true
- **Minimum**: `1`
- **Description**: Number of concurrent tasks to run
- **CLI equivalent**: [`-C, --concurrency`](./cli.md#-c---concurrency-number)
- **Environment variable**: [`TASK_CONCURRENCY`](./environment.md#task-concurrency)
- **Environment variable**:
[`TASK_CONCURRENCY`](./environment.md#task-concurrency)
```yaml
concurrency: 4
@@ -158,8 +162,8 @@ failfast: true
- **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.
variable. Requires a TTY. Task automatically detects non-TTY environments (CI
pipelines, etc.) and skips prompts.
- **CLI equivalent**: [`--interactive`](./cli.md#--interactive)
```yaml
@@ -178,6 +182,157 @@ interactive: true
temp-dir: .task
```
### `remote`
- **Type**: `object`
- **Description**: Remote configuration settings for handling
[remote Taskfiles](../remote-taskfiles.md).
#### `remote.insecure`
- **Type**: `boolean`
- **Default**: `false`
- **Description**: Allow insecure connections when fetching remote Taskfiles
- **CLI equivalent**: `--insecure`
- **Environment variable**:
[`TASK_REMOTE_INSECURE`](./environment.md#task_remote_insecure)
```yaml
remote:
insecure: true
```
#### `remote.offline`
- **Type**: `boolean`
- **Default**: `false`
- **Description**: Work in offline mode, preventing remote Taskfile fetching
- **CLI equivalent**: `--offline`
- **Environment variable**:
[`TASK_REMOTE_OFFLINE`](./environment.md#task_remote_offline)
```yaml
remote:
offline: true
```
#### `remote.timeout`
- **Type**: `string`
- **Default**: 10s
- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$`
- **Description**: Timeout duration for remote operations (e.g., '30s', '5m')
- **CLI equivalent**: `--timeout`
- **Environment variable**:
[`TASK_REMOTE_TIMEOUT`](./environment.md#task_remote_timeout)
```yaml
remote:
timeout: '1m'
```
#### `remote.cache-expiry`
- **Type**: `string`
- **Default**: 0s (no cache)
- **Pattern**: `^[0-9]+(ns|us|µs|ms|s|m|h)$`
- **Description**: Cache expiry duration for remote Taskfiles (e.g., '1h',
'24h')
- **CLI equivalent**: `--expiry`
- **Environment variable**:
[`TASK_REMOTE_CACHE_EXPIRY`](./environment.md#task_remote_cache_expiry)
```yaml
remote:
cache-expiry: '6h'
```
#### `remote.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_CACHE_DIR`](./environment.md#task_remote_cache_dir)
```yaml
remote:
cache-dir: ~/.task
```
#### `remote.trusted-hosts`
- **Type**: `array of strings`
- **Default**: `[]` (empty list)
- **Description**: List of trusted hosts for remote Taskfiles. Hosts in this
list will not prompt for confirmation when downloading Taskfiles
- **CLI equivalent**: `--trusted-hosts`
- **Environment variable**:
[`TASK_REMOTE_TRUSTED_HOSTS`](./environment.md#task_remote_trusted_hosts)
(comma-separated)
```yaml
remote:
trusted-hosts:
- github.com
- gitlab.com
- raw.githubusercontent.com
- example.com:8080
```
Hosts in the trusted hosts list will automatically be trusted without prompting
for confirmation when they are first downloaded or when their checksums change.
The host matching includes the port if specified in the URL. Use with caution
and only add hosts you fully trust.
You can also specify trusted hosts via the command line:
```shell
# Trust specific host for this execution
task --trusted-hosts github.com -t https://github.com/user/repo.git//Taskfile.yml
# Trust multiple hosts (comma-separated)
task --trusted-hosts github.com,gitlab.com -t https://github.com/user/repo.git//Taskfile.yml
# Trust a host with a specific port
task --trusted-hosts example.com:8080 -t https://example.com:8080/Taskfile.yml
```
#### `remote.cacert`
- **Type**: `string`
- **Default**: `""`
- **Description**: Path to a custom CA certificate file for TLS verification
```yaml
remote:
cacert: '/path/to/ca.crt'
```
#### `remote.cert`
- **Type**: `string`
- **Default**: `""`
- **Description**: Path to a client certificate file for mTLS authentication
```yaml
remote:
cert: '/path/to/client.crt'
```
#### `remote.cert-key`
- **Type**: `string`
- **Default**: `""`
- **Description**: Path to the client certificate private key file
```yaml
remote:
cert-key: '/path/to/client.key'
```
## Example Configuration
Here's a complete example of a `.taskrc.yml` file with all available options:
@@ -190,7 +345,20 @@ color: true
disable-fuzzy: false
concurrency: 2
temp-dir: .task
remote:
insecure: false
offline: false
timeout: '30s'
cache-expiry: '24h'
cache-dir: ~/.task
trusted-hosts:
- github.com
- gitlab.com
cacert: ''
cert: ''
cert-key: ''
# Enable experimental features
experiments:
REMOTE_TASKFILES: 1
GENTLE_FORCE: 1
```

View File

@@ -20,7 +20,8 @@ their configuration file equivalents.
## Variables
All [configuration file options](./config.md) can also be set via environment
variables. The priority order is: CLI flags > environment variables > config files > defaults.
variables. The priority order is: CLI flags > environment variables > config
files > defaults.
### `TASK_VERBOSE`
@@ -67,7 +68,8 @@ variables. The priority order is: CLI flags > environment variables > config fil
- **Type**: `boolean` (`true`, `false`, `1`, `0`)
- **Default**: `false`
- **Description**: Compiles and prints tasks in the order that they would be run, without executing them
- **Description**: Compiles and prints tasks in the order that they would be
run, without executing them
### `TASK_ASSUME_YES`
@@ -92,14 +94,16 @@ variables. The priority order is: CLI flags > environment variables > config fil
- **Type**: `string`
- **Description**: Message template to print before a task's grouped output.
Only applies when the output style is `group`.
- **CLI equivalent**: [`--output-group-begin`](./cli.md#--output-group-begin-template)
- **CLI equivalent**:
[`--output-group-begin`](./cli.md#--output-group-begin-template)
### `TASK_OUTPUT_GROUP_END`
- **Type**: `string`
- **Description**: Message template to print after a task's grouped output.
Only applies when the output style is `group`.
- **CLI equivalent**: [`--output-group-end`](./cli.md#--output-group-end-template)
- **Description**: Message template to print after a task's grouped output. Only
applies when the output style is `group`.
- **CLI equivalent**:
[`--output-group-end`](./cli.md#--output-group-end-template)
### `TASK_OUTPUT_GROUP_ERROR_ONLY`
@@ -107,7 +111,8 @@ variables. The priority order is: CLI flags > environment variables > config fil
- **Default**: `false`
- **Description**: Swallow output from successful tasks. Only applies when the
output style is `group`.
- **CLI equivalent**: [`--output-group-error-only`](./cli.md#--output-group-error-only)
- **CLI equivalent**:
[`--output-group-error-only`](./cli.md#--output-group-error-only)
### `TASK_TEMP_DIR`
@@ -118,16 +123,64 @@ Taskfile, not the working directory. Defaults to: `./.task`.
### `TASK_CORE_UTILS`
This env controls whether the Bash interpreter will use its own
core utilities implemented in Go, or the ones available in the system.
Valid values are `true` (`1`) or `false` (`0`). By default, this is `true` on
Windows and `false` on other operating systems. We might consider making this
enabled by default on all platforms in the future.
This env controls whether the Bash interpreter will use its own core utilities
implemented in Go, or the ones available in the system. Valid values are `true`
(`1`) or `false` (`0`). By default, this is `true` on Windows and `false` on
other operating systems. We might consider making this enabled by default on all
platforms in the future.
### `FORCE_COLOR`
Force color output usage.
## Remote Taskfile Variables
The following variables are used to control the behavior of
[remote Taskfiles](../remote-taskfiles.md).
### `TASK_REMOTE_INSECURE`
Allow insecure connections when fetching remote Taskfiles.
### `TASK_REMOTE_OFFLINE`
Work in offline mode, preventing remote Taskfile fetching.
### `TASK_REMOTE_TIMEOUT`
Timeout duration for remote operations (e.g., '30s', '5m').
### `TASK_REMOTE_CACHE_EXPIRY`
Cache expiry duration for remote Taskfiles (e.g., '1h', '24h').
### `TASK_REMOTE_CACHE_DIR`
Directory where remote Taskfiles are cached. Can be an absolute path (e.g.,
`/var/cache/task`) or relative to the Taskfile directory.
### `TASK_REMOTE_TRUSTED_HOSTS`
List of (comma-separated) trusted hosts for remote Taskfiles. Hosts in this list
will not prompt for confirmation when downloading Taskfiles.
Hosts in the trusted hosts list will automatically be trusted without prompting
for confirmation when they are first downloaded or when their checksums change.
The host matching includes the port if specified in the URL. Use with caution
and only add hosts you fully trust.
### `TASK_REMOTE_CACERT`
Path to a custom CA certificate file for TLS verification.
### `TASK_REMOTE_CERT`
Path to a client certificate file for mTLS authentication.
### `TASK_REMOTE_CERT_KEY`
Path to the client certificate private key file.
### Custom Colors
All color variables are [ANSI color codes][ansi]. You can specify multiple codes

View File

@@ -565,6 +565,9 @@ tasks:
- echo "{{.MULTILINE | catLines}}" # Replace newlines with spaces
```
In pipeline form, `join` receives the list from the left-hand side. The
equivalent non-pipeline form is <span v-pre>`{{join " " .WORDS}}`</span>.
#### Shell Argument Parsing
```yaml

View File

@@ -0,0 +1,336 @@
---
outline: deep
---
# Remote Taskfiles
::: danger
Never run remote Taskfiles from sources that you do not trust.
:::
Task allows you to use Taskfiles which are stored in remote locations. This
applies to both the root Taskfile (aka. Entrypoint) and also when including
Taskfiles.
Task uses "nodes" to reference remote Taskfiles. There are a few different types
of node which you can use:
::: code-group
```text [HTTP/HTTPS]
https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
```
```text [Git over HTTP]
https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
```text [Git over SSH]
git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
:::
## Node Types
### HTTP/HTTPS
`https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml`
This is the most basic type of remote node and works by downloading the file
from the specified URL. The file must be a valid Taskfile and can be of any
name. If a file is not found at the specified URL, Task will append each of the
supported file names in turn until it finds a valid file. If it still does not
find a valid Taskfile, an error is returned.
### Git over HTTP
`https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main`
This type of node works by downloading the file from a Git repository over
HTTP/HTTPS. The first part of the URL is the base URL of the Git repository.
This is the same URL that you would use to clone the repo over HTTP.
- You can optionally add the path to the Taskfile in the repository by appending
`//<path>` to the URL.
- You can also optionally specify a branch or tag to use by appending
`?ref=<ref>` to the end of the URL. If you omit a reference, the default
branch will be used.
### Git over SSH
`git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main`
This type of node works by downloading the file from a Git repository over SSH.
The first part of the URL is the user and base URL of the Git repository. This
is the same URL that you would use to clone the repo over SSH.
To use Git over SSH, you need to make sure that your SSH agent has your private
SSH keys added so that they can be used during authentication.
- You can optionally add the path to the Taskfile in the repository by appending
`//<path>` to the URL.
- You can also optionally specify a branch or tag to use by appending
`?ref=<ref>` to the end of the URL. If you omit a reference, the default
branch will be used.
Task has an example remote Taskfile in our repository that you can use for
testing and that we will use throughout this document:
```yaml
version: '3'
tasks:
default:
cmds:
- task: hello
hello:
cmds:
- echo "Hello Task!"
```
## Specifying a remote entrypoint
By default, Task will look for one of the supported file names on your local
filesystem. If you want to use a remote file instead, you can pass its URI into
the `--taskfile`/`-t` flag just like you would to specify a different local
file. For example:
::: code-group
```shell [HTTP/HTTPS]
$ task --taskfile https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
task: [hello] echo "Hello Task!"
Hello Task!
```
```shell [Git over HTTP]
$ task --taskfile https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
task: [hello] echo "Hello Task!"
Hello Task!
```
```shell [Git over SSH]
$ task --taskfile git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
task: [hello] echo "Hello Task!"
Hello Task!
```
:::
## Including remote Taskfiles
Including a remote file works exactly the same way that including a local file
does. You just need to replace the local path with a remote URI. Any tasks in
the remote Taskfile will be available to run from your main Taskfile.
::: code-group
```yaml [HTTP/HTTPS]
version: '3'
includes:
my-remote-namespace: https://raw.githubusercontent.com/go-task/task/main/website/src/public/Taskfile.yml
```
```yaml [Git over HTTP]
version: '3'
includes:
my-remote-namespace: https://github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
```yaml [Git over SSH]
version: '3'
includes:
my-remote-namespace: git@github.com/go-task/task.git//website/src/public/Taskfile.yml?ref=main
```
:::
```shell
$ task my-remote-namespace:hello
task: [hello] echo "Hello Task!"
Hello Task!
```
### Authenticating using environment variables
The Taskfile location is processed by the templating system, so you can
reference environment variables in your URL if you need to add authentication.
For example:
```yaml
version: '3'
includes:
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
```
## Special Variables
The file-path [special variables](../docs/reference/templating.md#file-paths)
behave differently when a Taskfile is loaded from a remote source, because there
is no local file or directory that corresponds 1:1 to the Taskfile:
| Variable | Value when loaded remotely |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TASKFILE` / `ROOT_TASKFILE` | The original URL, unchanged |
| `TASKFILE_DIR` / `ROOT_DIR` | Empty string — a directory variable cannot point to a URL |
| `TASK_DIR` | Resolved against `USER_WORKING_DIR` (relative `dir:` → joined with `USER_WORKING_DIR`, empty `dir:` → `USER_WORKING_DIR`, absolute `dir:` → kept as-is) |
If a remote Taskfile includes a local Taskfile (or vice-versa), each variable
reflects the source of the Taskfile it refers to.
## Security
### Automatic checksums
Running commands from sources that you do not control is always a potential
security risk. For this reason, we have added some automatic checks when using
remote Taskfiles:
1. When running a task from a remote Taskfile for the first time, Task will
print a warning to the console asking you to check that you are sure that you
trust the source of the Taskfile. If you do not accept the prompt, then Task
will exit with code `104` (not trusted) and nothing will run. If you accept
the prompt, the remote Taskfile will run and further calls to the remote
Taskfile will not prompt you again.
2. Whenever you run a remote Taskfile, Task will create and store a checksum of
the file that you are running. If the checksum changes, then Task will print
another warning to the console to inform you that the contents of the remote
file has changed. If you do not accept the prompt, then Task will exit with
code `104` (not trusted) and nothing will run. If you accept the prompt, the
checksum will be updated and the remote Taskfile will run.
Sometimes you need to run Task in an environment that does not have an
interactive terminal, so you are not able to accept a prompt. In these cases you
are able to tell task to accept these prompts automatically by using the `--yes`
flag or the `--trust` flag. The `--trust` flag allows you to specify trusted
hosts for remote Taskfiles, while `--yes` applies to all prompts in Task. You
can also configure trusted hosts in your
[taskrc configuration](./reference/config.md#remotetrusted-hosts) using
`remote.trusted-hosts`. Before enabling automatic trust, you should:
1. Be sure that you trust the source and contents of the remote Taskfile.
2. Consider using a pinned version of the remote Taskfile (e.g. A link
containing a commit hash) to prevent Task from automatically accepting a
prompt that says a remote Taskfile has changed.
### Manual checksum pinning
Alternatively, if you expect the contents of your remote files to be a constant
value, you can pin the checksum of the included file instead:
```yaml
version: '3'
includes:
included:
taskfile: https://taskfile.dev
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
```
This will disable the automatic checksum prompts discussed above. However, if
the checksums do not match, Task will exit immediately with an error. When
setting this up for the first time, you may not know the correct value of the
checksum. There are a couple of ways you can obtain this:
1. Add the include normally without the `checksum` key. The first time you run
the included Taskfile, a `.task/remote` temporary directory is created. Find
the correct set of files for your included Taskfile and open the file that
ends with `.checksum`. You can copy the contents of this file and paste it
into the `checksum` key of your include. This method is safest as it allows
you to inspect the downloaded Taskfile before you pin it.
2. Alternatively, add the include with a temporary random value in the
`checksum` key. When you try to run the Taskfile, you will get an error that
will report the incorrect expected checksum and the actual checksum. You can
copy the actual checksum and replace your temporary random value.
### TLS
Task currently supports both `http` and `https` URLs. However, the `http`
requests will not execute by default unless you run the task with the
`--insecure` flag. This is to protect you from accidentally running a remote
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
internet and cached locally. This cached file will be used for all future
invocations of the Taskfile until the cache expires. Once it expires, Task will
download the latest copy of the file and update the cache. By default, the cache
is set to expire immediately. This means that Task will always fetch the latest
version. However, the cache expiry duration can be modified by setting the
`--expiry` flag.
If for any reason you lose access to the internet or you are running Task in
offline mode (via the `--offline` flag or `TASK_OFFLINE` environment variable),
Task will run the any available cached files _even if they are expired_. This
means that you should never be stuck without the ability to run your tasks as
long as you have downloaded a remote Taskfile at least once.
By default, Task will timeout requests to download remote files after 10 seconds
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](./reference/config.md#remotecache-dir), or 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.
You can use the `--clear-cache` flag to clear all cached remote files.
## Configuration
It is also possible to adjust remote Taskfile functionality can be edited via a
configuration file. Check out the
- [CLI flags](./reference/cli.md#remote)
- [Environment Variables](./reference/environment.md#remote-taskfile-variables)
- [Config File](./reference/config.md#remote)

View File

@@ -0,0 +1,30 @@
version: "3"
tasks:
default:
cmds:
- task: hello
hello:
cmds:
- echo "Hello Task!"
special-variables:
silent: true
cmds:
- 'echo "CLI_ARGS: {{.CLI_ARGS}}"'
- 'echo "CLI_ARGS_LIST: {{.CLI_ARGS_LIST}}"'
- 'echo "CLI_ARGS_FORCE: {{.CLI_ARGS_FORCE}}"'
- 'echo "CLI_ARGS_SILENT: {{.CLI_ARGS_SILENT}}"'
- 'echo "CLI_ARGS_VERBOSE: {{.CLI_ARGS_VERBOSE}}"'
- 'echo "CLI_ARGS_OFFLINE: {{.CLI_ARGS_OFFLINE}}"'
- 'echo "TASK: {{.TASK}}"'
- 'echo "ALIAS: {{.ALIAS}}"'
- 'echo "TASK_EXE: {{.TASK_EXE}}"'
- 'echo "ROOT_TASKFILE: {{.ROOT_TASKFILE}}"'
- 'echo "ROOT_DIR: {{.ROOT_DIR}}"'
- 'echo "TASKFILE: {{.TASKFILE}}"'
- 'echo "TASKFILE_DIR: {{.TASKFILE_DIR}}"'
- 'echo "TASK_DIR: {{.TASK_DIR}}"'
- 'echo "USER_WORKING_DIR: {{.USER_WORKING_DIR}}"'
- 'echo "TASK_VERSION: {{.TASK_VERSION}}"'

View File

@@ -11,10 +11,6 @@
"type": "number",
"enum": [0, 1]
},
"REMOTE_TASKFILES": {
"type": "number",
"enum": [0, 1]
},
"GENTLE_FORCE": {
"type": "number",
"enum": [0, 1]

View File

@@ -718,11 +718,13 @@
"properties": {
"taskfile": {
"description": "The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the including Taskfile.",
"type": "string"
"type": "string",
"minLength": 1
},
"dir": {
"description": "The working directory of the included tasks when run.",
"type": "string"
"type": "string",
"minLength": 1
},
"optional": {
"description": "If `true`, no errors will be thrown if the specified file does not exist.",
@@ -758,7 +760,15 @@
"description": "The checksum of the file you expect to include. If the checksum does not match, the file will not be included.",
"type": "string"
}
}
},
"anyOf": [
{
"required": ["taskfile"]
},
{
"required": ["dir"]
}
]
}
]
}