Compare commits

...

73 Commits

Author SHA1 Message Date
Pete Davison
3c30a8066d feat: bump minor version when repo is dirty or untagged 2025-08-06 19:45:37 +00: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
109 changed files with 3904 additions and 2572 deletions

View File

@@ -23,9 +23,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v7 uses: golangci/golangci-lint-action@v8
with: with:
version: v2.0.2 version: v2.1.0
lint-jsonschema: lint-jsonschema:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -56,3 +56,19 @@ jobs:
with: with:
script: | script: |
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.') 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
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}

View File

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

View File

@@ -36,7 +36,7 @@ gomod:
proxy: true proxy: true
archives: archives:
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}" - name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
files: files:
- README.md - README.md
- LICENSE - LICENSE
@@ -48,24 +48,35 @@ archives:
release: release:
draft: true draft: true
git:
ignore_tags:
- "{{if not .IsNightly}}nightly{{end}}"
nightly:
publish_release: true
keep_single_release: true
version_template: "{{incminor .Version}}-nightly"
snapshot: snapshot:
version_template: "{{.Version}}" version_template: '{{.Version}}'
checksum: checksum:
name_template: "task_checksums.txt" name_template: 'task_checksums.txt'
nfpms: nfpms:
- vendor: Task - vendor: Task
homepage: https://taskfile.dev homepage: https://taskfile.dev
maintainer: The Task authors <task@taskfile.dev> maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go description: Simple task runner written in Go
section: golang
license: MIT license: MIT
conflicts: conflicts:
- taskwarrior - taskwarrior
formats: formats:
- deb - deb
- rpm - rpm
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
contents: contents:
- src: completion/bash/task.bash - src: completion/bash/task.bash
dst: /etc/bash_completion.d/task dst: /etc/bash_completion.d/task
@@ -83,8 +94,7 @@ brews:
repository: repository:
owner: go-task owner: go-task
name: homebrew-tap name: homebrew-tap
test: test: system "#{bin}/task", "--help"
system "#{bin}/task", "--help"
install: |- install: |-
bin.install "task" bin.install "task"
bash_completion.install "completion/bash/task.bash" => "task" bash_completion.install "completion/bash/task.bash" => "task"
@@ -107,7 +117,7 @@ winget:
commit_author: commit_author:
name: task-bot name: task-bot
email: 106601941+task-bot@users.noreply.github.com 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}} release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}}
tags: tags:
- build - build
@@ -121,13 +131,15 @@ winget:
- task-runner - task-runner
- taskfile - taskfile
- tool - tool
skip_upload: true
repository: repository:
owner: microsoft owner: go-task
name: winget-pkgs name: winget-pkgs
branch: 'chore/task-{{.Version}}'
pull_request: pull_request:
enabled: true enabled: true
draft: false
check_boxes: true
base: base:
owner: go-task owner: microsoft
name: winget-pkgs name: winget-pkgs
branch: "bump-task-to-{{.Tag}}" branch: master

2
.nvmrc
View File

@@ -1 +1 @@
22.14.0 22.18.0

View File

@@ -1,5 +1,51 @@
# Changelog # 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 ## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by - Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by

View File

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

View File

@@ -13,24 +13,14 @@ import (
// Get fetches the remaining arguments after CLI parsing and splits them into // Get fetches the remaining arguments after CLI parsing and splits them into
// two groups: the arguments before the double dash (--) and the arguments after // two groups: the arguments before the double dash (--) and the arguments after
// the double dash. // the double dash.
func Get() ([]string, string, error) { func Get() ([]string, []string, error) {
args := pflag.Args() args := pflag.Args()
doubleDashPos := pflag.CommandLine.ArgsLenAtDash() doubleDashPos := pflag.CommandLine.ArgsLenAtDash()
if doubleDashPos == -1 { if doubleDashPos == -1 {
return args, "", nil return args, nil, nil
} }
return args[:doubleDashPos], args[doubleDashPos:], 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
} }
// Parse parses command line argument: tasks and global variables // Parse parses command line argument: tasks and global variables
@@ -51,6 +41,18 @@ func Parse(args ...string) ([]*task.Call, *ast.Vars) {
return calls, globals 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) { func splitVar(s string) (string, string) {
pair := strings.SplitN(s, "=", 2) pair := strings.SplitN(s, "=", 2)
return pair[0], pair[1] return pair[0], pair[1]

View File

@@ -16,10 +16,14 @@ import (
) )
const ( const (
changelogSource = "CHANGELOG.md" changelogSource = "CHANGELOG.md"
changelogTarget = "website/docs/changelog.mdx" changelogTarget = "website/docs/changelog.mdx"
docsSource = "website/docs" docsSource = "website/docs"
docsTarget = "website/versioned_docs/version-latest" 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 ( var (
@@ -83,6 +87,10 @@ func release() error {
return err return err
} }
if err := schema(); err != nil {
return err
}
return nil return nil
} }
@@ -175,3 +183,13 @@ func docs() error {
} }
return nil 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

@@ -144,18 +144,23 @@ func run() error {
} }
// Parse the remaining arguments // Parse the remaining arguments
argv, cliArgs, err := args.Get() cliArgsPreDash, cliArgsPostDash, err := args.Get()
if err != nil { if err != nil {
return err return err
} }
calls, globals := args.Parse(argv...) calls, globals := args.Parse(cliArgsPreDash...)
// If there are no calls, run the default task instead // If there are no calls, run the default task instead
if len(calls) == 0 { if len(calls) == 0 {
calls = append(calls, &task.Call{Task: "default"}) 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_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent}) globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose}) globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})

View File

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

View File

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

View File

@@ -187,3 +187,24 @@ func (err TaskfileCycleError) Error() string {
func (err TaskfileCycleError) Code() int { func (err TaskfileCycleError) Code() int {
return CodeTaskfileCycle 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

@@ -4,7 +4,6 @@ import (
"context" "context"
"io" "io"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "time"
@@ -123,7 +122,6 @@ type dirOption struct {
} }
func (o *dirOption) ApplyToExecutor(e *Executor) { func (o *dirOption) ApplyToExecutor(e *Executor) {
e.UserWorkingDir, _ = filepath.Abs(o.dir)
e.Dir = o.dir e.Dir = o.dir
} }

View File

@@ -50,7 +50,8 @@ func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
task: "default", task: "default",
vars: map[string]any{}, vars: map[string]any{},
TaskTest: TaskTest{ TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{}, experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
}, },
} }
// Apply the functional options // Apply the functional options
@@ -232,7 +233,7 @@ func TestEmptyTaskfile(t *testing.T) {
task.WithDir("testdata/empty_taskfile"), task.WithDir("testdata/empty_taskfile"),
), ),
WithSetupError(), WithSetupError(),
WithPostProcessFn(PPRemoveAbsolutePaths), WithFixtureTemplating(),
) )
} }
@@ -367,7 +368,7 @@ func TestSpecialVars(t *testing.T) {
task.WithVersionCheck(true), task.WithVersionCheck(true),
), ),
WithTask(test), WithTask(test),
WithPostProcessFn(PPRemoveAbsolutePaths), WithFixtureTemplating(),
) )
} }
} }
@@ -551,7 +552,7 @@ func TestStatus(t *testing.T) {
task.WithVerbose(true), task.WithVerbose(true),
), ),
WithTask("gen-silent-baz"), WithTask("gen-silent-baz"),
WithPostProcessFn(PPRemoveAbsolutePaths), WithFixtureTemplating(),
) )
} }
@@ -777,7 +778,7 @@ func TestForCmds(t *testing.T) {
task.WithForce(true), task.WithForce(true),
), ),
WithTask(test.name), WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths), WithFixtureTemplating(),
} }
if test.wantErr { if test.wantErr {
opts = append(opts, WithRunError()) opts = append(opts, WithRunError())
@@ -822,7 +823,7 @@ func TestForDeps(t *testing.T) {
task.WithOutputStyle(ast.Output{Name: "group"}), task.WithOutputStyle(ast.Output{Name: "group"}),
), ),
WithTask(test.name), WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths), WithFixtureTemplating(),
WithPostProcessFn(PPSortedLines), WithPostProcessFn(PPSortedLines),
} }
if test.wantErr { if test.wantErr {
@@ -937,3 +938,53 @@ func TestVarInheritance(t *testing.T) {
) )
} }
} }
func TestFuzzyModel(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("instal"),
WithRunError(),
)
NewExecutorTest(t,
WithName("not-fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("install"),
)
NewExecutorTest(t,
WithName("intern"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("intern"),
WithRunError(),
)
}
func TestIncludeChecksum(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("correct"),
WithExecutorOptions(
task.WithDir("testdata/includes_checksum/correct"),
),
)
NewExecutorTest(t,
WithName("incorrect"),
WithExecutorOptions(
task.WithDir("testdata/includes_checksum/incorrect"),
),
WithSetupError(),
WithFixtureTemplating(),
)
}

View File

@@ -44,7 +44,8 @@ func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
task: "default", task: "default",
vars: map[string]any{}, vars: map[string]any{},
TaskTest: TaskTest{ TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{}, experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
}, },
} }
// Apply the functional options // Apply the functional options
@@ -218,3 +219,17 @@ func TestListDescInterpolation(t *testing.T) {
}), }),
) )
} }
func TestJsonListFormat(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/json_list_format"),
),
WithListOptions(task.ListOptions{
FormatTaskListAsJSON: true,
}),
WithFixtureTemplating(),
)
}

21
go.mod
View File

