Compare commits

...

78 Commits

Author SHA1 Message Date
Pete Davison
5128a98ee2 feat: test wildcards 2025-08-11 12:00:34 +00:00
Pete Davison
8353dffc7a fix: executor and formatter tests 2025-08-11 12:00:34 +00:00
Pete Davison
6e80b401e6 feat: remove entrypoint from the executor 2025-08-11 12:00:34 +00:00
Pete Davison
1402e2baaf refactor: merge Setup into NewExecutor 2025-08-11 12:00:34 +00:00
Pete Davison
4b99f60039 refactor: split executor and reader 2025-08-11 12:00:34 +00:00
Valentin Maerten
4bdfe5ce3b fix: publish nightly draft and title (#2358) 2025-08-09 16:03:46 +02:00
Valentin Maerten
26ef693417 chore: publish nightly (#2246)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2025-08-06 20:29:36 +02:00
renovate[bot]
952f32d388 chore(deps): update all non-major dependencies (#2351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 10:51:19 -03:00
Andrey Nering
e72c35f79f fix(goreleaser): fix automatic submission of winget pr 2025-07-28 10:59:45 -03:00
Pete Davison
72991d4f04 v3.44.1 2025-07-23 20:59:38 +00:00
Pete Davison
6f965e3043 chore: changelog for #2265 2025-07-23 20:59:14 +00:00
Pete Davison
1c6d686356 chore: replace PPRemoveAbsolutePaths with generic fixture template data (#2265)
* chore: replace PPRemoveAbsolutePaths with generic fixture template data

* chore: update to goldie v2.7.1
2025-07-23 21:57:25 +01:00
renovate[bot]
dac5aa1954 chore(deps): update all non-major dependencies (#2333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 09:46:14 -03:00
Emil
303bd6ccb2 chore(goreleaser): add section field to deb package (#2331) 2025-07-21 09:45:08 -03:00
renovate[bot]
f736cfaaf1 chore(deps): update all non-major dependencies (#2326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 09:23:53 -03:00
Pete Davison
53f97889bc chore: changelog for #2323 2025-07-09 17:38:54 +00:00
Max Mizikar
fe2da74ea3 fix: don't suggest internal tasks (#2323)
Co-authored-by: Max Mizikar <maxmzkr@gmail.com>
2025-07-09 18:36:40 +01:00
Pete Davison
64fb66895b chore: added changelogs for #2316 and #2322 2025-07-09 15:26:47 +00:00
Pete Davison
d2bd834c81 fix: spaces in path (#2322) 2025-07-09 16:21:42 +01:00
Andrey Nering
8a43ca5d8f chore: move away from deprecated func 2025-07-07 10:14:50 -03:00
Andrey Nering
a10a9faabf chore(taskfile): add go.mod as source for the lint tasks 2025-07-07 10:14:50 -03:00
renovate[bot]
3d3ed0e403 chore(deps): update all non-major dependencies 2025-07-07 10:14:50 -03:00
Pete Davison
47dc87a2c9 fix: remove extra breaking randInt function (#2316) 2025-07-03 23:08:43 +01:00
Pete Davison
3b0a746f85 feat: update go-task/template to latest version 2025-07-03 21:46:09 +00:00
renovate[bot]
281edfe5b3 chore(deps): update all non-major dependencies (#2311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 09:32:44 -03:00
Alexander Kavon
7289ffce0b docs: add macports / freebsd installation instructions (#2308) 2025-06-30 09:31:48 -03:00
dependabot[bot]
61e1af50ff chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 in /website (#2298)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 10:39:42 -03:00
renovate[bot]
715a143735 chore(deps): update all non-major dependencies (#2297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 09:26:27 -03:00
Andrey Nering
a0b1605634 chore: add changelog entry for #2291 2025-06-09 14:12:03 -03:00
Timothy Rule
69fc13bd13 fix(release): fix install script for armv5/6/7 (#2291) 2025-06-09 14:07:11 -03:00
renovate[bot]
b42a52ba77 chore(deps): update all non-major dependencies (#2289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 22:09:51 -03:00
Andrey Nering
cb812476b3 v3.44.0 2025-06-08 21:34:13 -03:00
Andrey Nering
b09c6870fe docs: add note about watcher reliability 2025-06-08 21:31:47 -03:00
Andrey Nering
86e4a3aac7 chore(changelog): add entried for watch fixes 2025-06-08 21:19:03 -03:00
Andrey Nering
7782bc92ae fix(watcher): fix some v3.43.x regressions (#2271) 2025-06-08 19:44:08 -03:00
renovate[bot]
9cc2d65091 chore(deps): update all non-major dependencies (#2281) 2025-06-02 13:26:32 +00:00
Andrey Nering
b932e539d9 chore: go mod tidy 2025-05-28 22:08:27 -03:00
Teddy Sommavilla
be45eb04d9 refactor: watchTasks - Chmod operations are already filtered in the Deduper 2025-05-26 16:51:37 -03:00
Teddy Sommavilla
6b878980dc refactor(fsnotifyext): use Event.Has to check for chmod operations
As recommended by the Event.Op godoc. Op is a bitmask, and some systems may send multiple operations at once
2025-05-26 16:51:37 -03:00
Teddy Sommavilla
cd910abd45 doc(fsnotifyext): add godoc for GetChan method 2025-05-26 16:51:37 -03:00
Teddy Sommavilla
6e524bb2fa refactor(fsnotifyext): GetChan should return a receive only chan 2025-05-26 16:51:37 -03:00
Teddy Sommavilla
b4c8f5a0fe refactor(fsnotifyext): handle Deduper timers in own goroutine, avoid mutex use 2025-05-26 16:51:37 -03:00
renovate[bot]
09f85844ba chore(deps): update all non-major dependencies (#2270) 2025-05-26 16:39:01 -03:00
Pete Davison
d54d2ccabc chore: add special variables task to remote for testing 2025-05-24 13:33:55 +00:00
Pete Davison
cf81ab3112 chore: go mod tidy 2025-05-24 13:11:02 +00:00
Pete Davison
aaa7b7772d chore: changelog for #2223 2025-05-24 13:03:29 +00:00
Pete Davison
71eb8cdeea feat: checksum pinning (#2223) 2025-05-24 14:00:02 +01:00
Pete Davison
68ce8b1d84 chore: changelog for #2220 2025-05-24 12:41:31 +00:00
Pete Davison
5323990c72 feat: redact credentials in remote urls (#2220)
* feat: redact credentials in remote urls

* chore: improve function naming

* fix: TaskfileNotSecureError should use redacted URI

* feat: unexport all node implementation fields

* fix: unexport HTTPNode.url
2025-05-24 13:38:18 +01:00
Pete Davison
ec4e68d601 chore: changelog for #2256 2025-05-20 20:40:28 +00:00
Aleksander Sh.
bb5b045293 feat: add task name to json output (#2256) 2025-05-20 21:37:57 +01:00
renovate[bot]
89f29cb75b chore(deps): update all non-major dependencies (#2260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 13:38:05 +02:00
Andrey Nering
da4ce5b0a5 fix(expand): return nothing if there are no matches 2025-05-09 15:55:52 -03:00
renovate[bot]
fb68a5f79a chore(deps): update golangci/golangci-lint-action action to v8 (#2237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2025-05-06 20:45:06 +02:00
renovate[bot]
f40f389cb4 chore(deps): update all non-major dependencies (#2236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 20:43:16 +02:00
Valentin Maerten
a459eeaabb chore: changelog for #2233 2025-05-03 17:18:27 +02:00
Valentin Maerten
84f02a822f docs: mention that method key is allowed at root level (#2233) 2025-05-03 17:17:11 +02:00
Valentin Maerten
55d1aa260d chore: changelog for #2211 2025-05-03 17:12:31 +02:00
Valentin Maerten
e7084cdf26 chore: update schemas only when a release is done (#2211) 2025-05-03 17:11:56 +02:00
Pete Davison
ca55e9b621 chore: changelog for #2225 2025-05-01 17:58:47 +00:00
Pete Davison
6528b36caa feat: add uuid and rand number functions (#2225)
* feat: add uuid and rand number functions

* chore: remove randFloat for now
2025-05-01 17:58:01 +00:00
Pete Davison
f8736c5f77 chore: changelog for #2140 2025-05-01 17:51:47 +00:00
Pete Davison
6896accf86 feat: cli args list (#2140) 2025-05-01 18:43:43 +01:00
Pete Davison
c12ed49acb chore: remove unused any2 testdata 2025-04-28 21:04:24 +00:00
Pete Davison
d1bfd3e9f7 docs: move yaml templating functions to the correct section 2025-04-28 20:57:18 +00:00
renovate[bot]
fc17343fcc chore(deps): update all non-major dependencies (#2214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 14:37:44 +02:00
Pete Davison
d3e9be1520 chore: changelog for #2219 2025-04-28 12:21:26 +00:00
Pete Davison
d850d03c96 feat: add yaml templating functions (#2219)
* feat: add yaml templating functions

* docs: add yaml functions to templating reference

* refactor: remove some unnecessary function wrappers
2025-04-28 12:19:56 +00:00
Pete Davison
0058f18676 chore: changelog for #2216 2025-04-28 12:05:10 +00:00
Pete Davison
b3c4007756 fix: double escaped paths (#2216) 2025-04-28 13:02:46 +01:00
Pete Davison
9e8fd54be9 chore: changelog for #2200 2025-04-27 23:02:32 +00:00
Valentin Maerten
a33544101a fix: fuzzy model was not instanciated (#2200)
* fix: fuzzy model was not instanciated

* add test

* add test
2025-04-28 00:00:54 +01:00
Pete Davison
1c35358fcc v3.43.3 2025-04-27 22:29:34 +00:00
Pete Davison
13daa6dc35 feat: formatting with golangci-lint and gci 2025-04-27 22:28:42 +00:00
Pete Davison
20c1ffe098 docs: update variables example so that it doesn't error 2025-04-27 22:26:59 +00:00
Pete Davison
bd8ccb8d03 chore: changelogs for reverts 2025-04-27 22:26:29 +00:00
Pete Davison
8162b05f59 Revert "feat: process variables in include vars (#2113)"
This reverts commit f0414f162d.
2025-04-27 22:15:49 +00:00
Pete Davison
68d5095761 Revert "fix: .USER_WORKING_DIR should contain the value of --dir if given (#2186)"
This reverts commit 768dca053b.
2025-04-27 22:14:50 +00:00
128 changed files with 6525 additions and 5195 deletions

View File

@@ -23,9 +23,9 @@ jobs:
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v8
with:
version: v2.0.2
version: v2.1.0
lint-jsonschema:
runs-on: ubuntu-latest
@@ -56,3 +56,19 @@ jobs:
with:
script: |
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.')
check_schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v46
with:
files: |
website/static/schema.json
website/static/schema-taskrc.json
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('schema.json or schema-taskrc.json has changed. Instead you need to update next-schema.json or next-schema-taskrc.json.')

29
.github/workflows/release-nightly.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Realease nightly
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * *
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
args: release --clean --nightly -f .goreleaser-nightly.yml
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}

View File

@@ -24,7 +24,7 @@ jobs:
with:
distribution: goreleaser-pro
version: latest
args: release --clean
args: release --clean --draft
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}

View File

@@ -5,8 +5,10 @@ formatters:
- gofmt
- gofumpt
- goimports
- gci
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
@@ -15,6 +17,12 @@ formatters:
goimports:
local-prefixes:
- github.com/go-task
gci:
sections:
- standard
- default
- prefix(github.com/go-task)
- localmodule
exclusions:
generated: lax
paths:

15
.goreleaser-nightly.yml Normal file
View File

@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
pro: true
release:
name_template: 'v{{.Version}}'
nightly:
publish_release: true
keep_single_release: true
version_template: "{{incminor .Version}}-nightly"
includes:
- from_file:
path: ./.goreleaser.yml

View File

@@ -30,13 +30,14 @@ builds:
flags:
- -trimpath
ldflags:
- -s -w # Don't set main.version.
- "-s -w"
- "{{if .IsNightly}}-X github.com/go-task/task/v3/internal/version.version={{.Version}}{{end}}"
gomod:
proxy: true
archives:
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
files:
- README.md
- LICENSE
@@ -45,27 +46,29 @@ archives:
- goos: windows
formats: [zip]
release:
draft: true
git:
ignore_tags:
- "{{if not .IsNightly}}nightly{{end}}"
snapshot:
version_template: "{{.Version}}"
version_template: '{{.Version}}'
checksum:
name_template: "task_checksums.txt"
name_template: 'task_checksums.txt'
nfpms:
- vendor: Task
homepage: https://taskfile.dev
maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go
section: golang
license: MIT
conflicts:
- taskwarrior
formats:
- deb
- rpm
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
contents:
- src: completion/bash/task.bash
dst: /etc/bash_completion.d/task
@@ -83,8 +86,7 @@ brews:
repository:
owner: go-task
name: homebrew-tap
test:
system "#{bin}/task", "--help"
test: system "#{bin}/task", "--help"
install: |-
bin.install "task"
bash_completion.install "completion/bash/task.bash" => "task"
@@ -107,7 +109,7 @@ winget:
commit_author:
name: task-bot
email: 106601941+task-bot@users.noreply.github.com
commit_msg_template: "chore: bump {{.PackageIdentifier}} to {{.Tag}}"
commit_msg_template: 'chore: release {{.PackageIdentifier}} {{.Tag}}'
release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}}
tags:
- build
@@ -121,13 +123,15 @@ winget:
- task-runner
- taskfile
- tool
skip_upload: true
repository:
owner: microsoft
name: winget-pkgs
pull_request:
enabled: true
base:
owner: go-task
name: winget-pkgs
branch: "bump-task-to-{{.Tag}}"
branch: 'chore/task-{{.Version}}'
pull_request:
enabled: true
draft: false
check_boxes: true
base:
owner: microsoft
name: winget-pkgs
branch: master

2
.nvmrc
View File

@@ -1 +1 @@
22.14.0
22.18.0

View File

@@ -1,5 +1,51 @@
# Changelog
## v3.44.1 - 2025-07-23
- Internal tasks will no longer be shown as suggestions since they cannot be
called (#2309, #2323 by @maxmzkrcensys)
- Fixed install script for some ARM platforms (#1516, #2291 by @trulede).
- Fixed a regression where fingerprinting was not working correctly if the path
to you Taskfile contained a space (#2321, #2322 by @pd93).
- Reverted a breaking change to `randInt` (#2312, #2316 by @pd93).
- Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests
(#2265 by @pd93).
## v3.44.0 - 2025-06-08
- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by
@pd93).
- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed
to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a
string). (#2138, #2139, #2140 by @pd93).
- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).
- Added `task` field the `--list --json` output (#2256 by @aleksandersh).
- Added the ability to
[pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)
by specifying a checksum. This works with both local and remote Taskfiles
(#2222, #2223 by @pd93).
- When using the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),
any credentials used in the URL will now be redacted in Task's output (#2100,
#2220 by @pd93).
- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200
by @vmaerten).
- Fixed a bug where taskfiles in directories containing spaces created
directories in the wrong location (#2208, #2216 by @pd93).
- Added support for dual JSON schema files, allowing changes without affecting
the current schema. The current schemas will only be updated during releases.
(#2211 by @vmaerten).
- Improved fingerprint documentation by specifying that the method can be set at
the root level to apply to all tasks (#2233 by @vmaerten).
- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by
@wazazaby, #2271 by @andreynering).
## v3.43.3 - 2025-04-27
Reverted the changes made in #2113 and #2186 that affected the
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
#2208.
## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by

View File

@@ -53,9 +53,12 @@ tasks:
generate:fixtures:
desc: Runs tests and generates golden fixture files
aliases: [gen:fixtures, g:fixtures]
env:
GOLDIE_UPDATE: 'true'
GOLDIE_TEMPLATE: 'true'
cmds:
- find ./testdata -name '*.golden' -delete
- go test -update ./...
- go test ./...
install:mockery:
desc: Installs mockgen; a tool to generate mock files
@@ -87,6 +90,7 @@ tasks:
sources:
- './**/*.go'
- .golangci.yml
- go.mod
cmds:
- golangci-lint run
@@ -95,9 +99,19 @@ tasks:
sources:
- './**/*.go'
- .golangci.yml
- go.mod
cmds:
- golangci-lint run --fix
format:
desc: Runs golangci-lint and formats any Go files
aliases: [fmt, f]
sources:
- './**/*.go'
- .golangci.yml
cmds:
- golangci-lint fmt
sleepit:build:
desc: Builds the sleepit test helper
sources:

View File

@@ -13,24 +13,14 @@ import (
// Get fetches the remaining arguments after CLI parsing and splits them into
// two groups: the arguments before the double dash (--) and the arguments after
// the double dash.
func Get() ([]string, string, error) {
func Get() ([]string, []string, error) {
args := pflag.Args()
doubleDashPos := pflag.CommandLine.ArgsLenAtDash()
if doubleDashPos == -1 {
return args, "", nil
return args, nil, nil
}
var quotedCliArgs []string
for _, arg := range args[doubleDashPos:] {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return nil, "", err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
return args[:doubleDashPos], args[doubleDashPos:], nil
}
// Parse parses command line argument: tasks and global variables
@@ -51,6 +41,18 @@ func Parse(args ...string) ([]*task.Call, *ast.Vars) {
return calls, globals
}
func ToQuotedString(args []string) (string, error) {
var quotedCliArgs []string
for _, arg := range args {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return "", err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return strings.Join(quotedCliArgs, " "), nil
}
func splitVar(s string) (string, string) {
pair := strings.SplitN(s, "=", 2)
return pair[0], pair[1]

View File

@@ -20,6 +20,10 @@ const (
changelogTarget = "website/docs/changelog.mdx"
docsSource = "website/docs"
docsTarget = "website/versioned_docs/version-latest"
schemaSource = "website/static/next-schema.json"
schemaTarget = "website/static/schema.json"
schemaTaskrcSource = "website/static/next-schema-taskrc.json"
schemaTaskrcTarget = "website/static/schema-taskrc.json"
)
var (
@@ -83,6 +87,10 @@ func release() error {
return err
}
if err := schema(); err != nil {
return err
}
return nil
}
@@ -175,3 +183,13 @@ func docs() error {
}
return nil
}
func schema() error {
if err := copy.Copy(schemaSource, schemaTarget); err != nil {
return err
}
if err := copy.Copy(schemaTaskrcSource, schemaTaskrcTarget); err != nil {
return err
}
return nil
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -110,16 +111,63 @@ func run() error {
return nil
}
e := task.NewExecutor(
flags.WithFlags(),
task.WithVersionCheck(true),
if err := experiments.Validate(); err != nil {
log.Warnf("%s\n", err.Error())
}
// Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
flags.Entrypoint,
flags.Dir,
flags.Insecure,
)
if err := e.Setup(); err != nil {
if err != nil {
return err
}
tempDir, err := task.NewTempDir(node.Dir())
if err != nil {
return err
}
reader := taskfile.NewReader(
flags.WithFlags(),
taskfile.WithTempDir(tempDir.Remote),
taskfile.WithDebugFunc(func(s string) {
log.VerboseOutf(logger.Magenta, s)
}),
taskfile.WithPromptFunc(func(s string) error {
return log.Prompt(logger.Yellow, s, "n", "y", "yes")
}),
)
ctx, cf := context.WithTimeout(context.Background(), flags.Timeout)
defer cf()
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: flags.Timeout}
}
return err
}
executor, err := task.NewExecutor(graph,
flags.WithFlags(),
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
)
if err != nil {
return err
}
// If the download flag is specified, we should stop execution as soon as
// taskfile is downloaded
if flags.Download {
return nil
}
if flags.ClearCache {
cachePath := filepath.Join(e.TempDir.Remote, "remote")
cachePath := filepath.Join(executor.TempDir.Remote, "remote")
return os.RemoveAll(cachePath)
}
@@ -131,9 +179,9 @@ func run() error {
)
if listOptions.ShouldListTasks() {
if flags.Silent {
return e.ListTaskNames(flags.ListAll)
return executor.ListTaskNames(flags.ListAll)
}
foundTasks, err := e.ListTasks(listOptions)
foundTasks, err := executor.ListTasks(listOptions)
if err != nil {
return err
}
@@ -144,33 +192,38 @@ func run() error {
}
// Parse the remaining arguments
argv, cliArgs, err := args.Get()
cliArgsPreDash, cliArgsPostDash, err := args.Get()
if err != nil {
return err
}
calls, globals := args.Parse(argv...)
calls, globals := args.Parse(cliArgsPreDash...)
// If there are no calls, run the default task instead
if len(calls) == 0 {
calls = append(calls, &task.Call{Task: "default"})
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
if err != nil {
return err
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
e.Taskfile.Vars.Merge(globals, nil)
executor.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch {
e.InterceptInterruptSignals()
executor.InterceptInterruptSignals()
}
ctx := context.Background()
ctx = context.Background()
if flags.Status {
return e.Status(ctx, calls...)
return executor.Status(ctx, calls...)
}
return e.Run(ctx, calls...)
return executor.Run(ctx, calls...)
}

View File

@@ -26,6 +26,7 @@ const (
CodeTaskfileNetworkTimeout
CodeTaskfileInvalid
CodeTaskfileCycle
CodeTaskfileDoesNotMatchChecksum
)
// Task related exit codes

View File

@@ -1,6 +1,7 @@
package errors
import (
"errors"
"fmt"
"strings"
@@ -46,8 +47,9 @@ func (err *TaskRunError) Code() int {
}
func (err *TaskRunError) TaskExitCode() int {
if c, ok := interp.IsExitStatus(err.Err); ok {
return int(c)
var exit interp.ExitStatus
if errors.As(err.Err, &exit) {
return int(exit)
}
return err.Code()
}

View File

@@ -187,3 +187,24 @@ func (err TaskfileCycleError) Error() string {
func (err TaskfileCycleError) Code() int {
return CodeTaskfileCycle
}
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
// match the one pinned in the parent Taskfile.
type TaskfileDoesNotMatchChecksum struct {
URI string
ExpectedChecksum string
ActualChecksum string
}
func (err *TaskfileDoesNotMatchChecksum) Error() string {
return fmt.Sprintf(
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
err.URI,
err.ActualChecksum,
err.ExpectedChecksum,
)
}
func (err *TaskfileDoesNotMatchChecksum) Code() int {
return CodeTaskfileDoesNotMatchChecksum
}

View File

@@ -28,15 +28,9 @@ type (
Executor struct {
// Flags
Dir string
Entrypoint string
TempDir TempDir
TempDir *TempDir
Force bool
ForceAll bool
Insecure bool
Download bool
Offline bool
Timeout time.Duration
CacheExpiryDuration time.Duration
Watch bool
Verbose bool
Silent bool
@@ -73,17 +67,17 @@ type (
executionHashesMutex sync.Mutex
watchedDirs *xsync.MapOf[string, bool]
}
TempDir struct {
Remote string
Fingerprint string
}
)
// NewExecutor creates a new [Executor] and applies the given functional options
// to it.
func NewExecutor(opts ...ExecutorOption) *Executor {
func NewExecutor(graph *ast.TaskfileGraph, opts ...ExecutorOption) (*Executor, error) {
tf, err := graph.Merge()
if err != nil {
return nil, err
}
e := &Executor{
Timeout: time.Second * 10,
Taskfile: tf,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
@@ -101,7 +95,10 @@ func NewExecutor(opts ...ExecutorOption) *Executor {
executionHashesMutex: sync.Mutex{},
}
e.Options(opts...)
return e
if err := e.setup(); err != nil {
return nil, err
}
return e, nil
}
// Options loops through the given [ExecutorOption] functions and applies them
@@ -123,34 +120,23 @@ type dirOption struct {
}
func (o *dirOption) ApplyToExecutor(e *Executor) {
e.UserWorkingDir, _ = filepath.Abs(o.dir)
absDir, err := filepath.Abs(o.dir)
if err != nil {
e.Dir = o.dir
return
}
// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By
// default, Task will search for one of the default Taskfiles in the given
// directory.
func WithEntrypoint(entrypoint string) ExecutorOption {
return &entrypointOption{entrypoint}
}
type entrypointOption struct {
entrypoint string
}
func (o *entrypointOption) ApplyToExecutor(e *Executor) {
e.Entrypoint = o.entrypoint
e.Dir = absDir
}
// WithTempDir sets the temporary directory that will be used by [Executor] for
// storing temporary files like checksums and cached remote files. By default,
// the temporary directory is set to the user's temporary directory.
func WithTempDir(tempDir TempDir) ExecutorOption {
func WithTempDir(tempDir *TempDir) ExecutorOption {
return &tempDirOption{tempDir}
}
type tempDirOption struct {
tempDir TempDir
tempDir *TempDir
}
func (o *tempDirOption) ApplyToExecutor(e *Executor) {
@@ -185,76 +171,6 @@ func (o *forceAllOption) ApplyToExecutor(e *Executor) {
e.ForceAll = o.forceAll
}
// WithInsecure allows the [Executor] to make insecure connections when reading
// remote taskfiles. By default, insecure connections are rejected.
func WithInsecure(insecure bool) ExecutorOption {
return &insecureOption{insecure}
}
type insecureOption struct {
insecure bool
}
func (o *insecureOption) ApplyToExecutor(e *Executor) {
e.Insecure = o.insecure
}
// WithDownload forces the [Executor] to download a fresh copy of the taskfile
// from the remote source.
func WithDownload(download bool) ExecutorOption {
return &downloadOption{download}
}
type downloadOption struct {
download bool
}
func (o *downloadOption) ApplyToExecutor(e *Executor) {
e.Download = o.download
}
// WithOffline stops the [Executor] from being able to make network connections.
// It will still be able to read local files and cached copies of remote files.
func WithOffline(offline bool) ExecutorOption {
return &offlineOption{offline}
}
type offlineOption struct {
offline bool
}
func (o *offlineOption) ApplyToExecutor(e *Executor) {
e.Offline = o.offline
}
// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
// default, the timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ExecutorOption {
return &timeoutOption{timeout}
}
type timeoutOption struct {
timeout time.Duration
}
func (o *timeoutOption) ApplyToExecutor(e *Executor) {
e.Timeout = o.timeout
}
// WithCacheExpiryDuration sets the duration after which the cache is considered
// expired. By default, the cache is considered expired after 24 hours.
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration}
}
type cacheExpiryDurationOption struct {
duration time.Duration
}
func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
r.CacheExpiryDuration = o.duration
}
// WithWatch tells the [Executor] to keep running in the background and watch
// for changes to the fingerprint of the tasks that are run. When changes are
// detected, a new task run is triggered.

View File

@@ -4,18 +4,13 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/version"
@@ -23,18 +18,8 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)
func (e *Executor) Setup() error {
func (e *Executor) setup() error {
e.setupLogger()
node, err := e.getRootNode()
if err != nil {
return err
}
if err := e.setupTempDir(); err != nil {
return err
}
if err := e.readTaskfile(node); err != nil {
return err
}
e.setupFuzzyModel()
e.setupStdFiles()
if err := e.setupOutput(); err != nil {
@@ -54,48 +39,8 @@ func (e *Executor) Setup() error {
return nil
}
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
if err != nil {
return nil, err
}
e.Dir = node.Dir()
return node, err
}
func (e *Executor) readTaskfile(node taskfile.Node) error {
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
defer cf()
debugFunc := func(s string) {
e.Logger.VerboseOutf(logger.Magenta, s)
}
promptFunc := func(s string) error {
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
}
reader := taskfile.NewReader(
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
}
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
return err
}
return nil
}
func (e *Executor) setupFuzzyModel() {
if e.Taskfile != nil {
if e.Taskfile == nil {
return
}
@@ -104,6 +49,9 @@ func (e *Executor) setupFuzzyModel() {
var words []string
for name, task := range e.Taskfile.Tasks.All(nil) {
if task.Internal {
continue
}
words = append(words, name)
words = slices.Concat(words, task.Aliases)
}
@@ -112,52 +60,6 @@ func (e *Executor) setupFuzzyModel() {
e.fuzzyModel = model
}
func (e *Executor) setupTempDir() error {
if e.TempDir != (TempDir{}) {
return nil
}
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return err
}
projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir)
e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, tempDir),
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
}
}
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir != "" {
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
}
return nil
}
func (e *Executor) setupStdFiles() {
if e.Stdin == nil {
e.Stdin = os.Stdin
@@ -203,7 +105,7 @@ func (e *Executor) setupCompiler() error {
e.Compiler = &Compiler{
Dir: e.Dir,
Entrypoint: e.Entrypoint,
Entrypoint: e.Taskfile.Location,
UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"testing"
"github.com/sebdah/goldie/v2"
@@ -15,6 +16,7 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -34,7 +36,12 @@ type (
task string
vars map[string]any
input string
nodeDir string
nodeEntrypoint string
nodeInsecure bool
readerOpts []taskfile.ReaderOption
executorOpts []task.ExecutorOption
wantReaderError bool
wantSetupError bool
wantRunError bool
wantStatusError bool
@@ -49,8 +56,10 @@ func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
tt := &ExecutorTest{
task: "default",
vars: map[string]any{},
nodeDir: ".",
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
},
}
// Apply the functional options
@@ -144,11 +153,52 @@ func (tt *ExecutorTest) run(t *testing.T) {
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
ctx := context.Background()
opts := append(
// Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
tt.nodeEntrypoint,
tt.nodeDir,
tt.nodeInsecure,
)
require.NoError(t, err)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")),
)
// Set up a temporary directory for the taskfile reader and task executor
tempDir, err := task.NewTempDir(node.Dir())
require.NoError(t, err)
tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote))
// Set up the taskfile reader
reader := taskfile.NewReader(tt.readerOpts...)
graph, err := reader.Read(ctx, node)
if tt.wantReaderError {
require.Error(t, err)
tt.writeFixtureErrReader(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
executorOpts := slices.Concat(
// Apply the node directory and temp directory to the executor options
// by default, but allow them to by overridden by the test options
[]task.ExecutorOption{
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
},
// Apply the executor options from the test
tt.executorOpts,
// Force the input/output streams to be set to the test buffer
[]task.ExecutorOption{
task.WithStdout(&buf),
task.WithStderr(&buf),
},
)
// If the test has input, create a reader for it and add it to the
@@ -156,19 +206,12 @@ func (tt *ExecutorTest) run(t *testing.T) {
if tt.input != "" {
var reader bytes.Buffer
reader.WriteString(tt.input)
opts = append(opts, task.WithStdin(&reader))
executorOpts = append(executorOpts, task.WithStdin(&reader))
}
// Set up the task executor
e := task.NewExecutor(opts...)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
)
// Call setup and check for errors
if err := e.Setup(); tt.wantSetupError {
executor, err := task.NewExecutor(graph, executorOpts...)
if tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
@@ -188,8 +231,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
}
// Run the task and check for errors
ctx := context.Background()
if err := e.Run(ctx, call); tt.wantRunError {
if err := executor.Run(ctx, call); tt.wantRunError {
require.Error(t, err)
tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
@@ -200,7 +242,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
// If the status flag is set, run the status check
if tt.wantStatusError {
if err := e.Status(ctx, call); err != nil {
if err := executor.Status(ctx, call); err != nil {
tt.writeFixtureStatus(t, g, err.Error())
}
}
@@ -219,20 +261,17 @@ func (tt *ExecutorTest) run(t *testing.T) {
func TestEmptyTask(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/empty_task"),
),
WithNodeDir("testdata/empty_task"),
WithExecutorOptions(),
)
}
func TestEmptyTaskfile(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithExecutorOptions(
task.WithDir("testdata/empty_taskfile"),
),
WithSetupError(),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithNodeDir("testdata/empty_taskfile"),
WithReaderError(),
WithFixtureTemplating(),
)
}
@@ -240,15 +279,15 @@ func TestEnv(t *testing.T) {
t.Setenv("QUX", "from_os")
NewExecutorTest(t,
WithName("env precedence disabled"),
WithNodeDir("testdata/env"),
WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true),
),
)
NewExecutorTest(t,
WithName("env precedence enabled"),
WithNodeDir("testdata/env"),
WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true),
),
WithExperiment(&experiments.EnvPrecedence, 1),
@@ -258,8 +297,8 @@ func TestEnv(t *testing.T) {
func TestVars(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/vars"),
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
)
@@ -269,25 +308,19 @@ func TestRequires(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("required var missing"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("missing-var"),
WithRunError(),
)
NewExecutorTest(t,
WithName("required var ok"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("missing-var"),
WithVar("FOO", "bar"),
)
NewExecutorTest(t,
WithName("fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("validation-var"),
WithVar("ENV", "dev"),
WithVar("FOO", "bar"),
@@ -295,48 +328,37 @@ func TestRequires(t *testing.T) {
)
NewExecutorTest(t,
WithName("passes validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("validation-var"),
WithVar("FOO", "one"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("required var missing + fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("validation-var"),
WithRunError(),
)
NewExecutorTest(t,
WithName("required var missing + fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("validation-var-dynamic"),
WithVar("FOO", "one"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("require before compile"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("require-before-compile"),
WithRunError(),
)
NewExecutorTest(t,
WithName("var defined in task"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithNodeDir("testdata/requires"),
WithTask("var-defined-in-task"),
)
}
// TODO: mock fs
func TestSpecialVars(t *testing.T) {
t.Parallel()
@@ -357,17 +379,18 @@ func TestSpecialVars(t *testing.T) {
"included:print-taskfile-dir",
}
for _, dir := range []string{dir, subdir} {
for _, executorDir := range []string{dir, subdir} {
for _, test := range tests {
name := fmt.Sprintf("%s-%s", executorDir, test)
NewExecutorTest(t,
WithName(fmt.Sprintf("%s-%s", dir, test)),
WithName(name),
WithNodeDir(executorDir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
task.WithVersionCheck(true),
),
WithTask(test),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
)
}
}
@@ -376,8 +399,8 @@ func TestSpecialVars(t *testing.T) {
func TestConcurrency(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/concurrency"),
WithExecutorOptions(
task.WithDir("testdata/concurrency"),
task.WithConcurrency(1),
),
WithPostProcessFn(PPSortedLines),
@@ -387,8 +410,8 @@ func TestConcurrency(t *testing.T) {
func TestParams(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/params"),
WithExecutorOptions(
task.WithDir("testdata/params"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
@@ -398,15 +421,14 @@ func TestParams(t *testing.T) {
func TestDeps(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/deps"),
WithExecutorOptions(
task.WithDir("testdata/deps"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
)
}
// TODO: mock fs
func TestStatus(t *testing.T) {
t.Parallel()
@@ -429,8 +451,8 @@ func TestStatus(t *testing.T) {
// gen-foo creates foo.txt, and will always fail it's status check.
NewExecutorTest(t,
WithName("run gen-foo 1 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-foo"),
@@ -441,8 +463,8 @@ func TestStatus(t *testing.T) {
// only exists after the first run.
NewExecutorTest(t,
WithName("run gen-bar 1 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -451,8 +473,8 @@ func TestStatus(t *testing.T) {
// if e.Verbose is set to true.
NewExecutorTest(t,
WithName("run gen-baz silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-silent-baz"),
@@ -467,8 +489,8 @@ func TestStatus(t *testing.T) {
// Run gen-bar a second time to produce a checksum file that matches bar.txt
NewExecutorTest(t,
WithName("run gen-bar 2 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -476,8 +498,8 @@ func TestStatus(t *testing.T) {
// Run gen-bar a third time, to make sure we've triggered the status check.
NewExecutorTest(t,
WithName("run gen-bar 3 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -489,8 +511,8 @@ func TestStatus(t *testing.T) {
require.NoError(t, err)
NewExecutorTest(t,
WithName("run gen-bar 4 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -498,60 +520,48 @@ func TestStatus(t *testing.T) {
// all: not up-to-date
NewExecutorTest(t,
WithName("run gen-foo 2"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("gen-foo"),
)
// status: not up-to-date
NewExecutorTest(t,
WithName("run gen-foo 3"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("gen-foo"),
)
// sources: not up-to-date
NewExecutorTest(t,
WithName("run gen-bar 5"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("gen-bar"),
)
// all: up-to-date
NewExecutorTest(t,
WithName("run gen-bar 6"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("gen-bar"),
)
// sources: not up-to-date, no output produced.
NewExecutorTest(t,
WithName("run gen-baz 2"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("gen-silent-baz"),
)
// up-to-date, no output produced
NewExecutorTest(t,
WithName("run gen-baz 3"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("gen-silent-baz"),
)
// up-to-date, output produced due to Verbose mode.
NewExecutorTest(t,
WithName("run gen-baz 4 verbose"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithVerbose(true),
),
WithTask("gen-silent-baz"),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
)
}
@@ -560,32 +570,24 @@ func TestPrecondition(t *testing.T) {
const dir = "testdata/precondition"
NewExecutorTest(t,
WithName("a precondition has been met"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("a precondition was not met"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("impossible"),
WithRunError(),
)
NewExecutorTest(t,
WithName("precondition in dependency fails the task"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("depends_on_impossible"),
WithRunError(),
)
NewExecutorTest(t,
WithName("precondition in cmd fails the task"),
WithExecutorOptions(
task.WithDir(dir),
),
WithNodeDir(dir),
WithTask("executes_failing_task_as_cmd"),
WithRunError(),
)
@@ -596,25 +598,21 @@ func TestAlias(t *testing.T) {
NewExecutorTest(t,
WithName("alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
),
WithNodeDir("testdata/alias"),
WithTask("f"),
)
NewExecutorTest(t,
WithName("duplicate alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
),
WithNodeDir("testdata/alias"),
WithTask("x"),
WithRunError(),
)
NewExecutorTest(t,
WithName("alias summary"),
WithNodeDir("testdata/alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
task.WithSummary(true),
),
WithTask("f"),
@@ -626,16 +624,14 @@ func TestLabel(t *testing.T) {
NewExecutorTest(t,
WithName("up to date"),
WithExecutorOptions(
task.WithDir("testdata/label_uptodate"),
),
WithNodeDir("testdata/label_uptodate"),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("summary"),
WithNodeDir("testdata/label_summary"),
WithExecutorOptions(
task.WithDir("testdata/label_summary"),
task.WithSummary(true),
),
WithTask("foo"),
@@ -643,26 +639,20 @@ func TestLabel(t *testing.T) {
NewExecutorTest(t,
WithName("status"),
WithExecutorOptions(
task.WithDir("testdata/label_status"),
),
WithNodeDir("testdata/label_status"),
WithTask("foo"),
WithStatusError(),
)
NewExecutorTest(t,
WithName("var"),
WithExecutorOptions(
task.WithDir("testdata/label_var"),
),
WithNodeDir("testdata/label_var"),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("label in summary"),
WithExecutorOptions(
task.WithDir("testdata/label_summary"),
),
WithNodeDir("testdata/label_summary"),
WithTask("foo"),
)
}
@@ -689,8 +679,8 @@ func TestPromptInSummary(t *testing.T) {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("foo"),
@@ -708,8 +698,8 @@ func TestPromptWithIndirectTask(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("bar"),
@@ -722,8 +712,8 @@ func TestPromptAssumeYes(t *testing.T) {
NewExecutorTest(t,
WithName("--yes flag should skip prompt"),
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
task.WithAssumeYes(true),
),
@@ -733,8 +723,8 @@ func TestPromptAssumeYes(t *testing.T) {
NewExecutorTest(t,
WithName("task should raise errors.TaskCancelledError"),
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("foo"),
@@ -771,13 +761,13 @@ func TestForCmds(t *testing.T) {
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/for/cmds"),
WithExecutorOptions(
task.WithDir("testdata/for/cmds"),
task.WithSilent(true),
task.WithForce(true),
),
WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
}
if test.wantErr {
opts = append(opts, WithRunError())
@@ -814,15 +804,15 @@ func TestForDeps(t *testing.T) {
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/for/deps"),
WithExecutorOptions(
task.WithDir("testdata/for/deps"),
task.WithSilent(true),
task.WithForce(true),
// Force output of each dep to be grouped together to prevent interleaving
task.WithOutputStyle(ast.Output{Name: "group"}),
),
WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
WithPostProcessFn(PPSortedLines),
}
if test.wantErr {
@@ -860,8 +850,8 @@ func TestReference(t *testing.T) {
for _, test := range tests {
NewExecutorTest(t,
WithName(test.name),
WithNodeDir("testdata/var_references"),
WithExecutorOptions(
task.WithDir("testdata/var_references"),
task.WithSilent(true),
task.WithForce(true),
),
@@ -928,8 +918,8 @@ func TestVarInheritance(t *testing.T) {
for _, test := range tests {
NewExecutorTest(t,
WithName(test.name),
WithNodeDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
WithExecutorOptions(
task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
task.WithSilent(true),
task.WithForce(true),
),
@@ -937,3 +927,95 @@ func TestVarInheritance(t *testing.T) {
)
}
}
func TestFuzzyModel(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("fuzzy"),
WithNodeDir("testdata/fuzzy"),
WithTask("instal"),
WithRunError(),
)
NewExecutorTest(t,
WithName("not-fuzzy"),
WithNodeDir("testdata/fuzzy"),
WithTask("install"),
)
NewExecutorTest(t,
WithName("intern"),
WithNodeDir("testdata/fuzzy"),
WithTask("intern"),
WithRunError(),
)
}
func TestIncludeChecksum(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("correct"),
WithNodeDir("testdata/includes_checksum/correct"),
)
NewExecutorTest(t,
WithName("incorrect"),
WithNodeDir("testdata/includes_checksum/incorrect"),
WithReaderError(),
WithFixtureTemplating(),
)
}
func TestWildcard(t *testing.T) {
t.Parallel()
tests := []struct {
name string
call string
wantErr bool
}{
{
name: "basic wildcard",
call: "wildcard-foo",
},
{
name: "double wildcard",
call: "foo-wildcard-bar",
},
{
name: "store wildcard",
call: "start-foo",
},
{
name: "matches exactly",
call: "matches-exactly-*",
},
{
name: "no matches",
call: "no-match",
wantErr: true,
},
{
name: "multiple matches",
call: "wildcard-foo-bar",
},
}
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/wildcards"),
WithExecutorOptions(
task.WithSilent(true),
task.WithForce(true),
),
WithTask(test.call),
}
if test.wantErr {
opts = append(opts, WithRunError())
}
NewExecutorTest(t, opts...)
}
}

View File

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

21
go.mod
View File

@@ -4,8 +4,8 @@ go 1.23.0
require (
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.16.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0
@@ -13,22 +13,23 @@ require (
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.0
github.com/go-git/go-git/v5 v5.16.2
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.1.0
github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.5.5
github.com/spf13/pflag v1.0.6
github.com/sebdah/goldie/v2 v2.7.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.13.0
golang.org/x/term v0.31.0
golang.org/x/sync v0.16.0
golang.org/x/term v0.33.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.11.0
mvdan.cc/sh/v3 v3.12.0
)
require (
@@ -55,6 +56,6 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/sys v0.34.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

72
go.sum
View File

@@ -2,21 +2,19 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA=
github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -25,8 +23,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@@ -36,8 +32,6 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
@@ -50,8 +44,6 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -62,22 +54,20 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -121,16 +111,16 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
@@ -146,21 +136,15 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -170,18 +154,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -193,5 +173,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

View File

@@ -149,6 +149,7 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
g.Go(func() error {
o.Tasks[i] = editors.Task{
Name: tasks[i].Name(),
Task: tasks[i].Task,
Desc: tasks[i].Desc,
Summary: tasks[i].Summary,
Aliases: aliases,

View File

@@ -64,21 +64,15 @@ get_binaries() {
case "$PLATFORM" in
darwin/amd64) BINARIES="task" ;;
darwin/arm64) BINARIES="task" ;;
darwin/armv5) BINARIES="task" ;;
darwin/armv6) BINARIES="task" ;;
darwin/armv7) BINARIES="task" ;;
darwin/arm) BINARIES="task" ;;
linux/386) BINARIES="task" ;;
linux/amd64) BINARIES="task" ;;
linux/arm64) BINARIES="task" ;;
linux/armv5) BINARIES="task" ;;
linux/armv6) BINARIES="task" ;;
linux/armv7) BINARIES="task" ;;
linux/arm) BINARIES="task" ;;
windows/386) BINARIES="task" ;;
windows/amd64) BINARIES="task" ;;
windows/arm64) BINARIES="task" ;;
windows/armv5) BINARIES="task" ;;
windows/armv6) BINARIES="task" ;;
windows/armv7) BINARIES="task" ;;
windows/arm) BINARIES="task" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1

View File

@@ -9,6 +9,7 @@ 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"`

View File

@@ -106,25 +106,17 @@ func ExpandLiteral(s string) (string, error) {
if s == "" {
return "", nil
}
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
word, err := p.Document(strings.NewReader(s))
if err != nil {
return "", err
}
if len(words) == 0 {
return "", nil
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
}
return expand.Literal(cfg, words[0])
return expand.Literal(cfg, word)
}
// ExpandFields is a wrapper around [expand.Fields]. It will escape the input
@@ -146,6 +138,7 @@ func ExpandFields(s string) ([]string, error) {
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
NullGlob: true,
}
return expand.Fields(cfg, words...)
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -201,7 +202,7 @@ func Validate() error {
// WithFlags is a special internal functional option that is used to pass flags
// from the CLI into any constructor that accepts functional options.
func WithFlags() task.ExecutorOption {
func WithFlags() *flagsOption {
return &flagsOption{}
}
@@ -228,14 +229,8 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
e.Options(
task.WithDir(dir),
task.WithEntrypoint(Entrypoint),
task.WithForce(Force),
task.WithForceAll(ForceAll),
task.WithInsecure(Insecure),
task.WithDownload(Download),
task.WithOffline(Offline),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithWatch(Watch),
task.WithVerbose(Verbose),
task.WithSilent(Silent),
@@ -251,3 +246,12 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithVersionCheck(true),
)
}
func (o *flagsOption) ApplyToReader(r *taskfile.Reader) {
r.Options(
taskfile.WithInsecure(Insecure),
taskfile.WithDownload(Download),
taskfile.WithOffline(Offline),
taskfile.WithCacheExpiryDuration(CacheExpiryDuration),
)
}

View File

@@ -2,7 +2,6 @@ package fsnotifyext
import (
"math"
"sync"
"time"
"github.com/fsnotify/fsnotify"
@@ -11,7 +10,6 @@ import (
type Deduper struct {
w *fsnotify.Watcher
waitTime time.Duration
mutex sync.Mutex
}
func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
@@ -21,31 +19,28 @@ func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
}
}
func (d *Deduper) GetChan() chan fsnotify.Event {
// GetChan returns a chan of deduplicated [fsnotify.Event].
//
// [fsnotify.Chmod] operations will be skipped.
func (d *Deduper) GetChan() <-chan fsnotify.Event {
channel := make(chan fsnotify.Event)
timers := make(map[string]*time.Timer)
go func() {
timers := make(map[string]*time.Timer)
for {
event, ok := <-d.w.Events
switch {
case !ok:
return
case event.Op == fsnotify.Chmod:
case event.Has(fsnotify.Chmod):
continue
}
d.mutex.Lock()
timer, ok := timers[event.String()]
d.mutex.Unlock()
if !ok {
timer = time.AfterFunc(math.MaxInt64, func() { channel <- event })
timer.Stop()
d.mutex.Lock()
timers[event.String()] = timer
d.mutex.Unlock()
}
timer.Reset(d.waitTime)

View File

@@ -2,11 +2,14 @@ package templater
import (
"maps"
"math/rand/v2"
"path/filepath"
"runtime"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
"mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax"
@@ -18,58 +21,27 @@ var templateFuncs template.FuncMap
func init() {
taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
"numCPU": func() int { return runtime.NumCPU() },
"catLines": func(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
},
"splitLines": func(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.Split(s, "\n")
},
"fromSlash": func(path string) string {
return filepath.FromSlash(path)
},
"toSlash": func(path string) string {
return filepath.ToSlash(path)
},
"exeExt": func() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
},
"shellQuote": func(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
},
"splitArgs": func(s string) ([]string, error) {
return shell.Fields(s, nil)
},
// IsSH is deprecated.
"IsSH": func() bool { return true },
"joinPath": func(elem ...string) string {
return filepath.Join(elem...)
},
"relPath": func(basePath, targetPath string) (string, error) {
return filepath.Rel(basePath, targetPath)
},
"merge": func(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
maps.Copy(result, base)
for _, m := range v {
maps.Copy(result, m)
}
return result
},
"spew": func(v any) string {
return spew.Sdump(v)
},
"OS": os,
"ARCH": arch,
"numCPU": runtime.NumCPU,
"catLines": catLines,
"splitLines": splitLines,
"fromSlash": filepath.FromSlash,
"toSlash": filepath.ToSlash,
"exeExt": exeExt,
"shellQuote": shellQuote,
"splitArgs": splitArgs,
"IsSH": IsSH, // Deprecated
"joinPath": filepath.Join,
"relPath": filepath.Rel,
"merge": merge,
"spew": spew.Sdump,
"fromYaml": fromYaml,
"mustFromYaml": mustFromYaml,
"toYaml": toYaml,
"mustToYaml": mustToYaml,
"uuid": uuid.New,
"randIntN": rand.IntN,
}
// aliases
@@ -83,3 +55,78 @@ func init() {
templateFuncs = template.FuncMap(sprig.TxtFuncMap())
maps.Copy(templateFuncs, taskFuncs)
}
func os() string {
return runtime.GOOS
}
func arch() string {
return runtime.GOARCH
}
func catLines(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
}
func splitLines(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.Split(s, "\n")
}
func exeExt() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
}
func shellQuote(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
}
func splitArgs(s string) ([]string, error) {
return shell.Fields(s, nil)
}
// Deprecated: now always returns true
func IsSH() bool {
return true
}
func merge(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
maps.Copy(result, base)
for _, m := range v {
maps.Copy(result, m)
}
return result
}
func fromYaml(v string) any {
output, _ := mustFromYaml(v)
return output
}
func mustFromYaml(v string) (any, error) {
var output any
err := yaml.Unmarshal([]byte(v), &output)
return output, err
}
func toYaml(v any) string {
output, _ := yaml.Marshal(v)
return string(output)
}
func mustToYaml(v any) (string, error) {
output, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(output), nil
}

View File

@@ -6,9 +6,10 @@ import (
"maps"
"strings"
"github.com/go-task/template"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/template"
)
// Cache is a help struct that allow us to call "replaceX" funcs multiple

View File

@@ -1 +1 @@
3.43.2
3.44.1

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.43.2",
"version": "3.44.1",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.43.2",
"version": "3.44.1",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"postinstall": "go-npm install",

15
task.go
View File

@@ -8,6 +8,9 @@ import (
"slices"
"sync/atomic"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
@@ -19,9 +22,6 @@ import (
"github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
)
const (
@@ -220,13 +220,13 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
}
exitCode, isExitError := interp.IsExitStatus(err)
if isExitError {
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) {
if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue
}
deferredExitCode = exitCode
deferredExitCode = uint8(exitCode)
}
if call.Indirect {
@@ -356,7 +356,8 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
}
if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,7 @@ type (
AdvancedImport bool
Vars *Vars
Flatten bool
Checksum string
}
// Includes is an ordered map of namespaces to includes.
Includes struct {
@@ -165,6 +166,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Aliases []string
Excludes []string
Vars *Vars
Checksum string
}
if err := node.Decode(&includedTaskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -178,6 +180,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.AdvancedImport = true
include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten
include.Checksum = includedTaskfile.Checksum
return nil
}
@@ -200,5 +203,7 @@ func (include *Include) DeepCopy() *Include {
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,
Aliases: deepcopy.Slice(include.Aliases),
Checksum: include.Checksum,
}
}

View File

@@ -3,7 +3,6 @@ package taskfile
import (
"context"
"strings"
"time"
giturls "github.com/chainguard-dev/git-urls"
@@ -17,6 +16,8 @@ type Node interface {
Parent() Node
Location() string
Dir() string
Checksum() string
Verify(checksum string) bool
ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error)
}
@@ -31,7 +32,6 @@ func NewRootNode(
entrypoint string,
dir string,
insecure bool,
timeout time.Duration,
) (Node, error) {
dir = fsext.DefaultDir(entrypoint, dir)
// If the entrypoint is "-", we read from stdin

View File

@@ -1,19 +1,20 @@
package taskfile
type (
NodeOption func(*BaseNode)
// BaseNode is a generic node that implements the Parent() methods of the
NodeOption func(*baseNode)
// baseNode is a generic node that implements the Parent() methods of the
// NodeReader interface. It does not implement the Read() method and it
// designed to be embedded in other node types so that this boilerplate code
// does not need to be repeated.
BaseNode struct {
baseNode struct {
parent Node
dir string
checksum string
}
)
func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
node := &BaseNode{
func NewBaseNode(dir string, opts ...NodeOption) *baseNode {
node := &baseNode{
parent: nil,
dir: dir,
}
@@ -27,15 +28,29 @@ func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
}
func WithParent(parent Node) NodeOption {
return func(node *BaseNode) {
return func(node *baseNode) {
node.parent = parent
}
}
func (node *BaseNode) Parent() Node {
func WithChecksum(checksum string) NodeOption {
return func(node *baseNode) {
node.checksum = checksum
}
}
func (node *baseNode) Parent() Node {
return node.parent
}
func (node *BaseNode) Dir() string {
func (node *baseNode) Dir() string {
return node.dir
}
func (node *baseNode) Checksum() string {
return node.checksum
}
func (node *baseNode) Verify(checksum string) bool {
return node.checksum == "" || node.checksum == checksum
}

View File

@@ -11,13 +11,13 @@ import (
const remoteCacheDir = "remote"
type CacheNode struct {
*BaseNode
*baseNode
source RemoteNode
}
func NewCacheNode(source RemoteNode, dir string) *CacheNode {
return &CacheNode{
BaseNode: &BaseNode{
baseNode: &baseNode{
dir: filepath.Join(dir, remoteCacheDir),
},
source: source,

View File

@@ -13,8 +13,8 @@ import (
// A FileNode is a node that reads a taskfile from the local filesystem.
type FileNode struct {
*BaseNode
Entrypoint string
*baseNode
entrypoint string
}
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
@@ -25,13 +25,13 @@ func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error)
return nil, err
}
return &FileNode{
BaseNode: base,
Entrypoint: entrypoint,
baseNode: base,
entrypoint: entrypoint,
}, nil
}
func (node *FileNode) Location() string {
return node.Entrypoint
return node.entrypoint
}
func (node *FileNode) Read() ([]byte, error) {
@@ -63,7 +63,7 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Entrypoint)
entrypointDir := filepath.Dir(node.entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil
}
@@ -79,6 +79,6 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Entrypoint)
entrypointDir := filepath.Dir(node.entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil
}

View File

@@ -21,8 +21,8 @@ import (
// An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct {
*BaseNode
URL *url.URL
*baseNode
url *url.URL
rawUrl string
ref string
path string
@@ -40,23 +40,20 @@ func NewGitNode(
return nil, err
}
basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
basePath, path := splitURLOnDoubleSlash(u)
ref := u.Query().Get("ref")
rawUrl := u.String()
rawUrl := u.Redacted()
u.RawQuery = ""
u.Path = basePath
if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
return nil, &errors.TaskfileNotSecureError{URI: u.Redacted()}
}
return &GitNode{
BaseNode: base,
URL: u,
baseNode: base,
url: u,
rawUrl: rawUrl,
ref: ref,
path: path,
@@ -79,7 +76,7 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(),
URL: node.url.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
@@ -102,7 +99,7 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
@@ -127,11 +124,23 @@ func (node *GitNode) ResolveDir(dir string) (string, error) {
func (node *GitNode) CacheKey() string {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=")
prefix := filepath.Base(filepath.Dir(node.path))
lastDir := filepath.Base(node.path)
lastDir := filepath.Base(filepath.Dir(node.path))
prefix := filepath.Base(node.path)
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s-%s", lastDir, prefix)
prefix = fmt.Sprintf("%s.%s", lastDir, prefix)
}
return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum)
}
func splitURLOnDoubleSlash(u *url.URL) (string, string) {
x := strings.Split(u.Path, "//")
switch len(x) {
case 0:
return "", ""
case 1:
return x[0], ""
default:
return x[0], x[1]
}
return fmt.Sprintf("%s.%s", prefix, checksum)
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGitNode_ssh(t *testing.T) {
@@ -13,8 +14,8 @@ func TestGitNode_ssh(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
@@ -27,8 +28,8 @@ func TestGitNode_sshWithDir(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
@@ -41,8 +42,8 @@ func TestGitNode_https(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "https://github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
@@ -55,8 +56,8 @@ func TestGitNode_httpsWithDir(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "https://github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
@@ -65,18 +66,28 @@ func TestGitNode_httpsWithDir(t *testing.T) {
func TestGitNode_CacheKey(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, "Taskfile.yml-directory.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb", key)
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
key = node.CacheKey()
assert.Equal(t, "Taskfile.yml-..39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4", key)
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
key = node.CacheKey()
assert.Equal(t, "Taskfile.yml-directory.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31", key)
tests := []struct {
entrypoint string
expectedKey string
}{
{
entrypoint: "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main",
expectedKey: "git.github.com.directory.Taskfile.yml.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb",
},
{
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
expectedKey: "git.github.com.Taskfile.yml.39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4",
},
{
entrypoint: "https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main",
expectedKey: "git.github.com.directory.Taskfile.yml.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31",
},
}
for _, tt := range tests {
node, err := NewGitNode(tt.entrypoint, "", false)
require.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, tt.expectedKey, key)
}
}

View File

@@ -16,9 +16,8 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*BaseNode
URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
entrypoint string // stores entrypoint url. used for building graph vertices.
*baseNode
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
}
func NewHTTPNode(
@@ -33,18 +32,16 @@ func NewHTTPNode(
return nil, err
}
if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}
}
return &HTTPNode{
BaseNode: base,
URL: url,
entrypoint: entrypoint,
baseNode: base,
url: url,
}, nil
}
func (node *HTTPNode) Location() string {
return node.entrypoint
return node.url.Redacted()
}
func (node *HTTPNode) Read() ([]byte, error) {
@@ -52,14 +49,13 @@ func (node *HTTPNode) Read() ([]byte, error) {
}
func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.URL)
url, err := RemoteExists(ctx, *node.url)
if err != nil {
return nil, err
}
node.URL = url
req, err := http.NewRequest("GET", node.URL.String(), nil)
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
@@ -67,12 +63,12 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
if ctx.Err() != nil {
return nil, err
}
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.TaskfileFetchFailedError{
URI: node.URL.String(),
URI: node.Location(),
HTTPStatusCode: resp.StatusCode,
}
}
@@ -91,7 +87,7 @@ func (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) {
if err != nil {
return "", err
}
return node.URL.ResolveReference(ref).String(), nil
return node.url.ResolveReference(ref).String(), nil
}
func (node *HTTPNode) ResolveDir(dir string) (string, error) {
@@ -116,12 +112,12 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
func (node *HTTPNode) CacheKey() string {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=")
dir, filename := filepath.Split(node.entrypoint)
dir, filename := filepath.Split(node.url.Path)
lastDir := filepath.Base(dir)
prefix := filename
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s-%s", lastDir, filename)
prefix = fmt.Sprintf("%s.%s", lastDir, filename)
}
return fmt.Sprintf("%s.%s", prefix, checksum)
return fmt.Sprintf("http.%s.%s.%s", node.url.Host, prefix, checksum)
}

View File

@@ -0,0 +1,49 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTPNode_CacheKey(t *testing.T) {
t.Parallel()
tests := []struct {
entrypoint string
expectedKey string
}{
{
entrypoint: "https://github.com",
expectedKey: "http.github.com..996e1f714b08e971ec79e3bea686287e66441f043177999a13dbc546d8fe402a",
},
{
entrypoint: "https://github.com/Taskfile.yml",
expectedKey: "http.github.com.Taskfile.yml.85b3c3ad71b78dc74e404c7b4390fc13672925cb644a4d26c21b9f97c17b5fc0",
},
{
entrypoint: "https://github.com/foo",
expectedKey: "http.github.com.foo.df3158dafc823e6847d9bcaf79328446c4877405e79b100723fa6fd545ed3e2b",
},
{
entrypoint: "https://github.com/foo/Taskfile.yml",
expectedKey: "http.github.com.foo.Taskfile.yml.aea946ea7eb6f6bb4e159e8b840b6b50975927778b2e666df988c03bbf10c4c4",
},
{
entrypoint: "https://github.com/foo/bar",
expectedKey: "http.github.com.foo.bar.d3514ad1d4daedf9cc2825225070b49ebc8db47fa5177951b2a5b9994597570c",
},
{
entrypoint: "https://github.com/foo/bar/Taskfile.yml",
expectedKey: "http.github.com.bar.Taskfile.yml.b9cf01e01e47c0e96ea536e1a8bd7b3a6f6c1f1881bad438990d2bfd4ccd0ac0",
},
}
for _, tt := range tests {
node, err := NewHTTPNode(tt.entrypoint, "", false)
require.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, tt.expectedKey, key)
}
}

View File

@@ -12,12 +12,12 @@ import (
// A StdinNode is a node that reads a taskfile from the standard input stream.
type StdinNode struct {
*BaseNode
*baseNode
}
func NewStdinNode(dir string) (*StdinNode, error) {
return &StdinNode{
BaseNode: NewBaseNode(dir),
baseNode: NewBaseNode(dir),
}, nil
}

View File

@@ -249,7 +249,8 @@ func (r *Reader) include(ctx context.Context, node Node) error {
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Excludes: include.Excludes,
Vars: templater.ReplaceVars(include.Vars, cache),
Vars: include.Vars,
Checksum: include.Checksum,
}
if err := cache.Err(); err != nil {
return err
@@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
WithParent(node),
WithChecksum(include.Checksum),
)
if err != nil {
if include.Optional {
@@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
if node, isRemote := node.(RemoteNode); isRemote {
return r.readRemoteNodeContent(ctx, node)
}
return node.Read()
// Read the Taskfile
b, err := node.Read()
if err != nil {
return nil, err
}
// If the given checksum doesn't match the sum pinned in the Taskfile
checksum := checksum(b)
if !node.Verify(checksum) {
return nil, &errors.TaskfileDoesNotMatchChecksum{
URI: node.Location(),
ExpectedChecksum: node.Checksum(),
ActualChecksum: checksum,
}
}
return b, nil
}
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
@@ -427,10 +446,21 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
}
r.debugf("found remote file at %q\n", node.Location())
checksum := checksum(downloadedBytes)
prompt := cache.ChecksumPrompt(checksum)
// If the given checksum doesn't match the sum pinned in the Taskfile
checksum := checksum(downloadedBytes)
if !node.Verify(checksum) {
return nil, &errors.TaskfileDoesNotMatchChecksum{
URI: node.Location(),
ExpectedChecksum: node.Checksum(),
ActualChecksum: checksum,
}
}
// If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" {
// Prompt the user if required
prompt := cache.ChecksumPrompt(checksum)
if prompt != "" {
if err := func() error {
r.promptMutex.Lock()
@@ -440,6 +470,7 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
}
}
}
// Store the checksum
if err := cache.WriteChecksum(checksum); err != nil {

View File

@@ -36,11 +36,11 @@ var (
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
// Request the given URL
@@ -49,7 +49,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
if ctx.Err() != nil {
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
}
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
defer resp.Body.Close()
@@ -61,7 +61,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
return strings.Contains(contentType, s)
}) {
return u, nil
return &u, nil
}
// If the request was not successful, append the default Taskfile names to
@@ -78,7 +78,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
// Try the alternative URL
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
defer resp.Body.Close()
@@ -88,5 +88,5 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
}
}
return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
return nil, errors.TaskfileNotFoundError{URI: u.Redacted(), Walk: false}
}

78
temp_dir.go Normal file
View File

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

View File

@@ -0,0 +1 @@
task: Missing schema version in Taskfile "{{.TEST_DIR}}/testdata/empty_taskfile/Taskfile.yml"

View File

@@ -1 +0,0 @@
task: Missing schema version in Taskfile "/testdata/empty_taskfile/Taskfile.yml"

View File

@@ -1,2 +1,2 @@
task: Failed to parse /testdata/for/cmds/Taskfile.yml:
task: Failed to parse {{.TEST_DIR}}/testdata/for/cmds/Taskfile.yml:
matrix reference ".NOT_A_LIST" must resolve to a list

View File

@@ -1,2 +1,2 @@
matrix reference ".NOT_A_LIST" must resolve to a list
task: Failed to parse /testdata/for/deps/Taskfile.yml:
task: Failed to parse {{.TEST_DIR}}/testdata/for/deps/Taskfile.yml:

9
testdata/fuzzy/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 3
tasks:
install: echo 'install'
internal:
internal: true
cmds:
- echo "internal"

View File

@@ -0,0 +1 @@
task: Task "instal" does not exist. Did you mean "install"?

View File

@@ -0,0 +1 @@
task: No tasks with description available. Try --list-all to list all tasks

View File

@@ -0,0 +1 @@
task: Task "intern" does not exist

View File

@@ -0,0 +1 @@
task: No tasks with description available. Try --list-all to list all tasks

View File

@@ -0,0 +1,2 @@
task: [install] echo 'install'
install

View File

@@ -1,23 +1,16 @@
version: "3"
vars:
VAR_1: included4-var1
includes:
included1:
taskfile: include/Taskfile.include.yml
taskfile: include/Taskfile.include1.yml
vars:
VAR_1: included1-var1
included2:
taskfile: include/Taskfile.include.yml
taskfile: include/Taskfile.include2.yml
vars:
VAR_1: included2-var1
included3:
taskfile: include/Taskfile.include.yml
included4:
taskfile: include/Taskfile.include.yml
vars:
VAR_1: "{{.VAR_1}}"
taskfile: include/Taskfile.include3.yml
tasks:
task1:
@@ -25,4 +18,3 @@ tasks:
- task: included1:task1
- task: included2:task1
- task: included3:task1
- task: included4:task1

View File

@@ -0,0 +1,11 @@
version: "3"
vars:
VAR_1: '{{.VAR_1 | default "included-default-var1"}}'
VAR_2: '{{.VAR_2 | default "included-default-var2"}}'
tasks:
task1:
cmds:
- echo "VAR_1 is {{.VAR_1}}"
- echo "VAR_2 is {{.VAR_2}}"

View File

@@ -0,0 +1,11 @@
version: "3"
vars:
VAR_1: '{{.VAR_1 | default "included-default-var1"}}'
VAR_2: '{{.VAR_2 | default "included-default-var2"}}'
tasks:
task1:
cmds:
- echo "VAR_1 is {{.VAR_1}}"
- echo "VAR_2 is {{.VAR_2}}"

View File

@@ -0,0 +1,12 @@
version: '3'
includes:
included:
taskfile: ../included.yml
internal: true
checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5
tasks:
default:
cmds:
- task: included:default

View File

@@ -0,0 +1,2 @@
task: [included:default] echo "Hello, World!"
Hello, World!

View File

@@ -0,0 +1,12 @@
version: '3'
includes:
included:
taskfile: https://taskfile.dev
internal: true
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
tasks:
default:
cmds:
- task: included:default

View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
default:
cmds:
- echo "Hello, World!"

View File

@@ -0,0 +1,12 @@
version: '3'
includes:
included:
taskfile: ../included.yml
internal: true
checksum: foo
tasks:
default:
cmds:
- task: included:default

View File

@@ -0,0 +1,3 @@
task: The checksum of the Taskfile at "{{.TEST_DIR}}/testdata/includes_checksum/included.yml" does not match!
got: "c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5"
want: "foo"

View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
foo:
label: "foobar"
desc: "task description"

View File

@@ -0,0 +1,18 @@
{
"tasks": [
{
"name": "foobar",
"task": "foo",
"desc": "task description",
"summary": "",
"aliases": [],
"up_to_date": false,
"location": {
"line": 4,
"column": 3,
"taskfile": "{{.TEST_DIR}}/testdata/json_list_format/Taskfile.yml"
}
}
],
"location": "{{.TEST_DIR}}/testdata/json_list_format/Taskfile.yml"
}

View File

@@ -1 +0,0 @@
/testdata/special_vars/included/Taskfile.yml

View File

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

View File

@@ -1 +1 @@
/testdata/special_vars
{{.TEST_DIR}}/testdata/special_vars

View File

@@ -1 +1 @@
/testdata/special_vars/included
{{.TEST_DIR}}/testdata/special_vars/included

View File

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

View File

@@ -1 +1 @@
/testdata/special_vars
{{.TEST_DIR}}/testdata/special_vars

View File

@@ -1 +1 @@
/testdata/special_vars/foo
{{.TEST_DIR}}/testdata/special_vars/foo

View File

@@ -1 +1 @@
/testdata/special_vars
{{.TEST_DIR}}/testdata/special_vars

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,107 +2,113 @@ version: '3'
tasks:
default:
- task: dynamic
- task: string
- task: bool
- task: int
- task: string-array
- task: map
- task: for-string
- task: for-int
- task: for-map
- task: for-multi-layer-map
dynamic:
vars:
STRING_A: '$echo "A"'
STRING_B: '$echo {{.STRING_A}}B'
STRING_C: '$echo {{.STRING_B}}C'
cmds:
- echo '{{.STRING_C}}'
string:
vars:
STRING_A: 'A'
STRING_B: '{{.STRING_A}}B'
STRING_C: '{{.STRING_B}}C'
cmds:
- echo '{{.STRING_C}}'
bool:
vars:
BOOL_TRUE: true
BOOL_FALSE: false
BOOL_A: '{{and .BOOL_TRUE .BOOL_FALSE}}'
BOOL_B: '{{or .BOOL_TRUE .BOOL_FALSE}}'
BOOL_C: '{{not .BOOL_TRUE}}'
cmds:
- echo '{{if .BOOL_TRUE}}A:{{.BOOL_A}} B:{{.BOOL_B}} C:{{.BOOL_C}}{{end}}'
int:
vars:
INT_100: 100
INT_10: 10
cmds:
- echo '100 + 10 = {{add .INT_100 .INT_10}}'
- echo '100 - 10 = {{sub .INT_100 .INT_10}}'
- echo '100 * 10 = {{mul .INT_100 .INT_10}}'
- echo '100 / 10 = {{div .INT_100 .INT_10}}'
string-array:
vars:
ARRAY_1: ['A', 'B', 'C']
ARRAY_2: ['D', 'E', 'F']
cmds:
- echo '{{append .ARRAY_1 "D"}}'
- echo '{{concat .ARRAY_1 .ARRAY_2}}'
- echo '{{join " " .ARRAY_1}}'
- task: nested-map
- task: slice
- task: ref
- task: ref-sh
- task: ref-dep
- task: ref-resolver
- task: json
map:
vars:
MAP_1: {A: 1, B: 2, C: 3}
MAP_2: {B: 4, C: 5, D: 6}
MAP_3: {C: 7, D: 8, E: 9}
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
cmds:
- echo '{{merge .MAP_1 .MAP_2 .MAP_3}}'
for-string:
- task: print-story
vars:
LIST: [foo, bar, baz]
cmds:
- for:
var: LIST
cmd: echo {{.ITEM}}
VAR:
ref: .MAP
for-int:
nested-map:
vars:
LIST: [1, 2, 3]
FOO: "foo"
nested:
map:
variables:
work: "{{.FOO}}"
cmds:
- for:
var: LIST
cmd: echo {{add .ITEM 100}}
- echo {{.nested.variables.work}}
for-map:
slice:
vars:
FOO: "foo"
BAR: "bar"
slice_variables_work: ["{{.FOO}}","{{.BAR}}"]
cmds:
- echo {{index .slice_variables_work 0}} {{index .slice_variables_work 1}}
ref:
vars:
MAP:
KEY_1: value_1
KEY_2: value_2
KEY_3: value_3
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF:
ref: .MAP
cmds:
- for:
var: MAP
cmd: echo {{.KEY}} {{.ITEM}}
- task: print-story
vars:
VAR:
ref: .MAP_REF
for-multi-layer-map:
ref-sh:
vars:
JSON_STRING:
sh: echo '{"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}'
JSON: "fromJson {{.JSON_STRING}}"
MAP_REF:
ref: .JSON
cmds:
- task: print-story
vars:
VAR:
ref: .MAP_REF
ref-dep:
vars:
MAP:
KEY_1:
SUBKEY: sub_value_1
KEY_2:
SUBKEY: sub_value_2
KEY_3:
SUBKEY: sub_value_3
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
deps:
- task: print-story
vars:
VAR:
ref: .MAP
ref-resolver:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF:
ref: .MAP
cmds:
- for:
var: MAP
cmd: echo {{.KEY}} {{.ITEM.SUBKEY}}
- task: print-var
vars:
VAR:
ref: (index .MAP_REF.children 0).name
json:
vars:
JSON_STRING:
sh: cat example.json
JSON:
ref: "fromJson .JSON_STRING"
cmds:
- task: print-story
vars:
VAR:
ref: .JSON
print-var:
cmds:
- echo "{{.VAR}}"
print-story:
cmds:
- >-
echo "{{.VAR.name}} has {{len .VAR.children}} children called
{{- $children := .VAR.children -}}
{{- range $i, $child := $children -}}
{{- if lt $i (sub (len $children) 1)}} {{$child.name -}},
{{- else}} and {{$child.name -}}
{{- end -}}
{{- end -}}"

View File

@@ -1,115 +0,0 @@
version: '3'
tasks:
default:
- task: map
- task: nested-map
- task: slice
- task: ref
- task: ref-sh
- task: ref-dep
- task: ref-resolver
- task: json
map:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
cmds:
- task: print-story
vars:
VAR:
ref: .MAP
nested-map:
vars:
FOO: "foo"
nested:
map:
variables:
work: "{{.FOO}}"
cmds:
- echo {{.nested.variables.work}}
slice:
vars:
FOO: "foo"
BAR: "bar"
slice_variables_work: ["{{.FOO}}","{{.BAR}}"]
cmds:
- echo {{index .slice_variables_work 0}} {{index .slice_variables_work 1}}
ref:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF:
ref: .MAP
cmds:
- task: print-story
vars:
VAR:
ref: .MAP_REF
ref-sh:
vars:
JSON_STRING:
sh: echo '{"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}'
JSON:
json: "{{.JSON_STRING}}"
MAP_REF:
ref: .JSON
cmds:
- task: print-story
vars:
VAR:
ref: .MAP_REF
ref-dep:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
deps:
- task: print-story
vars:
VAR:
ref: .MAP
ref-resolver:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF:
ref: .MAP
cmds:
- task: print-var
vars:
VAR:
ref: (index .MAP_REF.children 0).name
json:
vars:
JSON_STRING:
sh: cat example.json
JSON:
ref: "fromJson .JSON_STRING"
cmds:
- task: print-story
vars:
VAR:
ref: .JSON
print-var:
cmds:
- echo "{{.VAR}}"
print-story:
cmds:
- >-
echo "{{.VAR.name}} has {{len .VAR.children}} children called
{{- $children := .VAR.children -}}
{{- range $i, $child := $children -}}
{{- if lt $i (sub (len $children) 1)}} {{$child.name -}},
{{- else}} and {{$child.name -}}
{{- end -}}
{{- end -}}"

View File

@@ -0,0 +1 @@
Hello foo

View File

@@ -0,0 +1 @@
Hello foo bar

View File

@@ -0,0 +1 @@
I don't consume matches: []

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