diff --git a/.nvmrc b/.nvmrc index b8ffd70759..5b540673a8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.15.0 +22.16.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index b67c38000d..36d3c91d18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ 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 diff --git a/errors/errors.go b/errors/errors.go index ea3d7ce024..1ed50e8741 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -26,6 +26,7 @@ const ( CodeTaskfileNetworkTimeout CodeTaskfileInvalid CodeTaskfileCycle + CodeTaskfileDoesNotMatchChecksum ) // Task related exit codes diff --git a/errors/errors_taskfile.go b/errors/errors_taskfile.go index cbb160aebe..a2f0bad790 100644 --- a/errors/errors_taskfile.go +++ b/errors/errors_taskfile.go @@ -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 +} diff --git a/executor_test.go b/executor_test.go index 8f3c794c9d..4d1677db21 100644 --- a/executor_test.go +++ b/executor_test.go @@ -958,3 +958,23 @@ func TestFuzzyModel(t *testing.T) { 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), + ) +} diff --git a/formatter_test.go b/formatter_test.go index 51f10d92ff..2db067b24e 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -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, + }), + ) +} diff --git a/go.mod b/go.mod index bb2e51c919..80015ded85 100644 --- a/go.mod +++ b/go.mod @@ -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.17.2 + 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 diff --git a/go.sum b/go.sum index a2e8cb4b2f..ff69f46845 100644 --- a/go.sum +++ b/go.sum @@ -11,10 +11,8 @@ github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNx 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.17.0 h1:3r2Cgk+nXNICMBxIFGnTRTbQFUwMiLisW+9uos0TtUI= -github.com/alecthomas/chroma/v2 v2.17.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= -github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= -github.com/alecthomas/chroma/v2 v2.17.2/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= @@ -143,8 +141,6 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbR golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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.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= @@ -156,13 +152,9 @@ 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.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.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= diff --git a/help.go b/help.go index 02656d137f..2c687a97f4 100644 --- a/help.go +++ b/help.go @@ -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, diff --git a/internal/editors/output.go b/internal/editors/output.go index 42b893a1a4..9a6090b32f 100644 --- a/internal/editors/output.go +++ b/internal/editors/output.go @@ -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"` diff --git a/internal/fsnotifyext/fsnotify_dedup.go b/internal/fsnotifyext/fsnotify_dedup.go index ef9fa9cb1e..d081842380 100644 --- a/internal/fsnotifyext/fsnotify_dedup.go +++ b/internal/fsnotifyext/fsnotify_dedup.go @@ -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) diff --git a/task_test.go b/task_test.go index 711d672416..7b986662cd 100644 --- a/task_test.go +++ b/task_test.go @@ -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 { diff --git a/taskfile/ast/include.go b/taskfile/ast/include.go index 894fd23d1d..6902b5a61d 100644 --- a/taskfile/ast/include.go +++ b/taskfile/ast/include.go @@ -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, } } diff --git a/taskfile/node.go b/taskfile/node.go index 357ba1a522..3dc3344c6c 100644 --- a/taskfile/node.go +++ b/taskfile/node.go @@ -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) } diff --git a/taskfile/node_base.go b/taskfile/node_base.go index 8dd60f831a..7e641efdba 100644 --- a/taskfile/node_base.go +++ b/taskfile/node_base.go @@ -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 +} diff --git a/taskfile/node_cache.go b/taskfile/node_cache.go index b489161b23..0dac811429 100644 --- a/taskfile/node_cache.go +++ b/taskfile/node_cache.go @@ -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, diff --git a/taskfile/node_file.go b/taskfile/node_file.go index 79b7a9572e..aa35437cb6 100644 --- a/taskfile/node_file.go +++ b/taskfile/node_file.go @@ -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 } diff --git a/taskfile/node_git.go b/taskfile/node_git.go index fd15ce4e8a..5152ea7e76 100644 --- a/taskfile/node_git.go +++ b/taskfile/node_git.go @@ -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) } diff --git a/taskfile/node_git_test.go b/taskfile/node_git_test.go index 1b88a083a9..c359ae81be 100644 --- a/taskfile/node_git_test.go +++ b/taskfile/node_git_test.go @@ -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) + } } diff --git a/taskfile/node_http.go b/taskfile/node_http.go index 16e0ee40c9..faa6616db4 100644 --- a/taskfile/node_http.go +++ b/taskfile/node_http.go @@ -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) } diff --git a/taskfile/node_http_test.go b/taskfile/node_http_test.go new file mode 100644 index 0000000000..ade7c905b9 --- /dev/null +++ b/taskfile/node_http_test.go @@ -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) + } +} diff --git a/taskfile/node_stdin.go b/taskfile/node_stdin.go index 387f50fe83..b09a779538 100644 --- a/taskfile/node_stdin.go +++ b/taskfile/node_stdin.go @@ -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 } diff --git a/taskfile/reader.go b/taskfile/reader.go index 3230807639..3f36ad62b2 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -250,6 +250,7 @@ func (r *Reader) include(ctx context.Context, node Node) error { AdvancedImport: include.AdvancedImport, Excludes: include.Excludes, 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()) + + // If the given checksum doesn't match the sum pinned in the Taskfile 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 !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()} + } } } diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go index d08e74ec6a..e209444acc 100644 --- a/taskfile/taskfile.go +++ b/taskfile/taskfile.go @@ -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} } diff --git a/testdata/includes_checksum/correct/Taskfile.yml b/testdata/includes_checksum/correct/Taskfile.yml new file mode 100644 index 0000000000..ef1121dec1 --- /dev/null +++ b/testdata/includes_checksum/correct/Taskfile.yml @@ -0,0 +1,12 @@ +version: '3' + +includes: + included: + taskfile: ../included.yml + internal: true + checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5 + +tasks: + default: + cmds: + - task: included:default diff --git a/testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden b/testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden new file mode 100644 index 0000000000..a2d7af2d34 --- /dev/null +++ b/testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden @@ -0,0 +1,2 @@ +task: [included:default] echo "Hello, World!" +Hello, World! diff --git a/testdata/includes_checksum/correct_remote/Taskfile.yml b/testdata/includes_checksum/correct_remote/Taskfile.yml new file mode 100644 index 0000000000..34a5cda654 --- /dev/null +++ b/testdata/includes_checksum/correct_remote/Taskfile.yml @@ -0,0 +1,12 @@ +version: '3' + +includes: + included: + taskfile: https://taskfile.dev + internal: true + checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9 + +tasks: + default: + cmds: + - task: included:default diff --git a/testdata/includes_checksum/included.yml b/testdata/includes_checksum/included.yml new file mode 100644 index 0000000000..f97528ea21 --- /dev/null +++ b/testdata/includes_checksum/included.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + default: + cmds: + - echo "Hello, World!" diff --git a/testdata/includes_checksum/incorrect/Taskfile.yml b/testdata/includes_checksum/incorrect/Taskfile.yml new file mode 100644 index 0000000000..56c13cca10 --- /dev/null +++ b/testdata/includes_checksum/incorrect/Taskfile.yml @@ -0,0 +1,12 @@ +version: '3' + +includes: + included: + taskfile: ../included.yml + internal: true + checksum: foo + +tasks: + default: + cmds: + - task: included:default diff --git a/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden b/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden new file mode 100644 index 0000000000..58dedf8795 --- /dev/null +++ b/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden @@ -0,0 +1,3 @@ +task: The checksum of the Taskfile at "/testdata/includes_checksum/included.yml" does not match! +got: "c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5" +want: "foo" \ No newline at end of file diff --git a/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden b/testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden new file mode 100644 index 0000000000..e69de29bb2 diff --git a/testdata/json_list_format/Taskfile.yml b/testdata/json_list_format/Taskfile.yml new file mode 100644 index 0000000000..5c7db94b76 --- /dev/null +++ b/testdata/json_list_format/Taskfile.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + foo: + label: "foobar" + desc: "task description" diff --git a/testdata/json_list_format/testdata/TestJsonListFormat.golden b/testdata/json_list_format/testdata/TestJsonListFormat.golden new file mode 100644 index 0000000000..44f0528452 --- /dev/null +++ b/testdata/json_list_format/testdata/TestJsonListFormat.golden @@ -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 }}" +} diff --git a/watch.go b/watch.go index 76bab9ae6c..515bde0af9 100644 --- a/watch.go +++ b/watch.go @@ -71,12 +71,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) diff --git a/website/docs/experiments/remote_taskfiles.mdx b/website/docs/experiments/remote_taskfiles.mdx index b9c2d1f2cb..b815422462 100644 --- a/website/docs/experiments/remote_taskfiles.mdx +++ b/website/docs/experiments/remote_taskfiles.mdx @@ -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 diff --git a/website/docs/reference/cli.mdx b/website/docs/reference/cli.mdx index e5094a9a42..55ab128f6e 100644 --- a/website/docs/reference/cli.mdx +++ b/website/docs/reference/cli.mdx @@ -104,6 +104,7 @@ structure: "tasks": [ { "name": "", + "task": "", "desc": "", "summary": "", "up_to_date": false, diff --git a/website/docs/reference/schema.mdx b/website/docs/reference/schema.mdx index f426e3faab..8339f5100e 100644 --- a/website/docs/reference/schema.mdx +++ b/website/docs/reference/schema.mdx @@ -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 diff --git a/website/static/Taskfile.yml b/website/static/Taskfile.yml index e4712ffc33..c485182b88 100644 --- a/website/static/Taskfile.yml +++ b/website/static/Taskfile.yml @@ -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}}"' diff --git a/website/static/next-schema.json b/website/static/next-schema.json index 0a942697d6..0a229bedc9 100644 --- a/website/static/next-schema.json +++ b/website/static/next-schema.json @@ -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" } } } diff --git a/website/yarn.lock b/website/yarn.lock index c5306e80e9..4a39c3d87b 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2444,9 +2444,9 @@ csstype "^3.0.2" "@types/react@^19.0.0": - version "19.1.4" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.4.tgz#4d125f014d6ac26b4759775698db118701e314fe" - integrity sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g== + version "19.1.5" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.5.tgz#9feb3bdeb506d0c79d8533b6ebdcacdbcb4756db" + integrity sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g== dependencies: csstype "^3.0.2"