@@ -4,8 +4,8 @@ go 1.23.0
require ( require (
github.com/Ladicle/tabwriter v1.0.0 github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1 github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.16.0 github.com/alecthomas/chroma/v2 v2.20.0
github.com/chainguard-dev/git-urls v1.0.2 github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0 github.com/dominikbraun/graph v0.23.0
@@ -13,22 +13,23 @@ require (
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2 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/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/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.1 github.com/otiai10/copy v1.14.1
github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0 github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.5.5 github.com/sebdah/goldie/v2 v2.7.1
github.com/spf13/pflag v1.0.6 github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.0.2 github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.13.0 golang.org/x/sync v0.16.0
golang.org/x/term v0.31.0 golang.org/x/term v0.33.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.11.0 mvdan.cc/sh/v3 v3.12.0
) )
require ( require (
@@ -55,6 +56,6 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.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 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= 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 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.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 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 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 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -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/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 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/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 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
@@ -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/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 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 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-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 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
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-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE= github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= 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 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/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 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 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/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
@@ -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 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.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 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 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 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.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 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.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/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= 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 { g.Go(func() error {
o.Tasks[i] = editors.Task{ o.Tasks[i] = editors.Task{
Name: tasks[i].Name(), Name: tasks[i].Name(),
Task: tasks[i].Task,
Desc: tasks[i].Desc, Desc: tasks[i].Desc,
Summary: tasks[i].Summary, Summary: tasks[i].Summary,
Aliases: aliases, Aliases: aliases,

View File

@@ -64,21 +64,15 @@ get_binaries() {
case "$PLATFORM" in case "$PLATFORM" in
darwin/amd64) BINARIES="task" ;; darwin/amd64) BINARIES="task" ;;
darwin/arm64) BINARIES="task" ;; darwin/arm64) BINARIES="task" ;;
darwin/armv5) BINARIES="task" ;; darwin/arm) BINARIES="task" ;;
darwin/armv6) BINARIES="task" ;;
darwin/armv7) BINARIES="task" ;;
linux/386) BINARIES="task" ;; linux/386) BINARIES="task" ;;
linux/amd64) BINARIES="task" ;; linux/amd64) BINARIES="task" ;;
linux/arm64) BINARIES="task" ;; linux/arm64) BINARIES="task" ;;
linux/armv5) BINARIES="task" ;; linux/arm) BINARIES="task" ;;
linux/armv6) BINARIES="task" ;;
linux/armv7) BINARIES="task" ;;
windows/386) BINARIES="task" ;; windows/386) BINARIES="task" ;;
windows/amd64) BINARIES="task" ;; windows/amd64) BINARIES="task" ;;
windows/arm64) BINARIES="task" ;; windows/arm64) BINARIES="task" ;;
windows/armv5) BINARIES="task" ;; windows/arm) BINARIES="task" ;;
windows/armv6) BINARIES="task" ;;
windows/armv7) 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" 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 exit 1

View File

