Compare commits

...

47 Commits

Author SHA1 Message Date
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
81 changed files with 3560 additions and 2447 deletions

View File

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

View File

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

2
.nvmrc
View File

@@ -1 +1 @@
22.14.0
22.16.0

View File

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

View File

@@ -98,6 +98,15 @@ tasks:
cmds:
- golangci-lint run --fix
format:
desc: Runs golangci-lint and formats any Go files
aliases: [fmt, f]
sources:
- './**/*.go'
- .golangci.yml
cmds:
- golangci-lint fmt
sleepit:build:
desc: Builds the sleepit test helper
sources:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -937,3 +937,44 @@ 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"),
)
}
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(),
WithPostProcessFn(PPRemoveAbsolutePaths),
)
}

View File

@@ -218,3 +218,23 @@ func TestListDescInterpolation(t *testing.T) {
}),
)
}
func TestJsonListFormat(t *testing.T) {
t.Parallel()
fp, err := filepath.Abs("testdata/json_list_format/Taskfile.yml")
require.NoError(t, err)
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/json_list_format"),
),
WithListOptions(task.ListOptions{
FormatTaskListAsJSON: true,
}),
WithFixtureTemplateData(struct {
TaskfileLocation string
}{
TaskfileLocation: fp,
}),
)
}

9
go.mod
View File