@@ -9,6 +9,7 @@ type (
// Task describes a single task // Task describes a single task
Task struct { Task struct {
Name string `json:"name"` Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"` Desc string `json:"desc"`
Summary string `json:"summary"` Summary string `json:"summary"`
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`

View File

@@ -106,25 +106,17 @@ func ExpandLiteral(s string) (string, error) {
if s == "" { if s == "" {
return "", nil return "", nil
} }
s = escape(s)
p := syntax.NewParser() p := syntax.NewParser()
var words []*syntax.Word word, err := p.Document(strings.NewReader(s))
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
if err != nil { if err != nil {
return "", err return "", err
} }
if len(words) == 0 {
return "", nil
}
cfg := &expand.Config{ cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv), Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir, ReadDir2: os.ReadDir,
GlobStar: true, 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 // 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), Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir, ReadDir2: os.ReadDir,
GlobStar: true, GlobStar: true,
NullGlob: true,
} }
return expand.Fields(cfg, words...) return expand.Fields(cfg, words...)
} }

View File

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

View File

@@ -2,11 +2,14 @@ package templater
import ( import (
"maps" "maps"
"math/rand/v2"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
@@ -18,58 +21,27 @@ var templateFuncs template.FuncMap
func init() { func init() {
taskFuncs := template.FuncMap{ taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS }, "OS": os,
"ARCH": func() string { return runtime.GOARCH }, "ARCH": arch,
"numCPU": func() int { return runtime.NumCPU() }, "numCPU": runtime.NumCPU,
"catLines": func(s string) string { "catLines": catLines,
s = strings.ReplaceAll(s, "\r\n", " ") "splitLines": splitLines,
return strings.ReplaceAll(s, "\n", " ") "fromSlash": filepath.FromSlash,
}, "toSlash": filepath.ToSlash,
"splitLines": func(s string) []string { "exeExt": exeExt,
s = strings.ReplaceAll(s, "\r\n", "\n") "shellQuote": shellQuote,
return strings.Split(s, "\n") "splitArgs": splitArgs,
}, "IsSH": IsSH, // Deprecated
"fromSlash": func(path string) string { "joinPath": filepath.Join,
return filepath.FromSlash(path) "relPath": filepath.Rel,
}, "merge": merge,
"toSlash": func(path string) string { "spew": spew.Sdump,
return filepath.ToSlash(path) "fromYaml": fromYaml,
}, "mustFromYaml": mustFromYaml,
"exeExt": func() string { "toYaml": toYaml,
if runtime.GOOS == "windows" { "mustToYaml": mustToYaml,
return ".exe" "uuid": uuid.New,
} "randIntN": rand.IntN,
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)
},
} }
// aliases // aliases
@@ -83,3 +55,78 @@ func init() {
templateFuncs = template.FuncMap(sprig.TxtFuncMap()) templateFuncs = template.FuncMap(sprig.TxtFuncMap())
maps.Copy(templateFuncs, taskFuncs) 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" "maps"
"strings" "strings"
"github.com/go-task/template"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/taskfile/ast" "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 // Cache is a help struct that allow us to call "replaceX" funcs multiple

View File

@@ -4,6 +4,8 @@ import (
_ "embed" _ "embed"
"runtime/debug" "runtime/debug"
"strings" "strings"
"github.com/Masterminds/semver/v3"
) )
var ( var (
@@ -46,6 +48,10 @@ func getCommit(info *debug.BuildInfo) string {
// However, it can also be overridden at build time using: // However, it can also be overridden at build time using:
// -ldflags="-X 'github.com/go-task/task/v3/internal/version.version=vX.X.X'". // -ldflags="-X 'github.com/go-task/task/v3/internal/version.version=vX.X.X'".
func GetVersion() string { func GetVersion() string {
// If its a development version, we bump the minor version.
if commit != "" || dirty {
return semver.MustParse(version).IncMinor().String()
}
return version return version
} }
@@ -61,7 +67,7 @@ func GetVersionWithBuildInfo() string {
buildMetadata = append(buildMetadata, "dirty") buildMetadata = append(buildMetadata, "dirty")
} }
if len(buildMetadata) > 0 { if len(buildMetadata) > 0 {
return version + "+" + strings.Join(buildMetadata, ".") return GetVersion() + "+" + strings.Join(buildMetadata, ".")
} }
return version return GetVersion()
} }

View File

@@ -1 +1 @@
3.43.2 3.44.1

View File

@@ -0,0 +1,58 @@
package version
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestVersionTxt(t *testing.T) {
// Check that the version.txt is a valid semver version.
require.NotEmpty(t, GetVersion(), "version.txt is not semver compliant")
}
func TestGetVersion(t *testing.T) {
tests := []struct {
version string
commit string
dirty bool
want string
}{
{"1.2.3", "", false, "1.2.3"},
{"1.2.3", "", true, "1.3.0"},
{"1.2.3", "abcdefg", false, "1.3.0"},
{"1.2.3", "abcdefg", true, "1.3.0"},
}
for _, tt := range tests {
version = tt.version
commit = tt.commit
dirty = tt.dirty
t.Run(tt.want, func(t *testing.T) {
require.Equal(t, tt.want, GetVersion())
})
}
}
func TestGetVersionWithBuildInfo(t *testing.T) {
tests := []struct {
version string
commit string
dirty bool
want string
}{
{"1.2.3", "", false, "1.2.3"},
{"1.2.3", "", true, "1.3.0+dirty"},
{"1.2.3", "abcdefg", false, "1.3.0+abcdefg"},
{"1.2.3", "abcdefg", true, "1.3.0+abcdefg.dirty"},
}
for _, tt := range tests {
version = tt.version
commit = tt.commit
dirty = tt.dirty
t.Run(tt.want, func(t *testing.T) {
require.Equal(t, tt.want, GetVersionWithBuildInfo())
})
}
}

2
package-lock.json generated
View File

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

View File

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

View File

@@ -95,7 +95,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
} }
func (e *Executor) setupFuzzyModel() { func (e *Executor) setupFuzzyModel() {
if e.Taskfile != nil { if e.Taskfile == nil {
return return
} }
@@ -104,6 +104,9 @@ func (e *Executor) setupFuzzyModel() {
var words []string var words []string
for name, task := range e.Taskfile.Tasks.All(nil) { for name, task := range e.Taskfile.Tasks.All(nil) {
if task.Internal {
continue
}
words = append(words, name) words = append(words, name)
words = slices.Concat(words, task.Aliases) words = slices.Concat(words, task.Aliases)
} }

15
task.go
View File

@@ -8,6 +8,9 @@ import (
"slices" "slices"
"sync/atomic" "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/errors"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext" "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/summary"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
) )
const ( 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) e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
} }
exitCode, isExitError := interp.IsExitStatus(err) var exitCode interp.ExitStatus
if isExitError { if errors.As(err, &exitCode) {
if t.IgnoreError { if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue continue
} }
deferredExitCode = exitCode deferredExitCode = uint8(exitCode)
} }
if call.Indirect { 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 { if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr) 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) e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
return nil return nil
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"maps"
rand "math/rand/v2" rand "math/rand/v2"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -42,9 +43,11 @@ type (
FormatterTestOption FormatterTestOption
} }
TaskTest struct { TaskTest struct {
name string name string
experiments map[*experiments.Experiment]int experiments map[*experiments.Experiment]int
postProcessFns []PostProcessFn postProcessFns []PostProcessFn
fixtureTemplateData map[string]any
fixtureTemplatingEnabled bool
} }
) )
@@ -79,7 +82,22 @@ func (tt *TaskTest) writeFixture(
if goldenFileSuffix != "" { if goldenFileSuffix != "" {
goldenFileName += "-" + goldenFileSuffix goldenFileName += "-" + goldenFileSuffix
} }
g.Assert(t, goldenFileName, b) // Create a set of data to be made available to every test fixture
wd, err := os.Getwd()
require.NoError(t, err)
if tt.fixtureTemplatingEnabled {
fixtureTemplateData := map[string]any{
"TEST_NAME": t.Name(),
"TEST_DIR": wd,
}
// If the test has additional template data, copy it into the map
if tt.fixtureTemplateData != nil {
maps.Copy(fixtureTemplateData, tt.fixtureTemplateData)
}
g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, b)
} else {
g.Assert(t, goldenFileName, b)
}
} }
// writeFixtureBuffer is a wrapper for writing the main output of the task to a // writeFixtureBuffer is a wrapper for writing the main output of the task to a
@@ -234,23 +252,52 @@ func (opt *setupErrorTestOption) applyToFormatterTest(t *FormatterTest) {
t.wantSetupError = true t.wantSetupError = true
} }
// WithFixtureTemplating enables templating for the golden fixture files with
// the default set of data. This is useful if the golden file is dynamic in some
// way (e.g. contains user-specific directories). To add more data, see
// WithFixtureTemplateData.
func WithFixtureTemplating() TestOption {
return &fixtureTemplatingTestOption{}
}
type fixtureTemplatingTestOption struct{}
func (opt *fixtureTemplatingTestOption) applyToExecutorTest(t *ExecutorTest) {
t.fixtureTemplatingEnabled = true
}
func (opt *fixtureTemplatingTestOption) applyToFormatterTest(t *FormatterTest) {
t.fixtureTemplatingEnabled = true
}
// WithFixtureTemplateData adds data to the golden fixture file templates. Keys
// given here will override any existing values. This option will also enable
// global templating, so you do not need to call WithFixtureTemplating as well.
func WithFixtureTemplateData(key string, value any) TestOption {
return &fixtureTemplateDataTestOption{key, value}
}
type fixtureTemplateDataTestOption struct {
k string
v any
}
func (opt *fixtureTemplateDataTestOption) applyToExecutorTest(t *ExecutorTest) {
t.fixtureTemplatingEnabled = true
t.fixtureTemplateData[opt.k] = opt.v
}
func (opt *fixtureTemplateDataTestOption) applyToFormatterTest(t *FormatterTest) {
t.fixtureTemplatingEnabled = true
t.fixtureTemplateData[opt.k] = opt.v
}
// Post-processing // Post-processing
// A PostProcessFn is a function that can be applied to the output of a test // A PostProcessFn is a function that can be applied to the output of a test
// fixture before the file is written. // fixture before the file is written.
type PostProcessFn func(*testing.T, []byte) []byte type PostProcessFn func(*testing.T, []byte) []byte
// PPRemoveAbsolutePaths removes any absolute paths from the output of the task.
// This is useful when the task output contains paths that are can be different
// in different environments such as home directories. The function looks for
// any paths that contain the current working directory and truncates them.
func PPRemoveAbsolutePaths(t *testing.T, b []byte) []byte {
t.Helper()
wd, err := os.Getwd()
require.NoError(t, err)
return bytes.ReplaceAll(b, []byte(wd), nil)
}
// PPSortedLines sorts the lines of the output of the task. This is useful when // PPSortedLines sorts the lines of the output of the task. This is useful when
// the order of the output is not important, but the output is expected to be // the order of the output is not important, but the output is expected to be
// the same each time the task is run (e.g. when running tasks in parallel). // the same each time the task is run (e.g. when running tasks in parallel).
@@ -702,6 +749,7 @@ func TestIncludesRemote(t *testing.T) {
enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1) enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)
dir := "testdata/includes_remote" dir := "testdata/includes_remote"
os.RemoveAll(filepath.Join(dir, ".task", "remote"))
srv := httptest.NewServer(http.FileServer(http.Dir(dir))) srv := httptest.NewServer(http.FileServer(http.Dir(dir)))
defer srv.Close() defer srv.Close()
@@ -777,8 +825,8 @@ func TestIncludesRemote(t *testing.T) {
}, },
} }
for j, e := range executors { for _, e := range executors {
t.Run(fmt.Sprint(j), func(t *testing.T) { t.Run(e.name, func(t *testing.T) {
require.NoError(t, e.executor.Setup()) require.NoError(t, e.executor.Setup())
for k, taskCall := range taskCalls { for k, taskCall := range taskCalls {
@@ -933,6 +981,7 @@ func TestIncludesHttp(t *testing.T) {
for _, tc := range tcs { for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel()
task, err := e.CompiledTask(&task.Call{Task: tc.name}) task, err := e.CompiledTask(&task.Call{Task: tc.name})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.dir, task.Dir) assert.Equal(t, tc.dir, task.Dir)
@@ -1964,10 +2013,6 @@ task: [included3:task1] echo "VAR_1 is included-default-var1"
VAR_1 is included-default-var1 VAR_1 is included-default-var1
task: [included3:task1] echo "VAR_2 is included-default-var2" task: [included3:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2 VAR_2 is included-default-var2
task: [included4:task1] echo "VAR_1 is included4-var1"
VAR_1 is included4-var1
task: [included4:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2
`) `)
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task1"})) require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task1"}))
t.Log(buff.String()) t.Log(buff.String())
@@ -2151,7 +2196,7 @@ func TestUserWorkingDirectory(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := task.NewExecutor( e := task.NewExecutor(
task.WithEntrypoint("testdata/user_working_dir/Taskfile.yml"), task.WithDir("testdata/user_working_dir"),
task.WithStdout(&buff), task.WithStdout(&buff),
task.WithStderr(&buff), task.WithStderr(&buff),
) )

View File

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

View File

@@ -17,6 +17,8 @@ type Node interface {
Parent() Node Parent() Node
Location() string Location() string
Dir() string Dir() string
Checksum() string
Verify(checksum string) bool
ResolveEntrypoint(entrypoint string) (string, error) ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error) ResolveDir(dir string) (string, error)
} }

View File

@@ -1,19 +1,20 @@
package taskfile package taskfile
type ( type (
NodeOption func(*BaseNode) NodeOption func(*baseNode)
// BaseNode is a generic node that implements the Parent() methods of the // baseNode is a generic node that implements the Parent() methods of the
// NodeReader interface. It does not implement the Read() method and it // NodeReader interface. It does not implement the Read() method and it
// designed to be embedded in other node types so that this boilerplate code // designed to be embedded in other node types so that this boilerplate code
// does not need to be repeated. // does not need to be repeated.
BaseNode struct { baseNode struct {
parent Node parent Node
dir string dir string
checksum string
} }
) )
func NewBaseNode(dir string, opts ...NodeOption) *BaseNode { func NewBaseNode(dir string, opts ...NodeOption) *baseNode {
node := &BaseNode{ node := &baseNode{
parent: nil, parent: nil,
dir: dir, dir: dir,
} }
@@ -27,15 +28,29 @@ func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
} }
func WithParent(parent Node) NodeOption { func WithParent(parent Node) NodeOption {
return func(node *BaseNode) { return func(node *baseNode) {
node.parent = parent 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 return node.parent
} }
func (node *BaseNode) Dir() string { func (node *baseNode) Dir() string {
return node.dir 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" const remoteCacheDir = "remote"
type CacheNode struct { type CacheNode struct {
*BaseNode *baseNode
source RemoteNode source RemoteNode
} }
func NewCacheNode(source RemoteNode, dir string) *CacheNode { func NewCacheNode(source RemoteNode, dir string) *CacheNode {
return &CacheNode{ return &CacheNode{
BaseNode: &BaseNode{ baseNode: &baseNode{
dir: filepath.Join(dir, remoteCacheDir), dir: filepath.Join(dir, remoteCacheDir),
}, },
source: source, source: source,

View File

@@ -13,8 +13,8 @@ import (
// A FileNode is a node that reads a taskfile from the local filesystem. // A FileNode is a node that reads a taskfile from the local filesystem.
type FileNode struct { type FileNode struct {
*BaseNode *baseNode
Entrypoint string entrypoint string
} }
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { 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 nil, err
} }
return &FileNode{ return &FileNode{
BaseNode: base, baseNode: base,
Entrypoint: entrypoint, entrypoint: entrypoint,
}, nil }, nil
} }
func (node *FileNode) Location() string { func (node *FileNode) Location() string {
return node.Entrypoint return node.entrypoint
} }
func (node *FileNode) Read() ([]byte, error) { 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 // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another // 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 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 // NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another // 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 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. // An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct { type GitNode struct {
*BaseNode *baseNode
URL *url.URL url *url.URL
rawUrl string rawUrl string
ref string ref string
path string path string
@@ -40,23 +40,20 @@ func NewGitNode(
return nil, err return nil, err
} }
basePath, path := func() (string, string) { basePath, path := splitURLOnDoubleSlash(u)
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
ref := u.Query().Get("ref") ref := u.Query().Get("ref")
rawUrl := u.String() rawUrl := u.Redacted()
u.RawQuery = "" u.RawQuery = ""
u.Path = basePath u.Path = basePath
if u.Scheme == "http" && !insecure { if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint} return nil, &errors.TaskfileNotSecureError{URI: u.Redacted()}
} }
return &GitNode{ return &GitNode{
BaseNode: base, baseNode: base,
URL: u, url: u,
rawUrl: rawUrl, rawUrl: rawUrl,
ref: ref, ref: ref,
path: path, path: path,
@@ -79,7 +76,7 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
fs := memfs.New() fs := memfs.New()
storer := memory.NewStorage() storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{ _, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(), URL: node.url.String(),
ReferenceName: plumbing.ReferenceName(node.ref), ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true, SingleBranch: true,
Depth: 1, Depth: 1,
@@ -102,7 +99,7 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path) dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint)) resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
if node.ref != "" { if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil 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 { func (node *GitNode) CacheKey() string {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=") checksum := strings.TrimRight(checksum([]byte(node.Location())), "=")
prefix := filepath.Base(filepath.Dir(node.path)) lastDir := filepath.Base(filepath.Dir(node.path))
lastDir := filepath.Base(node.path) prefix := filepath.Base(node.path)
// Means it's not "", nor "." nor "/", so it's a valid directory // Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 { 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" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGitNode_ssh(t *testing.T) { func TestGitNode_ssh(t *testing.T) {
@@ -13,8 +14,8 @@ func TestGitNode_ssh(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path) 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//Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String()) assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint) assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
@@ -27,8 +28,8 @@ func TestGitNode_sshWithDir(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path) 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//directory/Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String()) assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) 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.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path) 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//Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String()) assert.Equal(t, "https://github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint) 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.NoError(t, err)
assert.Equal(t, "main", node.ref) assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path) 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//directory/Taskfile.yml?ref=main", node.Location())
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String()) assert.Equal(t, "https://github.com/foo/bar.git", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml") entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint) 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) { func TestGitNode_CacheKey(t *testing.T) {
t.Parallel() t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false) tests := []struct {
assert.NoError(t, err) entrypoint string
key := node.CacheKey() expectedKey string
assert.Equal(t, "Taskfile.yml-directory.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb", key) }{
{
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",
},
}
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false) for _, tt := range tests {
assert.NoError(t, err) node, err := NewGitNode(tt.entrypoint, "", false)
key = node.CacheKey() require.NoError(t, err)
assert.Equal(t, "Taskfile.yml-..39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4", key) key := node.CacheKey()
assert.Equal(t, tt.expectedKey, 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)
} }

View File

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

View File

@@ -249,7 +249,8 @@ func (r *Reader) include(ctx context.Context, node Node) error {
Aliases: include.Aliases, Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport, AdvancedImport: include.AdvancedImport,
Excludes: include.Excludes, Excludes: include.Excludes,
Vars: templater.ReplaceVars(include.Vars, cache), Vars: include.Vars,
Checksum: include.Checksum,
} }
if err := cache.Err(); err != nil { if err := cache.Err(); err != nil {
return err return err
@@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure, includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
WithParent(node), WithParent(node),
WithChecksum(include.Checksum),
) )
if err != nil { if err != nil {
if include.Optional { if include.Optional {
@@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
if node, isRemote := node.(RemoteNode); isRemote { if node, isRemote := node.(RemoteNode); isRemote {
return r.readRemoteNodeContent(ctx, node) 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) { func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
@@ -427,17 +446,29 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
} }
r.debugf("found remote file at %q\n", node.Location()) r.debugf("found remote file at %q\n", node.Location())
checksum := checksum(downloadedBytes)
prompt := cache.ChecksumPrompt(checksum)
// Prompt the user if required // If the given checksum doesn't match the sum pinned in the Taskfile
if prompt != "" { checksum := checksum(downloadedBytes)
if err := func() error { if !node.Verify(checksum) {
r.promptMutex.Lock() return nil, &errors.TaskfileDoesNotMatchChecksum{
defer r.promptMutex.Unlock() URI: node.Location(),
return r.promptf(prompt, node.Location()) ExpectedChecksum: node.Checksum(),
}(); err != nil { ActualChecksum: checksum,
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} }
}
// 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()
defer r.promptMutex.Unlock()
return r.promptf(prompt, node.Location())
}(); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
}
} }
} }

View File

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

View File

@@ -1 +1 @@
task: Missing schema version in Taskfile "/testdata/empty_taskfile/Taskfile.yml" task: Missing schema version in Taskfile "{{.TEST_DIR}}/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 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 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" version: "3"
vars:
VAR_1: included4-var1
includes: includes:
included1: included1:
taskfile: include/Taskfile.include.yml taskfile: include/Taskfile.include1.yml
vars: vars:
VAR_1: included1-var1 VAR_1: included1-var1
included2: included2:
taskfile: include/Taskfile.include.yml taskfile: include/Taskfile.include2.yml
vars: vars:
VAR_1: included2-var1 VAR_1: included2-var1
included3: included3:
taskfile: include/Taskfile.include.yml taskfile: include/Taskfile.include3.yml
included4:
taskfile: include/Taskfile.include.yml
vars:
VAR_1: "{{.VAR_1}}"
tasks: tasks:
task1: task1:
@@ -25,4 +18,3 @@ tasks:
- task: included1:task1 - task: included1:task1
- task: included2:task1 - task: included2:task1
- task: included3: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 +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

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

@@ -2,107 +2,113 @@ version: '3'
tasks: tasks:
default: default:
- task: dynamic
- task: string
- task: bool
- task: int
- task: string-array
- task: map - task: map
- task: for-string - task: nested-map
- task: for-int - task: slice
- task: for-map - task: ref
- task: for-multi-layer-map - task: ref-sh
- task: ref-dep
dynamic: - task: ref-resolver
vars: - task: json
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}}'
map: map:
vars: vars:
MAP_1: {A: 1, B: 2, C: 3} MAP:
MAP_2: {B: 4, C: 5, D: 6} map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_3: {C: 7, D: 8, E: 9}
cmds: cmds:
- echo '{{merge .MAP_1 .MAP_2 .MAP_3}}' - task: print-story
vars:
VAR:
ref: .MAP
for-string: nested-map:
vars: vars:
LIST: [foo, bar, baz] FOO: "foo"
nested:
map:
variables:
work: "{{.FOO}}"
cmds: cmds:
- for: - echo {{.nested.variables.work}}
var: LIST
cmd: echo {{.ITEM}}
for-int: slice:
vars: vars:
LIST: [1, 2, 3] FOO: "foo"
BAR: "bar"
slice_variables_work: ["{{.FOO}}","{{.BAR}}"]
cmds: cmds:
- for: - echo {{index .slice_variables_work 0}} {{index .slice_variables_work 1}}
var: LIST
cmd: echo {{add .ITEM 100}}
for-map: ref:
vars: vars:
MAP: MAP:
KEY_1: value_1 map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
KEY_2: value_2 MAP_REF:
KEY_3: value_3 ref: .MAP
cmds: cmds:
- for: - task: print-story
var: MAP vars:
cmd: echo {{.KEY}} {{.ITEM}} 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: vars:
MAP: MAP:
KEY_1: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
SUBKEY: sub_value_1 deps:
KEY_2: - task: print-story
SUBKEY: sub_value_2 vars:
KEY_3: VAR:
SUBKEY: sub_value_3 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: cmds:
- for: - task: print-var
var: MAP vars:
cmd: echo {{.KEY}} {{.ITEM.SUBKEY}} 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 -}}"

133
watch.go
View File

@@ -19,6 +19,8 @@ import (
"github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/fingerprint"
"github.com/go-task/task/v3/internal/fsnotifyext" "github.com/go-task/task/v3/internal/fsnotifyext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/taskfile/ast"
) )
const defaultWaitTime = 100 * time.Millisecond const defaultWaitTime = 100 * time.Millisecond
@@ -71,12 +73,9 @@ func (e *Executor) watchTasks(calls ...*Call) error {
for { for {
select { select {
case event, ok := <-eventsChan: case event, ok := <-eventsChan:
switch { if !ok {
case !ok:
cancel() cancel()
return return
case event.Op == fsnotify.Chmod:
continue
} }
e.Logger.VerboseErrf(logger.Magenta, "task: received watch event: %v\n", event) e.Logger.VerboseErrf(logger.Magenta, "task: received watch event: %v\n", event)
@@ -88,17 +87,22 @@ func (e *Executor) watchTasks(calls ...*Call) error {
for _, c := range calls { for _, c := range calls {
c := c c := c
go func() { go func() {
if ShouldIgnore(event.Name) {
e.Logger.VerboseErrf(logger.Magenta, "task: event skipped for being an ignored dir: %s\n", event.Name)
return
}
t, err := e.GetTask(c) t, err := e.GetTask(c)
if err != nil { if err != nil {
e.Logger.Errf(logger.Red, "%v\n", err) e.Logger.Errf(logger.Red, "%v\n", err)
return return
} }
baseDir := filepathext.SmartJoin(e.Dir, t.Dir) baseDir := filepathext.SmartJoin(e.Dir, t.Dir)
files, err := fingerprint.Globs(baseDir, t.Sources) files, err := e.collectSources(calls)
if err != nil { if err != nil {
e.Logger.Errf(logger.Red, "%v\n", err) e.Logger.Errf(logger.Red, "%v\n", err)
return return
} }
if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) { if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) {
relPath, _ := filepath.Rel(baseDir, event.Name) relPath, _ := filepath.Rel(baseDir, event.Name)
e.Logger.VerboseErrf(logger.Magenta, "task: skipped for file not in sources: %s\n", relPath) e.Logger.VerboseErrf(logger.Magenta, "task: skipped for file not in sources: %s\n", relPath)
@@ -161,65 +165,36 @@ func closeOnInterrupt(w *fsnotify.Watcher) {
} }
func (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error { func (e *Executor) registerWatchedDirs(w *fsnotify.Watcher, calls ...*Call) error {
var registerTaskDirs func(*Call) error files, err := e.collectSources(calls)
registerTaskDirs = func(c *Call) error { if err != nil {
task, err := e.CompiledTask(c) return err
if err != nil {
return err
}
for _, d := range task.Deps {
if err := registerTaskDirs(&Call{Task: d.Task, Vars: d.Vars}); err != nil {
return err
}
}
for _, c := range task.Cmds {
if c.Task != "" {
if err := registerTaskDirs(&Call{Task: c.Task, Vars: c.Vars}); err != nil {
return err
}
}
}
files, err := fingerprint.Globs(task.Dir, task.Sources)
if err != nil {
return err
}
for _, f := range files {
d := filepath.Dir(f)
if isSet, ok := e.watchedDirs.Load(d); ok && isSet {
continue
}
if ShouldIgnoreFile(d) {
continue
}
if err := w.Add(d); err != nil {
return err
}
e.watchedDirs.Store(d, true)
relPath, _ := filepath.Rel(e.Dir, d)
w.Events <- fsnotify.Event{Name: f, Op: fsnotify.Create}
e.Logger.VerboseOutf(logger.Green, "task: watching new dir: %v\n", relPath)
}
return nil
} }
for _, f := range files {
for _, c := range calls { d := filepath.Dir(f)
if err := registerTaskDirs(c); err != nil { if isSet, ok := e.watchedDirs.Load(d); ok && isSet {
continue
}
if ShouldIgnore(d) {
continue
}
if err := w.Add(d); err != nil {
return err return err
} }
e.watchedDirs.Store(d, true)
relPath, _ := filepath.Rel(e.Dir, d)
e.Logger.VerboseOutf(logger.Green, "task: watching new dir: %v\n", relPath)
} }
return nil return nil
} }
func ShouldIgnoreFile(path string) bool { var ignorePaths = []string{
ignorePaths := []string{ "/.task",
"/.task", "/.git",
"/.git", "/.hg",
"/.hg", "/node_modules",
"/node_modules", }
}
func ShouldIgnore(path string) bool {
for _, p := range ignorePaths { for _, p := range ignorePaths {
if strings.Contains(path, fmt.Sprintf("%s/", p)) || strings.HasSuffix(path, p) { if strings.Contains(path, fmt.Sprintf("%s/", p)) || strings.HasSuffix(path, p) {
return true return true
@@ -227,3 +202,47 @@ func ShouldIgnoreFile(path string) bool {
} }
return false return false
} }
func (e *Executor) collectSources(calls []*Call) ([]string, error) {
var sources []string
err := e.traverse(calls, func(task *ast.Task) error {
files, err := fingerprint.Globs(task.Dir, task.Sources)
if err != nil {
return err
}
sources = append(sources, files...)
return nil
})
return slicesext.UniqueJoin(sources), err
}
type traverseFunc func(*ast.Task) error
func (e *Executor) traverse(calls []*Call, yield traverseFunc) error {
for _, c := range calls {
task, err := e.CompiledTask(c)
if err != nil {
return err
}
for _, dep := range task.Deps {
if dep.Task != "" {
if err := e.traverse([]*Call{{Task: dep.Task, Vars: dep.Vars}}, yield); err != nil {
return err
}
}
}
for _, cmd := range task.Cmds {
if cmd.Task != "" {
if err := e.traverse([]*Call{{Task: cmd.Task, Vars: cmd.Vars}}, yield); err != nil {
return err
}
}
}
if err := yield(task); err != nil {
return err
}
}
return nil
}

View File

@@ -31,16 +31,17 @@ task: Started watching for tasks: default
task: [default] echo "Task running!" task: [default] echo "Task running!"
Task running! Task running!
task: task "default" finished running task: task "default" finished running
task: Task "default" is up to date task: [default] echo "Task running!"
Task running!
task: task "default" finished running task: task "default" finished running
`) `)
var buff bytes.Buffer var buff bytes.Buffer
e := task.NewExecutor( e := task.NewExecutor(
task.ExecutorWithDir(dir), task.WithDir(dir),
task.ExecutorWithStdout(&buff), task.WithStdout(&buff),
task.ExecutorWithStderr(&buff), task.WithStderr(&buff),
task.ExecutorWithWatch(true), task.WithWatch(true),
) )
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
@@ -49,10 +50,10 @@ task: task "default" finished running
dirPath := filepathext.SmartJoin(dir, "src") dirPath := filepathext.SmartJoin(dir, "src")
filePath := filepathext.SmartJoin(dirPath, "a") filePath := filepathext.SmartJoin(dirPath, "a")
err := os.MkdirAll(dirPath, 0755) err := os.MkdirAll(dirPath, 0o755)
require.NoError(t, err) require.NoError(t, err)
err = os.WriteFile(filePath, []byte("test"), 0644) err = os.WriteFile(filePath, []byte("test"), 0o644)
require.NoError(t, err) require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@@ -71,16 +72,16 @@ task: task "default" finished running
} }
}() }()
time.Sleep(10 * time.Millisecond) time.Sleep(200 * time.Millisecond)
err = os.WriteFile(filePath, []byte("test updated"), 0644) err = os.WriteFile(filePath, []byte("test updated"), 0o644)
require.NoError(t, err) require.NoError(t, err)
time.Sleep(150 * time.Millisecond) time.Sleep(200 * time.Millisecond)
cancel() cancel()
assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String())) assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
} }
func TestShouldIgnoreFile(t *testing.T) { func TestShouldIgnore(t *testing.T) {
t.Parallel() t.Parallel()
tt := []struct { tt := []struct {
@@ -95,7 +96,7 @@ func TestShouldIgnoreFile(t *testing.T) {
ct := ct ct := ct
t.Run(fmt.Sprintf("ignore - %d", k), func(t *testing.T) { t.Run(fmt.Sprintf("ignore - %d", k), func(t *testing.T) {
t.Parallel() t.Parallel()
require.Equal(t, task.ShouldIgnoreFile(ct.path), ct.expect) require.Equal(t, task.ShouldIgnore(ct.path), ct.expect)
}) })
} }
} }

View File

@@ -5,6 +5,52 @@ sidebar_position: 14
# Changelog # 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 ## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by - Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by

View File

@@ -43,12 +43,16 @@ Studio Code][vscode-task].
## 2. Making changes ## 2. Making changes
- **Code style** - Try to maintain the existing code style where possible. Go - **Code style** - Try to maintain the existing code style where possible. Go
code should be formatted by [`gofumpt`][gofumpt] and linted using code should be formatted and linted by [`golangci-lint`][golangci-lint]. This
[`golangci-lint`][golangci-lint]. Any Markdown or TypeScript files should be wraps the [`gofumpt`][gofumpt] and [`gci`][gci] formatters and a number of
formatted and linted by [Prettier][prettier]. This style is enforced by our CI linters. We recommend that you take a look at the [golangci-lint
to ensure that we have a consistent style across the project. You can use the docs][golangci-lint-docs] for a guide on how to setup your editor to
`task lint` command to lint the code locally and the `task lint:fix` command auto-format your code. Any Markdown or TypeScript files should be formatted
to automatically fix any issues that are found. and linted by [Prettier][prettier]. This style is enforced by our CI to ensure
that we have a consistent style across the project. You can use the `task
lint` command to lint the code locally and the `task lint:fix` command to try
to automatically fix any issues that are found. You can also use the `task
fmt` command to auto-format the files if your editor doesn't do it for you.
- **Documentation** - Ensure that you add/update any relevant documentation. See - **Documentation** - Ensure that you add/update any relevant documentation. See
the [updating documentation](#updating-documentation) section below. the [updating documentation](#updating-documentation) section below.
- **Tests** - Ensure that you add/update any relevant tests and that all tests - **Tests** - Ensure that you add/update any relevant tests and that all tests
@@ -73,8 +77,9 @@ install the extension.
Task uses [Docusaurus][docusaurus] to host a documentation server. The code for Task uses [Docusaurus][docusaurus] to host a documentation server. The code for
this is located in the core Task repository. This can be setup and run locally this is located in the core Task repository. This can be setup and run locally
by using `task website` (requires `nodejs` & `yarn`). All content is written in by using `task website` (requires `nodejs` & `yarn`). All content is written in
Markdown and is located in the `website/docs` directory. All Markdown documents [MDX][mdx] (an extension of Markdown) and is located in the `website/docs`
should have an 80 character line wrap limit (enforced by Prettier). directory. All Markdown documents should have an 80 character line wrap limit
(enforced by Prettier).
When making a change, consider whether a change to the [Usage Guide](/usage) is When making a change, consider whether a change to the [Usage Guide](/usage) is
necessary. This document contains descriptions and examples of how to use Task necessary. This document contains descriptions and examples of how to use Task
@@ -154,7 +159,9 @@ If you have questions, feel free to ask them in the `#help` forum channel on our
[vscode-task]: https://github.com/go-task/vscode-task [vscode-task]: https://github.com/go-task/vscode-task
[go]: https://go.dev [go]: https://go.dev
[gofumpt]: https://github.com/mvdan/gofumpt [gofumpt]: https://github.com/mvdan/gofumpt
[gci]: https://github.com/daixiang0/gci
[golangci-lint]: https://golangci-lint.run [golangci-lint]: https://golangci-lint.run
[golangci-lint-docs]: https://golangci-lint.run/welcome/integrations/
[prettier]: https://prettier.io [prettier]: https://prettier.io
[nodejs]: https://nodejs.org/en/ [nodejs]: https://nodejs.org/en/
[yarn]: https://yarnpkg.com/ [yarn]: https://yarnpkg.com/
@@ -166,4 +173,5 @@ If you have questions, feel free to ask them in the `#help` forum channel on our
[discord-server]: https://discord.gg/6TY36E39UK [discord-server]: https://discord.gg/6TY36E39UK
[discussion]: https://github.com/go-task/task/discussions [discussion]: https://github.com/go-task/task/discussions
[conventional-commits]: https://www.conventionalcommits.org [conventional-commits]: https://www.conventionalcommits.org
[mdx]: https://mdxjs.com/
{/* prettier-ignore-end */} {/* prettier-ignore-end */}

View File

@@ -182,9 +182,11 @@ includes:
## Security ## Security
### Automatic checksums
Running commands from sources that you do not control is always a potential Running commands from sources that you do not control is always a potential
security risk. For this reason, we have added some checks when using remote security risk. For this reason, we have added some automatic checks when using
Taskfiles: remote Taskfiles:
1. When running a task from a remote Taskfile for the first time, Task will 1. When running a task from a remote Taskfile for the first time, Task will
print a warning to the console asking you to check that you are sure that you print a warning to the console asking you to check that you are sure that you
@@ -209,6 +211,38 @@ flag. Before enabling this flag, you should:
containing a commit hash) to prevent Task from automatically accepting a containing a commit hash) to prevent Task from automatically accepting a
prompt that says a remote Taskfile has changed. prompt that says a remote Taskfile has changed.
### Manual checksum pinning
Alternatively, if you expect the contents of your remote files to be a constant
value, you can pin the checksum of the included file instead:
```yaml
version: '3'
includes:
included:
taskfile: https://taskfile.dev
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
```
This will disable the automatic checksum prompts discussed above. However, if
the checksums do not match, Task will exit immediately with an error. When
setting this up for the first time, you may not know the correct value of the
checksum. There are a couple of ways you can obtain this:
1. Add the include normally without the `checksum` key. The first time you run
the included Taskfile, a `.task/remote` temporary directory is created. Find
the correct set of files for your included Taskfile and open the file that
ends with `.checksum`. You can copy the contents of this file and paste it
into the `checksum` key of your include. This method is safest as it allows
you to inspect the downloaded Taskfile before you pin it.
2. Alternatively, add the include with a temporary random value in the
`checksum` key. When you try to run the Taskfile, you will get an error that
will report the incorrect expected checksum and the actual checksum. You can
copy the actual checksum and replace your temporary random value.
### TLS
Task currently supports both `http` and `https` URLs. However, the `http` Task currently supports both `http` and `https` URLs. However, the `http`
requests will not execute by default unless you run the task with the requests will not execute by default unless you run the task with the
`--insecure` flag. This is to protect you from accidentally running a remote `--insecure` flag. This is to protect you from accidentally running a remote

View File

@@ -36,6 +36,14 @@ repository [[package](https://formulae.brew.sh/formula/go-task)]
brew install go-task brew install go-task
``` ```
### [Macports][macports] ![][macos] ![][community] \{#macports}
Task repository is tracked by Macports [[package](https://ports.macports.org/port/go-task/details/)] [[source](https://github.com/macports/macports-ports/blob/master/devel/go-task/Portfile)]:
```shell
port install go-task
```
### [Snap][snapcraft] ![][macos] ![][linux] \{#snap} ### [Snap][snapcraft] ![][macos] ![][linux] \{#snap}
Task is available on [Snapcraft][snapcraft] [[source](https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml)], but keep in mind that your Linux Task is available on [Snapcraft][snapcraft] [[source](https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml)], but keep in mind that your Linux
@@ -104,6 +112,14 @@ pacman -S go-task
dnf install go-task dnf install go-task
``` ```
### FreeBSD ([Ports][freebsdports]) ![][freebsd] ![][community] \{#freebsd}
[[package](https://cgit.freebsd.org/ports/tree/devel/task)] [[source](https://cgit.freebsd.org/ports/tree/devel/task/Makefile)]
```shell
pkg install task
```
### NixOS ([nix][nix]) ![][nixos] ![][linux] ![][community] \{#nix} ### NixOS ([nix][nix]) ![][nixos] ![][linux] ![][community] \{#nix}
[[source](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix)] [[source](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/go/go-task/package.nix)]
@@ -304,6 +320,7 @@ task --completion fish > ~/.config/fish/completions/task.fish
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[homebrew]: https://brew.sh [homebrew]: https://brew.sh
[macports]: https://macports.org
[snapcraft]: https://snapcraft.io/task [snapcraft]: https://snapcraft.io/task
[winget]: https://github.com/microsoft/winget-cli [winget]: https://github.com/microsoft/winget-cli
[choco]: https://chocolatey.org [choco]: https://chocolatey.org
@@ -317,6 +334,7 @@ task --completion fish > ~/.config/fish/completions/task.fish
[aqua]: https://aquaproj.github.io [aqua]: https://aquaproj.github.io
[pacstall]: https://github.com/pacstall/pacstall [pacstall]: https://github.com/pacstall/pacstall
[pkgx]: https://pkgx.sh [pkgx]: https://pkgx.sh
[freebsdports]: https://ports.freebsd.org/cgi/ports.cgi
[go]: https://golang.org [go]: https://golang.org
[godownloader]: https://github.com/goreleaser/godownloader [godownloader]: https://github.com/goreleaser/godownloader
@@ -332,4 +350,5 @@ task --completion fish > ~/.config/fish/completions/task.fish
[nixos]: https://img.shields.io/badge/NixOS-5277C3?logo=nixos&logoColor=fff [nixos]: https://img.shields.io/badge/NixOS-5277C3?logo=nixos&logoColor=fff
[debian]: https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=fff [debian]: https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=fff
[ubuntu]: https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=fff [ubuntu]: https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=fff
[freebsd]: https://img.shields.io/badge/FreeBSD-990000?logo=freebsd&logoColor=fff
{/* prettier-ignore-end */} {/* prettier-ignore-end */}

View File

@@ -104,6 +104,7 @@ structure:
"tasks": [ "tasks": [
{ {
"name": "", "name": "",
"task": "",
"desc": "", "desc": "",
"summary": "", "summary": "",
"up_to_date": false, "up_to_date": false,

View File

@@ -34,6 +34,7 @@ toc_max_heading_level: 5
| `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. | | `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. | | `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. |
| `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. | | `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. |
| `checksum` | `string` | | The checksum of the file you expect to include. If the checksum does not match, the file will not be included. |
:::info :::info

View File

@@ -102,7 +102,8 @@ special variable will be overridden.
| Var | Description | | Var | Description |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `CLI_ARGS` | Contain all extra arguments passed after `--` when calling Task through the CLI. | | `CLI_ARGS` | Contain all extra arguments passed after `--` when calling Task through the CLI as a string. |
| `CLI_ARGS_LIST` | Contain all extra arguments passed after `--` when calling Task through the CLI as a shell parsed list. |
| `CLI_FORCE` | A boolean containing whether the `--force` or `--force-all` flags were set. | | `CLI_FORCE` | A boolean containing whether the `--force` or `--force-all` flags were set. |
| `CLI_SILENT` | A boolean containing whether the `--silent` flag was set. | | `CLI_SILENT` | A boolean containing whether the `--silent` flag was set. |
| `CLI_VERBOSE` | A boolean containing whether the `--verbose` flag was set. | | `CLI_VERBOSE` | A boolean containing whether the `--verbose` flag was set. |
@@ -115,7 +116,7 @@ special variable will be overridden.
| `TASKFILE` | The absolute path of the included Taskfile. | | `TASKFILE` | The absolute path of the included Taskfile. |
| `TASKFILE_DIR` | The absolute path of the included Taskfile directory. | | `TASKFILE_DIR` | The absolute path of the included Taskfile directory. |
| `TASK_DIR` | The absolute path of the directory where the task is executed. | | `TASK_DIR` | The absolute path of the directory where the task is executed. |
| `USER_WORKING_DIR` | The absolute path of the directory `task` was called from, or the value of `--dir` (`-d`) if given. | | `USER_WORKING_DIR` | The absolute path of the directory `task` was called from. |
| `CHECKSUM` | The checksum of the files listed in `sources`. Only available within the `status` prop and if method is set to `checksum`. | | `CHECKSUM` | The checksum of the files listed in `sources`. Only available within the `status` prop and if method is set to `checksum`. |
| `TIMESTAMP` | The date object of the greatest timestamp of the files listed in `sources`. Only available within the `status` prop and if method is set to `timestamp`. | | `TIMESTAMP` | The date object of the greatest timestamp of the files listed in `sources`. Only available within the `status` prop and if method is set to `timestamp`. |
| `TASK_VERSION` | The current version of task. | | `TASK_VERSION` | The current version of task. |
@@ -269,6 +270,10 @@ description here for completeness. For detailed usage, please refer to the
| `b32enc` | Encodes a string into base 32. | | `b32enc` | Encodes a string into base 32. |
| `b32dec` | Decodes a string from base 32. | | `b32dec` | Decodes a string from base 32. |
:::note
YAML encoding functions are [provided directly by Task](#task-functions).
:::
#### [List Functions][list-functions] #### [List Functions][list-functions]
| Function | Description | | Function | Description |
@@ -336,6 +341,10 @@ description here for completeness. For detailed usage, please refer to the
| `osExt` | Returns the file extension of a filepath. | | `osExt` | Returns the file extension of a filepath. |
| `osIsAbs` | Checks if a filepath is absolute. | | `osIsAbs` | Checks if a filepath is absolute. |
:::note
More filepath encoding functions are [provided directly by Task](#task-functions).
:::
#### [Flow Control Functions][flow-control-functions] #### [Flow Control Functions][flow-control-functions]
| Function | Description | | Function | Description |
@@ -375,7 +384,7 @@ Lastly, Task itself provides a few functions:
| Function | Description | | Function | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `OS` | Returns the operating system. Possible values are `windows`, `linux`, `darwin` (macOS) and `freebsd`. | | `OS` | Returns the operating system. Possible values are `windows`, `linux`, `darwin` (macOS) and `freebsd`. |
| `ARCH` | Returns the architecture Task was compiled to: `386`, `amd64`, `arm` or `s390x`. | | `ARCH` | Returns the architecture Task was compiled to: `386`, `amd64`, `arm` or `s390x`. |
| `numCPU` | Returns the number of logical CPU's usable by the current process. | | `numCPU` | Returns the number of logical CPU's usable by the current process. |
| `splitLines` | Splits Unix (`\n`) and Windows (`\r\n`) styled newlines. | | `splitLines` | Splits Unix (`\n`) and Windows (`\r\n`) styled newlines. |
| `catLines` | Replaces Unix (`\n`) and Windows (`\r\n`) styled newlines with a space. | | `catLines` | Replaces Unix (`\n`) and Windows (`\r\n`) styled newlines with a space. |
@@ -388,6 +397,10 @@ Lastly, Task itself provides a few functions:
| `relPath` | Converts an absolute path (second argument) into a relative path, based on a base path (first argument). The same as Go's [filepath.Rel](https://pkg.go.dev/path/filepath#Rel). | | `relPath` | Converts an absolute path (second argument) into a relative path, based on a base path (first argument). The same as Go's [filepath.Rel](https://pkg.go.dev/path/filepath#Rel). |
| `merge` | Creates a new map that is a copy of the first map with the keys of each subsequent map merged into it. If there is a duplicate key, the value of the last map with that key is used. | | `merge` | Creates a new map that is a copy of the first map with the keys of each subsequent map merged into it. If there is a duplicate key, the value of the last map with that key is used. |
| `spew` | Returns the Go representation of a specific variable. Useful for debugging. Uses the [davecgh/go-spew](https://github.com/davecgh/go-spew) package. | | `spew` | Returns the Go representation of a specific variable. Useful for debugging. Uses the [davecgh/go-spew](https://github.com/davecgh/go-spew) package. |
| `fromYaml`\* | Decodes a YAML string into an object. |
| `toYaml`\* | Encodes an object as a YAML string. |
| `uuid` | Generates a new pseudo-random UUIDv4 string. |
| `randIntN` | Generates a new pseudo-random, non-negative, 32bit integer in the half-open interval `[0,n)`. Generated numbers are not suitable for security-sensitive work. |
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[text/template]: https://pkg.go.dev/text/template [text/template]: https://pkg.go.dev/text/template

View File

@@ -61,12 +61,6 @@ In this example, we can run `cd <service>` and `task up` and as long as the
`<service>` directory contains a `docker-compose.yml`, the Docker composition `<service>` directory contains a `docker-compose.yml`, the Docker composition
will be brought up. will be brought up.
:::info
`.USER_WORKING_DIR` will contain the value of the `--dir` (`-d`) flag, if given.
:::
### Running a global Taskfile ### Running a global Taskfile
If you call Task with the `--global` (alias `-g`) flag, it will look for your If you call Task with the `--global` (alias `-g`) flag, it will look for your
@@ -787,7 +781,10 @@ tasks:
If you prefer these check to be made by the modification timestamp of the files, If you prefer these check to be made by the modification timestamp of the files,
instead of its checksum (content), just set the `method` property to instead of its checksum (content), just set the `method` property to
`timestamp`. `timestamp`. This can be done at two levels:
At the task level for a specific task:
```yaml ```yaml
version: '3' version: '3'
@@ -803,6 +800,24 @@ tasks:
method: timestamp method: timestamp
``` ```
At the root level of the Taskfile to apply it globally to all tasks:
```yaml
version: '3'
method: timestamp # Will be the default for all tasks
tasks:
build:
cmds:
- go build .
sources:
- ./*.go
generates:
- app{{exeExt}}
```
In situations where you need more flexibility the `status` keyword can be used. In situations where you need more flexibility the `status` keyword can be used.
You can even combine the two. See the documentation for You can even combine the two. See the documentation for
[status](#using-programmatic-checks-to-indicate-a-task-is-up-to-date) for an [status](#using-programmatic-checks-to-indicate-a-task-is-up-to-date) for an
@@ -1122,14 +1137,14 @@ tasks:
MAP: MAP:
map: {A: 1, B: 2, C: 3} map: {A: 1, B: 2, C: 3}
cmds: cmds:
- 'echo {{.STRING}}' # Hello, World! - 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true - 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42 - 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14 - 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3] - 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{.ARRAY.0}}' # 1 - 'echo {{index .ARRAY 0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3] - 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1 - 'echo {{.MAP.A}}' # 1
``` ```
Variables can be set in many places in a Taskfile. When executing Variables can be set in many places in a Taskfile. When executing
@@ -2366,6 +2381,21 @@ if called by another task, either directly or as a dependency.
::: :::
:::caution
The watcher can misbehave in certain scenarios, in particular for long-running
servers.
There is a known bug where child processes of the running might not be killed
appropriately. It's adviced to avoid running commands as `go run` and prefer
`go build [...] && ./binary` instead.
If you are having issues, you might want to try tools specifically designed for
live-reloading, like [Air](https://github.com/air-verse/air/). Also, be sure to
[report any issues](https://github.com/go-task/task/issues/new?template=bug_report.yml)
to us.
:::
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[gotemplate]: https://golang.org/pkg/text/template/ [gotemplate]: https://golang.org/pkg/text/template/
[templating-reference]: ./reference/templating.mdx [templating-reference]: ./reference/templating.mdx

View File

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

View File

@@ -64,21 +64,15 @@ get_binaries() {
case "$PLATFORM" in case "$PLATFORM" in
darwin/amd64) BINARIES="task" ;; darwin/amd64) BINARIES="task" ;;
darwin/arm64) BINARIES="task" ;; darwin/arm64) BINARIES="task" ;;
darwin/armv5) BINARIES="task" ;; darwin/arm) BINARIES="task" ;;
darwin/armv6) BINARIES="task" ;;
darwin/armv7) BINARIES="task" ;;
linux/386) BINARIES="task" ;; linux/386) BINARIES="task" ;;
linux/amd64) BINARIES="task" ;; linux/amd64) BINARIES="task" ;;
linux/arm64) BINARIES="task" ;; linux/arm64) BINARIES="task" ;;
linux/armv5) BINARIES="task" ;; linux/arm) BINARIES="task" ;;
linux/armv6) BINARIES="task" ;;
linux/armv7) BINARIES="task" ;;
windows/386) BINARIES="task" ;; windows/386) BINARIES="task" ;;
windows/amd64) BINARIES="task" ;; windows/amd64) BINARIES="task" ;;
windows/arm64) BINARIES="task" ;; windows/arm64) BINARIES="task" ;;
windows/armv5) BINARIES="task" ;; windows/arm) BINARIES="task" ;;
windows/armv6) BINARIES="task" ;;
windows/armv7) 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" 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 exit 1

View File

@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Taskrc YAML Schema",
"description": "Schema for .taskrc files.",
"type": "object",
"properties": {
"experiments": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
},
"additionalProperties": false
}

View File

@@ -0,0 +1,760 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Taskfile YAML Schema",
"description": "Schema for Taskfile files.",
"definitions": {
"env": {
"$ref": "#/definitions/vars"
},
"platforms": {
"type": "array",
"items": {
"type": "string"
}
},
"tasks": {
"type": "object",
"patternProperties": {
"^.*$": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/task_call"
},
{
"$ref": "#/definitions/defer_task_call"
},
{
"$ref": "#/definitions/defer_cmd_call"
}
]
}
},
{
"$ref": "#/definitions/task"
}
]
}
}
},
"task": {
"type": "object",
"additionalProperties": false,
"properties": {
"cmds": {
"description": "A list of commands to be executed.",
"$ref": "#/definitions/cmds"
},
"cmd": {
"description": "The command to be executed.",
"$ref": "#/definitions/cmd"
},
"deps": {
"description": "A list of dependencies of this task. Tasks defined here will run in parallel before this task.",
"$ref": "#/definitions/deps"
},
"label": {
"description": "Overrides the name of the task in the output when a task is run. Supports variables.",
"type": "string"
},
"desc": {
"description": "A short description of the task. This is displayed when calling `task --list`.",
"type": "string"
},
"prompt": {
"description": "One or more prompts that will be presented before a task is run. Declining will cancel running the current and any subsequent tasks.",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"summary": {
"description": "A longer description of the task. This is displayed when calling `task --summary [task]`.",
"type": "string"
},
"aliases": {
"description": "A list of alternative names by which the task can be called.",
"type": "array",
"items": {
"type": "string"
}
},
"sources": {
"description": "A list of sources to check before running this task. Relevant for `checksum` and `timestamp` methods. Can be file paths or star globs.",
"type": "array",
"items": {
"$ref": "#/definitions/glob"
}
},
"generates": {
"description": "A list of files meant to be generated by this task. Relevant for `timestamp` method. Can be file paths or star globs.",
"type": "array",
"items": {
"$ref": "#/definitions/glob"
}
},
"status": {
"description": "A list of commands to check if this task should run. The task is skipped otherwise. This overrides `method`, `sources` and `generates`.",
"type": "array",
"items": {
"type": "string"
}
},
"preconditions": {
"description": "A list of commands to check if this task should run. If a condition is not met, the task will error.",
"type": "array",
"items": {
"$ref": "#/definitions/precondition"
}
},
"dir": {
"description": "The directory in which this task should run. Defaults to the current working directory.",
"type": "string"
},
"set": {
"description": "Enables POSIX shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/shopt"
}
},
"vars": {
"description": "A set of variables that can be used in the task.",
"$ref": "#/definitions/vars"
},
"env": {
"description": "A set of environment variables that will be made available to shell commands.",
"$ref": "#/definitions/env"
},
"dotenv": {
"description": "A list of `.env` file paths to be parsed.",
"type": "array",
"items": {
"type": "string"
}
},
"silent": {
"description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. When combined with the `--list` flag, task descriptions will be hidden.",
"type": "boolean",
"default": false
},
"interactive": {
"description": "Tells task that the command is interactive.",
"type": "boolean",
"default": false
},
"internal": {
"description": "Stops a task from being callable on the command line. It will also be omitted from the output when used with `--list`.",
"type": "boolean",
"default": false
},
"method": {
"description": "Defines which method is used to check the task is up-to-date. `timestamp` will compare the timestamp of the sources and generates files. `checksum` will check the checksum (You probably want to ignore the .task folder in your .gitignore file). `none` skips any validation and always run the task.",
"type": "string",
"enum": ["none", "checksum", "timestamp"],
"default": "none"
},
"prefix": {
"description": "Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`.",
"type": "string"
},
"ignore_error": {
"description": "Continue execution if errors happen while executing commands.",
"type": "boolean"
},
"run": {
"description": "Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`.",
"$ref": "#/definitions/run"
},
"platforms": {
"description": "Specifies which platforms the task should be run on.",
"$ref": "#/definitions/platforms"
},
"requires": {
"description": "A list of variables which should be set if this task is to run, if any of these variables are unset the task will error and not run",
"$ref": "#/definitions/requires_obj"
},
"watch": {
"description": "Configures a task to run in watch mode automatically.",
"type": "boolean",
"default": false
}
}
},
"cmds": {
"type": "array",
"items": {
"$ref": "#/definitions/cmd"
}
},
"cmd": {
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/cmd_call"
},
{
"$ref": "#/definitions/task_call"
},
{
"$ref": "#/definitions/defer_task_call"
},
{
"$ref": "#/definitions/defer_cmd_call"
},
{
"$ref": "#/definitions/for_cmds_call"
}
]
},
"deps": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/task_call"
},
{
"$ref": "#/definitions/for_deps_call"
}
]
}
},
"set": {
"type": "string",
"enum": [
"allexport",
"a",
"errexit",
"e",
"noexec",
"n",
"noglob",
"f",
"nounset",
"u",
"xtrace",
"x",
"pipefail"
]
},
"shopt": {
"type": "string",
"enum": ["expand_aliases", "globstar", "nullglob"]
},
"vars": {
"type": "object",
"patternProperties": {
"^.*$": {
"anyOf": [
{
"type": ["boolean", "integer", "null", "number", "string", "array"]
},
{
"$ref": "#/definitions/var_subkey"
}
]
}
}
},
"var_subkey": {
"type": "object",
"properties": {
"sh": {
"type": "string",
"description": "The value will be treated as a command and the output assigned to the variable"
},
"ref": {
"type": "string",
"description": "The value will be used to lookup the value of another variable which will then be assigned to this variable"
},
"map": {
"type": "object",
"description": "The value will be treated as a literal map type and stored in the variable"
}
},
"additionalProperties": false
},
"task_call": {
"type": "object",
"properties": {
"task": {
"description": "Name of the task to run",
"type": "string"
},
"vars": {
"description": "Values passed to the task called",
"$ref": "#/definitions/vars"
},
"silent": {
"description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.",
"type": "boolean"
}
},
"additionalProperties": false,
"required": ["task"]
},
"cmd_call": {
"type": "object",
"properties": {
"cmd": {
"description": "Command to run",
"type": "string"
},
"silent": {
"description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean"
},
"set": {
"description": "Enables POSIX shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/set"
}
},
"shopt": {
"description": "Enables Bash shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/shopt"
}
},
"ignore_error": {
"description": "Prevent command from aborting the execution of task even after receiving a status code of 1",
"type": "boolean"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"$ref": "#/definitions/platforms"
}
},
"additionalProperties": false,
"required": ["cmd"]
},
"defer_task_call": {
"type": "object",
"properties": {
"defer": {
"description": "Run a command when the task completes. This command will run even when the task fails",
"anyOf": [
{
"$ref": "#/definitions/task_call"
}
]
}
},
"additionalProperties": false,
"required": ["defer"]
},
"defer_cmd_call": {
"type": "object",
"properties": {
"defer": {
"description": "Name of the command to defer",
"type": "string"
},
"silent": {
"description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.",
"type": "boolean"
}
},
"additionalProperties": false,
"required": ["defer"]
},
"for_cmds_call": {
"type": "object",
"properties": {
"for": {
"$ref": "#/definitions/for"
},
"cmd": {
"description": "Command to run",
"type": "string"
},
"silent": {
"description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean"
},
"task": {
"description": "Task to run",
"type": "string"
},
"vars": {
"description": "Values passed to the task called",
"$ref": "#/definitions/vars"
},
"platforms": {
"description": "Specifies which platforms the command should be run on.",
"$ref": "#/definitions/platforms"
}
},
"oneOf": [
{"required": ["cmd"]},
{"required": ["task"]}
],
"additionalProperties": false,
"required": ["for"]
},
"for_deps_call": {
"type": "object",
"properties": {
"for": {
"$ref": "#/definitions/for"
},
"silent": {
"description": "Silent mode disables echoing of command before Task runs it",
"type": "boolean"
},
"task": {
"description": "Task to run",
"type": "string"
},
"vars": {
"description": "Values passed to the task called",
"$ref": "#/definitions/vars"
}
},
"oneOf": [
{"required": ["cmd"]},
{"required": ["task"]}
],
"additionalProperties": false,
"required": ["for"]
},
"for": {
"anyOf": [
{
"$ref": "#/definitions/for_list"
},
{
"$ref": "#/definitions/for_attribute"
},
{
"$ref": "#/definitions/for_var"
},
{
"$ref": "#/definitions/for_matrix"
}
]
},
"for_list": {
"description": "A list of values to iterate over",
"type": "array",
"items": {
"type": "string"
}
},
"for_attribute": {
"description": "The task attribute to iterate over",
"type": "string",
"enum": ["sources", "generates"]
},
"for_var": {
"description": "Which variables to iterate over. The variable will be split using any whitespace character by default. This can be changed by using the `split` attribute.",
"type": "object",
"properties": {
"var": {
"description": "Name of the variable to iterate over",
"type": "string"
},
"split": {
"description": "String to split the variable on",
"type": "string"
},
"as": {
"description": "What the loop variable should be named",
"default": "ITEM",
"type": "string"
}
},
"additionalProperties": false,
"required": ["var"]
},
"for_matrix": {
"description": "A matrix of values to iterate over",
"type": "object",
"additionalProperties": true,
"required": ["matrix"]
},
"precondition": {
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/precondition_obj"
}
]
},
"precondition_obj": {
"type": "object",
"properties": {
"sh": {
"description": "Command to run. If that command returns 1, the condition will fail",
"type": "string"
},
"msg": {
"description": "Failure message to display when the condition fails",
"type": "string"
}
},
"additionalProperties": false
},
"glob": {
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/glob_obj"
}
]
},
"glob_obj": {
"type": "object",
"properties": {
"exclude": {
"description": "File or glob pattern to exclude from the list",
"type": "string"
}
},
"additionalProperties": false
},
"run": {
"type": "string",
"enum": ["always", "once", "when_changed"]
},
"outputString": {
"type": "string",
"enum": ["interleaved", "prefixed", "group"],
"default": "interleaved"
},
"outputObject": {
"type": "object",
"properties": {
"group": {
"type": "object",
"properties": {
"begin": {
"type": "string"
},
"end": {
"type": "string"
},
"error_only": {
"description": "Swallows command output on zero exit code",
"type": "boolean",
"default": false
}
}
}
},
"additionalProperties": false
},
"requires_obj": {
"type": "object",
"properties": {
"vars": {
"description": "List of variables that must be defined for the task to run",
"type": "array",
"items": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"enum": { "type": "array",
"items": { "type": "string" } }
},
"required": ["name", "enum"],
"additionalProperties": false
}
]
}
}
},
"additionalProperties": false
}
},
"allOf": [
{
"type": "object",
"properties": {
"version": {
"description": "Specify the Taskfile format that this file conforms to.",
"oneOf": [
{
"type": "string",
"pattern": "^(0|[1-9]\\d*)(?:\\.(0|[1-9]\\d*))?(?:\\.(0|[1-9]\\d*))?(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
},
{
"type": "number",
"enum": [3]
}
]
},
"output": {
"description": "Defines how the STDOUT and STDERR are printed when running tasks in parallel. The interleaved output prints lines in real time (default). The group output will print the entire output of a command once, after it finishes, so you won't have live feedback for commands that take a long time to run. The prefix output will prefix every line printed by a command with [task-name] as the prefix, but you can customize the prefix for a command with the prefix: attribute.",
"anyOf": [
{ "$ref": "#/definitions/outputString" },
{ "$ref": "#/definitions/outputObject" }
]
},
"method": {
"description": "Defines which method is used to check the task is up-to-date. (default: checksum)",
"type": "string",
"enum": ["none", "checksum", "timestamp"],
"default": "checksum"
},
"includes": {
"description": "Imports tasks from the specified taskfiles. The tasks described in the given Taskfiles will be available with the informed namespace.",
"type": "object",
"patternProperties": {
"^.*$": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"taskfile": {
"description": "The path for the Taskfile or directory to be included. If a directory, Task will look for files named `Taskfile.yml` or `Taskfile.yaml` inside that directory. If a relative path, resolved relative to the directory containing the including Taskfile.",
"type": "string"
},
"dir": {
"description": "The working directory of the included tasks when run.",
"type": "string"
},
"optional": {
"description": "If `true`, no errors will be thrown if the specified file does not exist.",
"type": "boolean"
},
"flatten": {
"description": "If `true`, the tasks from the included Taskfile will be available in the including Taskfile without a namespace. If a task with the same name already exists in the including Taskfile, an error will be thrown.",
"type": "boolean"
},
"internal": {
"description": "Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`.",
"type": "boolean"
},
"aliases": {
"description": "Alternative names for the namespace of the included Taskfile.",
"type": "array",
"items": {
"type": "string"
}
},
"excludes": {
"description": "A list of tasks to be excluded from inclusion.",
"type": "array",
"items": {
"type": "string"
}
},
"vars": {
"description": "A set of variables to apply to the included Taskfile.",
"$ref": "#/definitions/vars"
},
"checksum": {
"description": "The checksum of the file you expect to include. If the checksum does not match, the file will not be included.",
"type": "string"
}
}
}
]
}
}
},
"vars": {
"description": "A set of global variables.",
"$ref": "#/definitions/vars"
},
"env": {
"description": "A set of global environment variables.",
"$ref": "#/definitions/env"
},
"tasks": {
"description": "A set of task definitions.",
"$ref": "#/definitions/tasks"
},
"silent": {
"description": "Default 'silent' options for this Taskfile. If `false`, can be overridden with `true` in a task by task basis.",
"type": "boolean"
},
"set": {
"description": "Enables POSIX shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/set"
}
},
"shopt": {
"description": "Enables Bash shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
"type": "array",
"items": {
"$ref": "#/definitions/shopt"
}
},
"dotenv": {
"type": "array",
"description": "A list of `.env` file paths to be parsed.",
"items": {
"type": "string"
}
},
"run": {
"description": "Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`.",
"$ref": "#/definitions/run"
},
"interval": {
"description": "Sets a different watch interval when using `--watch`, the default being 100 milliseconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
"type": "string",
"pattern": "^[0-9]+(?:m|s|ms)$"
}
},
"additionalProperties": false,
"required": ["version"],
"anyOf": [
{
"required": ["includes"]
},
{
"required": ["tasks"]
},
{
"required": ["includes", "tasks"]
}
]
}
]
}

View File

@@ -684,6 +684,10 @@
"vars": { "vars": {
"description": "A set of variables to apply to the included Taskfile.", "description": "A set of variables to apply to the included Taskfile.",
"$ref": "#/definitions/vars" "$ref": "#/definitions/vars"
},
"checksum": {
"description": "The checksum of the file you expect to include. If the checksum does not match, the file will not be included.",
"type": "string"
} }
} }
} }

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