@@ -5,7 +5,7 @@ go 1.23.0
require (
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.16.0
github.com/alecthomas/chroma/v2 v2.18.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0
@@ -16,6 +16,7 @@ require (
github.com/go-git/go-git/v5 v5.16.0
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.1.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.1
@@ -25,8 +26,8 @@ require (
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.13.0
golang.org/x/term v0.31.0
golang.org/x/sync v0.14.0
golang.org/x/term v0.32.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.11.0
)
@@ -55,6 +56,6 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

46
go.sum
View File

@@ -7,16 +7,12 @@ github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA=
github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -25,8 +21,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@@ -36,8 +30,6 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
@@ -50,8 +42,6 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -62,10 +52,6 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
@@ -78,6 +64,8 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -146,21 +134,15 @@ github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -170,18 +152,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

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

View File

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

View File

@@ -90,15 +90,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return r.Run(ctx, p)
}
func escape(s string) string {
s = filepath.ToSlash(s)
s = strings.ReplaceAll(s, " ", `\ `)
s = strings.ReplaceAll(s, "&", `\&`)
s = strings.ReplaceAll(s, "(", `\(`)
s = strings.ReplaceAll(s, ")", `\)`)
return s
}
// ExpandLiteral is a wrapper around [expand.Literal]. It will escape the input
// string, expand any shell symbols (such as '~') and resolve any environment
// variables.
@@ -106,25 +97,17 @@ func ExpandLiteral(s string) (string, error) {
if s == "" {
return "", nil
}
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
word, err := p.Document(strings.NewReader(s))
if err != nil {
return "", err
}
if len(words) == 0 {
return "", nil
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
}
return expand.Literal(cfg, words[0])
return expand.Literal(cfg, word)
}
// ExpandFields is a wrapper around [expand.Fields]. It will escape the input
@@ -132,7 +115,6 @@ func ExpandLiteral(s string) (string, error) {
// variables. It also expands brace expressions ({a.b}) and globs (*/**) and
// returns the results as a list of strings.
func ExpandFields(s string) ([]string, error) {
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
@@ -146,6 +128,7 @@ func ExpandFields(s string) ([]string, error) {
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
NullGlob: true,
}
return expand.Fields(cfg, words...)
}

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
3.43.2
3.44.0

2
package-lock.json generated
View File

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

View File

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

View File

@@ -95,7 +95,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
}
func (e *Executor) setupFuzzyModel() {
if e.Taskfile != nil {
if e.Taskfile == nil {
return
}

View File

@@ -8,6 +8,9 @@ import (
"slices"
"sync/atomic"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
@@ -19,9 +22,6 @@ import (
"github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
)
const (

View File

@@ -42,9 +42,10 @@ type (
FormatterTestOption
}
TaskTest struct {
name string
experiments map[*experiments.Experiment]int
postProcessFns []PostProcessFn
name string
experiments map[*experiments.Experiment]int
postProcessFns []PostProcessFn
fixtureTemplateData any
}
)
@@ -79,7 +80,11 @@ func (tt *TaskTest) writeFixture(
if goldenFileSuffix != "" {
goldenFileName += "-" + goldenFileSuffix
}
g.Assert(t, goldenFileName, b)
if tt.fixtureTemplateData != nil {
g.AssertWithTemplate(t, goldenFileName, tt.fixtureTemplateData, b)
} else {
g.Assert(t, goldenFileName, b)
}
}
// writeFixtureBuffer is a wrapper for writing the main output of the task to a
@@ -234,6 +239,26 @@ func (opt *setupErrorTestOption) applyToFormatterTest(t *FormatterTest) {
t.wantSetupError = true
}
// WithFixtureTemplateData sets up data defined in the golden file using golang
// template. Useful if the golden file can change depending on the test.
// Example template: {{ .Value }}
// Example data definition: struct{ Value string }{Value: "value"}
func WithFixtureTemplateData(data any) TestOption {
return &fixtureTemplateDataTestOption{data: data}
}
type fixtureTemplateDataTestOption struct {
data any
}
func (opt *fixtureTemplateDataTestOption) applyToExecutorTest(t *ExecutorTest) {
t.fixtureTemplateData = opt.data
}
func (opt *fixtureTemplateDataTestOption) applyToFormatterTest(t *FormatterTest) {
t.fixtureTemplateData = opt.data
}
// Post-processing
// A PostProcessFn is a function that can be applied to the output of a test
@@ -702,6 +727,7 @@ func TestIncludesRemote(t *testing.T) {
enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)
dir := "testdata/includes_remote"
os.RemoveAll(filepath.Join(dir, ".task", "remote"))
srv := httptest.NewServer(http.FileServer(http.Dir(dir)))
defer srv.Close()
@@ -777,8 +803,8 @@ func TestIncludesRemote(t *testing.T) {
},
}
for j, e := range executors {
t.Run(fmt.Sprint(j), func(t *testing.T) {
for _, e := range executors {
t.Run(e.name, func(t *testing.T) {
require.NoError(t, e.executor.Setup())
for k, taskCall := range taskCalls {
@@ -933,6 +959,7 @@ func TestIncludesHttp(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
task, err := e.CompiledTask(&task.Call{Task: tc.name})
require.NoError(t, err)
assert.Equal(t, tc.dir, task.Dir)
@@ -1964,10 +1991,6 @@ task: [included3:task1] echo "VAR_1 is included-default-var1"
VAR_1 is included-default-var1
task: [included3:task1] echo "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"}))
t.Log(buff.String())
@@ -2151,7 +2174,7 @@ func TestUserWorkingDirectory(t *testing.T) {
var buff bytes.Buffer
e := task.NewExecutor(
task.WithEntrypoint("testdata/user_working_dir/Taskfile.yml"),
task.WithDir("testdata/user_working_dir"),
task.WithStdout(&buff),
task.WithStderr(&buff),
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -249,7 +249,8 @@ func (r *Reader) include(ctx context.Context, node Node) error {
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Excludes: include.Excludes,
Vars: templater.ReplaceVars(include.Vars, cache),
Vars: include.Vars,
Checksum: include.Checksum,
}
if err := cache.Err(); err != nil {
return err
@@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
WithParent(node),
WithChecksum(include.Checksum),
)
if err != nil {
if include.Optional {
@@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
if node, isRemote := node.(RemoteNode); isRemote {
return r.readRemoteNodeContent(ctx, node)
}
return node.Read()
// Read the Taskfile
b, err := node.Read()
if err != nil {
return nil, err
}
// If the given checksum doesn't match the sum pinned in the Taskfile
checksum := checksum(b)
if !node.Verify(checksum) {
return nil, &errors.TaskfileDoesNotMatchChecksum{
URI: node.Location(),
ExpectedChecksum: node.Checksum(),
ActualChecksum: checksum,
}
}
return b, nil
}
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
@@ -427,17 +446,29 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
}
r.debugf("found remote file at %q\n", node.Location())
checksum := checksum(downloadedBytes)
prompt := cache.ChecksumPrompt(checksum)
// Prompt the user if required
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()}
// If the given checksum doesn't match the sum pinned in the Taskfile
checksum := checksum(downloadedBytes)
if !node.Verify(checksum) {
return nil, &errors.TaskfileDoesNotMatchChecksum{
URI: node.Location(),
ExpectedChecksum: node.Checksum(),
ActualChecksum: checksum,
}
}
// If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" {
// Prompt the user if required
prompt := cache.ChecksumPrompt(checksum)
if prompt != "" {
if err := func() error {
r.promptMutex.Lock()
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
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
// Request the given URL
@@ -49,7 +49,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
if ctx.Err() != nil {
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
}
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
defer resp.Body.Close()
@@ -61,7 +61,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
return strings.Contains(contentType, s)
}) {
return u, nil
return &u, nil
}
// If the request was not successful, append the default Taskfile names to
@@ -78,7 +78,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
// Try the alternative URL
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
defer resp.Body.Close()
@@ -88,5 +88,5 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
}
}
return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
return nil, errors.TaskfileNotFoundError{URI: u.Redacted(), Walk: false}
}

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

@@ -0,0 +1,4 @@
version: 3
tasks:
install: echo 'install'

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,2 @@
task: [install] echo 'install'
install

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
task: The checksum of the Taskfile at "/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": "{{ .TaskfileLocation }}"
}
}
],
"location": "{{ .TaskfileLocation }}"
}

View File

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

View File

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

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/fsnotifyext"
"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
@@ -71,12 +73,9 @@ func (e *Executor) watchTasks(calls ...*Call) error {
for {
select {
case event, ok := <-eventsChan:
switch {
case !ok:
if !ok {
cancel()
return
case event.Op == fsnotify.Chmod:
continue
}
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 {
c := c
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)
if err != nil {
e.Logger.Errf(logger.Red, "%v\n", err)
return
}
baseDir := filepathext.SmartJoin(e.Dir, t.Dir)
files, err := fingerprint.Globs(baseDir, t.Sources)
files, err := e.collectSources(calls)
if err != nil {
e.Logger.Errf(logger.Red, "%v\n", err)
return
}
if !event.Has(fsnotify.Remove) && !slices.Contains(files, event.Name) {
relPath, _ := filepath.Rel(baseDir, event.Name)
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 {
var registerTaskDirs func(*Call) error
registerTaskDirs = func(c *Call) error {
task, err := e.CompiledTask(c)
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
files, err := e.collectSources(calls)
if err != nil {
return err
}
for _, c := range calls {
if err := registerTaskDirs(c); err != nil {
for _, f := range files {
d := filepath.Dir(f)
if isSet, ok := e.watchedDirs.Load(d); ok && isSet {
continue
}
if ShouldIgnore(d) {
continue
}
if err := w.Add(d); err != nil {
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
}
func ShouldIgnoreFile(path string) bool {
ignorePaths := []string{
"/.task",
"/.git",
"/.hg",
"/node_modules",
}
var ignorePaths = []string{
"/.task",
"/.git",
"/.hg",
"/node_modules",
}
func ShouldIgnore(path string) bool {
for _, p := range ignorePaths {
if strings.Contains(path, fmt.Sprintf("%s/", p)) || strings.HasSuffix(path, p) {
return true
@@ -227,3 +202,47 @@ func ShouldIgnoreFile(path string) bool {
}
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 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
`)
var buff bytes.Buffer
e := task.NewExecutor(
task.ExecutorWithDir(dir),
task.ExecutorWithStdout(&buff),
task.ExecutorWithStderr(&buff),
task.ExecutorWithWatch(true),
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithWatch(true),
)
require.NoError(t, e.Setup())
@@ -49,10 +50,10 @@ task: task "default" finished running
dirPath := filepathext.SmartJoin(dir, "src")
filePath := filepathext.SmartJoin(dirPath, "a")
err := os.MkdirAll(dirPath, 0755)
err := os.MkdirAll(dirPath, 0o755)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("test"), 0644)
err = os.WriteFile(filePath, []byte("test"), 0o644)
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -71,16 +72,16 @@ task: task "default" finished running
}
}()
time.Sleep(10 * time.Millisecond)
err = os.WriteFile(filePath, []byte("test updated"), 0644)
time.Sleep(200 * time.Millisecond)
err = os.WriteFile(filePath, []byte("test updated"), 0o644)
require.NoError(t, err)
time.Sleep(150 * time.Millisecond)
time.Sleep(200 * time.Millisecond)
cancel()
assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
}
func TestShouldIgnoreFile(t *testing.T) {
func TestShouldIgnore(t *testing.T) {
t.Parallel()
tt := []struct {
@@ -95,7 +96,7 @@ func TestShouldIgnoreFile(t *testing.T) {
ct := ct
t.Run(fmt.Sprintf("ignore - %d", k), func(t *testing.T) {
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,41 @@ sidebar_position: 14
# Changelog
## v3.44.0 - 2025-06-08
- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by
@pd93).
- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed
to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a
string). (#2138, #2139, #2140 by @pd93).
- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).
- Added `task` field the `--list --json` output (#2256 by @aleksandersh).
- Added the ability to
[pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)
by specifying a checksum. This works with both local and remote Taskfiles
(#2222, #2223 by @pd93).
- When using the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),
any credentials used in the URL will now be redacted in Task's output (#2100,
#2220 by @pd93).
- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200
by @vmaerten).
- Fixed a bug where taskfiles in directories containing spaces created
directories in the wrong location (#2208, #2216 by @pd93).
- Added support for dual JSON schema files, allowing changes without affecting
the current schema. The current schemas will only be updated during releases.
(#2211 by @vmaerten).
- Improved fingerprint documentation by specifying that the method can be set at
the root level to apply to all tasks (#2233 by @vmaerten).
- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by
@wazazaby, #2271 by @andreynering).
## v3.43.3 - 2025-04-27
Reverted the changes made in #2113 and #2186 that affected the
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
#2208.
## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by

View File

@@ -43,12 +43,16 @@ Studio Code][vscode-task].
## 2. Making changes
- **Code style** - Try to maintain the existing code style where possible. Go
code should be formatted by [`gofumpt`][gofumpt] and linted using
[`golangci-lint`][golangci-lint]. Any Markdown or TypeScript files should be
formatted 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 automatically fix any issues that are found.
code should be formatted and linted by [`golangci-lint`][golangci-lint]. This
wraps the [`gofumpt`][gofumpt] and [`gci`][gci] formatters and a number of
linters. We recommend that you take a look at the [golangci-lint
docs][golangci-lint-docs] for a guide on how to setup your editor to
auto-format your code. Any Markdown or TypeScript files should be formatted
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
the [updating documentation](#updating-documentation) section below.
- **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
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
Markdown and is located in the `website/docs` directory. All Markdown documents
should have an 80 character line wrap limit (enforced by Prettier).
[MDX][mdx] (an extension of Markdown) and is located in the `website/docs`
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
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
[go]: https://go.dev
[gofumpt]: https://github.com/mvdan/gofumpt
[gci]: https://github.com/daixiang0/gci
[golangci-lint]: https://golangci-lint.run
[golangci-lint-docs]: https://golangci-lint.run/welcome/integrations/
[prettier]: https://prettier.io
[nodejs]: https://nodejs.org/en/
[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
[discussion]: https://github.com/go-task/task/discussions
[conventional-commits]: https://www.conventionalcommits.org
[mdx]: https://mdxjs.com/
{/* prettier-ignore-end */}

View File

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

View File

@@ -104,6 +104,7 @@ structure:
"tasks": [
{
"name": "",
"task": "",
"desc": "",
"summary": "",
"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`. |
| `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. |
| `checksum` | `string` | | The checksum of the file you expect to include. If the checksum does not match, the file will not be included. |
:::info

View File

@@ -102,7 +102,8 @@ special variable will be overridden.
| 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_SILENT` | A boolean containing whether the `--silent` 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_DIR` | The absolute path of the included Taskfile directory. |
| `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`. |
| `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. |
@@ -269,6 +270,10 @@ description here for completeness. For detailed usage, please refer to the
| `b32enc` | Encodes a string into base 32. |
| `b32dec` | Decodes a string from base 32. |
:::note
YAML encoding functions are [provided directly by Task](#task-functions).
:::
#### [List Functions][list-functions]
| 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. |
| `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]
| Function | Description |
@@ -375,7 +384,7 @@ Lastly, Task itself provides a few functions:
| Function | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |
| `splitLines` | Splits Unix (`\n`) and Windows (`\r\n`) styled newlines. |
| `catLines` | Replaces Unix (`\n`) and Windows (`\r\n`) styled newlines with a space. |
@@ -388,6 +397,11 @@ 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). |
| `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. |
| `fromYaml`\* | Decodes a YAML string into an object. |
| `toYaml`\* | Encodes an object as a YAML string. |
| `uuid` | Generates a new pseudo-random UUIDv4 string. |
| `randInt` | Generates a new pseudo-random, non-negative, 32bit integer. Generated numbers are not suitable for security-sensitive work. |
| `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 */}
[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
will be brought up.
:::info
`.USER_WORKING_DIR` will contain the value of the `--dir` (`-d`) flag, if given.
:::
### Running a global Taskfile
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,
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
version: '3'
@@ -803,6 +800,24 @@ tasks:
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.
You can even combine the two. See the documentation for
[status](#using-programmatic-checks-to-indicate-a-task-is-up-to-date) for an
@@ -1122,14 +1137,14 @@ tasks:
MAP:
map: {A: 1, B: 2, C: 3}
cmds:
- 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{.ARRAY.0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1
- 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{index .ARRAY 0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1
```
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 */}
[gotemplate]: https://golang.org/pkg/text/template/
[templating-reference]: ./reference/templating.mdx

View File

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

View File

@@ -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": {
"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"
}
}
}

View File

@@ -5,6 +5,41 @@ sidebar_position: 14
# Changelog
## v3.44.0 - 2025-06-08
- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by
@pd93).
- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed
to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a
string). (#2138, #2139, #2140 by @pd93).
- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).
- Added `task` field the `--list --json` output (#2256 by @aleksandersh).
- Added the ability to
[pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)
by specifying a checksum. This works with both local and remote Taskfiles
(#2222, #2223 by @pd93).
- When using the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),
any credentials used in the URL will now be redacted in Task's output (#2100,
#2220 by @pd93).
- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200
by @vmaerten).
- Fixed a bug where taskfiles in directories containing spaces created
directories in the wrong location (#2208, #2216 by @pd93).
- Added support for dual JSON schema files, allowing changes without affecting
the current schema. The current schemas will only be updated during releases.
(#2211 by @vmaerten).
- Improved fingerprint documentation by specifying that the method can be set at
the root level to apply to all tasks (#2233 by @vmaerten).
- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by
@wazazaby, #2271 by @andreynering).
## v3.43.3 - 2025-04-27
Reverted the changes made in #2113 and #2186 that affected the
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
#2208.
## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by

View File

@@ -43,12 +43,16 @@ Studio Code][vscode-task].
## 2. Making changes
- **Code style** - Try to maintain the existing code style where possible. Go
code should be formatted by [`gofumpt`][gofumpt] and linted using
[`golangci-lint`][golangci-lint]. Any Markdown or TypeScript files should be
formatted 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 automatically fix any issues that are found.
code should be formatted and linted by [`golangci-lint`][golangci-lint]. This
wraps the [`gofumpt`][gofumpt] and [`gci`][gci] formatters and a number of
linters. We recommend that you take a look at the [golangci-lint
docs][golangci-lint-docs] for a guide on how to setup your editor to
auto-format your code. Any Markdown or TypeScript files should be formatted
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
the [updating documentation](#updating-documentation) section below.
- **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
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
Markdown and is located in the `website/docs` directory. All Markdown documents
should have an 80 character line wrap limit (enforced by Prettier).
[MDX][mdx] (an extension of Markdown) and is located in the `website/docs`
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
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
[go]: https://go.dev
[gofumpt]: https://github.com/mvdan/gofumpt
[gci]: https://github.com/daixiang0/gci
[golangci-lint]: https://golangci-lint.run
[golangci-lint-docs]: https://golangci-lint.run/welcome/integrations/
[prettier]: https://prettier.io
[nodejs]: https://nodejs.org/en/
[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
[discussion]: https://github.com/go-task/task/discussions
[conventional-commits]: https://www.conventionalcommits.org
[mdx]: https://mdxjs.com/
{/* prettier-ignore-end */}

View File

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

View File

@@ -104,6 +104,7 @@ structure:
"tasks": [
{
"name": "",
"task": "",
"desc": "",
"summary": "",
"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`. |
| `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. |
| `checksum` | `string` | | The checksum of the file you expect to include. If the checksum does not match, the file will not be included. |
:::info

View File

@@ -102,7 +102,8 @@ special variable will be overridden.
| 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_SILENT` | A boolean containing whether the `--silent` 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_DIR` | The absolute path of the included Taskfile directory. |
| `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`. |
| `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. |
@@ -269,6 +270,10 @@ description here for completeness. For detailed usage, please refer to the
| `b32enc` | Encodes a string into base 32. |
| `b32dec` | Decodes a string from base 32. |
:::note
YAML encoding functions are [provided directly by Task](#task-functions).
:::
#### [List Functions][list-functions]
| 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. |
| `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]
| Function | Description |
@@ -375,7 +384,7 @@ Lastly, Task itself provides a few functions:
| Function | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |
| `splitLines` | Splits Unix (`\n`) and Windows (`\r\n`) styled newlines. |
| `catLines` | Replaces Unix (`\n`) and Windows (`\r\n`) styled newlines with a space. |
@@ -388,6 +397,11 @@ 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). |
| `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. |
| `fromYaml`\* | Decodes a YAML string into an object. |
| `toYaml`\* | Encodes an object as a YAML string. |
| `uuid` | Generates a new pseudo-random UUIDv4 string. |
| `randInt` | Generates a new pseudo-random, non-negative, 32bit integer. Generated numbers are not suitable for security-sensitive work. |
| `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 */}
[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
will be brought up.
:::info
`.USER_WORKING_DIR` will contain the value of the `--dir` (`-d`) flag, if given.
:::
### Running a global Taskfile
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,
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
version: '3'
@@ -803,6 +800,24 @@ tasks:
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.
You can even combine the two. See the documentation for
[status](#using-programmatic-checks-to-indicate-a-task-is-up-to-date) for an
@@ -1122,14 +1137,14 @@ tasks:
MAP:
map: {A: 1, B: 2, C: 3}
cmds:
- 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{.ARRAY.0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1
- 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{index .ARRAY 0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1
```
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 */}
[gotemplate]: https://golang.org/pkg/text/template/
[templating-reference]: ./reference/templating.mdx

File diff suppressed because it is too large Load Diff