diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d02bd1a..e0b6d469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Unreleased + +- Add support to marking tasks and includes as internal, which will hide them + from `--list` and `--list-all` + ([#818](https://github.com/go-task/task/pull/818)). + +## v3.15.2 - 2022-09-08 + +- Fix error when using variable in `env:` introduced in the previous release + ([#858](https://github.com/go-task/task/issues/858), [#866](https://github.com/go-task/task/pull/866)). +- Fix handling of `CLI_ARGS` (`--`) in Bash completion + ([#863](https://github.com/go-task/task/pull/863)). +- On zsh completion, add ability to replace `--list-all` with `--list` as + already possible on the Bash completion + ([#861](https://github.com/go-task/task/pull/861)). + ## v3.15.0 - 2022-09-03 - Add new special variables `ROOT_DIR` and `TASKFILE_DIR`. This was a highly diff --git a/Taskfile.yml b/Taskfile.yml index c218ff3e..16fe4dc5 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -80,6 +80,19 @@ tasks: cmds: - goreleaser --snapshot --rm-dist + docs:changelog: + desc: Copy CHANGELOG.md to the documentation website + vars: + FILE: docs/docs/changelog.md + cmds: + - rm {{.FILE}} + - 'echo "---" >> {{.FILE}}' + - 'echo "slug: /changelog/" >> {{.FILE}}' + - 'echo "sidebar_position: 6" >> {{.FILE}}' + - 'echo "---" >> {{.FILE}}' + - 'echo "" >> {{.FILE}}' + - 'cat CHANGELOG.md >> {{.FILE}}' + packages: cmds: - echo '{{.GO_PACKAGES}}' diff --git a/completion/bash/task.bash b/completion/bash/task.bash index f1622111..de93e4c8 100644 --- a/completion/bash/task.bash +++ b/completion/bash/task.bash @@ -7,6 +7,18 @@ function _task() local cur prev words cword _init_completion -n : || return + # Check for `--` within command-line and quit or strip suffix. + local i + for i in "${!words[@]}"; do + if [ "${words[$i]}" == "--" ]; then + # Do not complete words following `--` passed to CLI_ARGS. + [ $cword -gt $i ] && return + # Remove the words following `--` to not put --list in CLI_ARGS. + words=( "${words[@]:0:$i}" ) + break + fi + done + # Handle special arguments of options. case "$prev" in -d|--dir) @@ -33,7 +45,7 @@ function _task() esac # Prepare task name completions. - local tasks=( $( "${COMP_WORDS[@]}" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) ) + local tasks=( $( "${words[@]}" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) ) COMPREPLY=( $( compgen -W "${tasks[*]}" -- "$cur" ) ) # Post-process because task names might contain colons. diff --git a/completion/zsh/_task b/completion/zsh/_task index c36a2093..95cde770 100755 --- a/completion/zsh/_task +++ b/completion/zsh/_task @@ -3,6 +3,8 @@ local context state state_descr line typeset -A opt_args +_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}" + # Listing commands from Taskfile.yml function __task_list() { local -a scripts cmd @@ -27,7 +29,7 @@ function __task_list() { (( enabled )) || return 0 scripts=() - for item in "${(@)${(f)$("${cmd[@]}" --list-all)}[2,-1]#\* }"; do + for item in "${(@)${(f)$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION)}[2,-1]#\* }"; do task="${item%%:[[:space:]]*}" desc="${item##[^[:space:]]##[[:space:]]##}" scripts+=( "${task//:/\\:}:$desc" ) diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 39433ef9..e67b721a 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -97,6 +97,7 @@ Some environment variables can be overriden to adjust Task behavior. | `taskfile` | `string` | | 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. | | `dir` | `string` | The parent Taskfile directory | The working directory of the included tasks when run. | | `optional` | `bool` | `false` | If `true`, no errors will be thrown if the specified file does not exist. | +| `internal` | `bool` | `false` | If `true`, tasks will be omitted from both `--list` and `--list-all`. | :::info @@ -119,6 +120,7 @@ includes: | `dir` | `string` | | The current directory which this task should run. | | `method` | `string` | `checksum` | Method used by this task. Default to the one declared globally or `checksum`. Available options: `checksum`, `timestamp` and `none` | | `silent` | `bool` | `false` | Skips some output for this task. Note that STDOUT and STDERR of the commands will still be redirected. | +| `internal` | `bool` | `false` | If `true`, omit this task from both `--list` and `--list-all`. | | `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. | | `prefix` | `string` | | Allows to override the prefix print before the STDOUT. Only relevant when using the `prefixed` output mode. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the commands. | diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 1022bba7..4b9aa4ec 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -5,6 +5,16 @@ sidebar_position: 6 # Changelog +## v3.15.2 - 2022-09-08 + +- Fix error when using variable in `env:` introduced in the previous release + ([#858](https://github.com/go-task/task/issues/858), [#866](https://github.com/go-task/task/pull/866)). +- Fix handling of `CLI_ARGS` (`--`) in Bash completion + ([#863](https://github.com/go-task/task/pull/863)). +- On zsh completion, add ability to replace `--list-all` with `--list` as + already possible on the Bash completion + ([#861](https://github.com/go-task/task/pull/861)). + ## v3.15.0 - 2022-09-03 - Add new special variables `ROOT_DIR` and `TASKFILE_DIR`. This was a highly diff --git a/docs/docs/usage.md b/docs/docs/usage.md index e6dbe7fa..f79f10bf 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -194,6 +194,22 @@ tasks: - echo "This command can still be successfully executed if ./tests/Taskfile.yml does not exist" ``` +### Internal includes + +Includes marked as internal will set all the tasks of the included file to be +internal as well (see the [Internal tasks](#internal-tasks) section below). +This is useful when including utility tasks that are not intended to be used +directly by the user. + +```yaml +version: '3' + +includes: + tests: + taskfile: ./taskfiles/Utils.yml + internal: true +``` + ### Vars of included Taskfiles You can also specify variables when including a Taskfile. This may be useful @@ -223,6 +239,29 @@ use the [default function](https://go-task.github.io/slim-sprig/defaults.html): ::: +## Internal tasks + +Internal tasks are tasks that cannot be called directly by the user. They will +not appear in the output when running `task --list|--list-all`. Other tasks may +call internal tasks in the usual way. This is useful for creating reusable, +function-like tasks that have no useful purpose on the command line. + +```yaml +version: '3' + +tasks: + build-image-1: + cmds: + - task: build-image + vars: + DOCKER_IMAGE: image-1 + + build-image: + internal: true + cmds: + - docker build -t {{.DOCKER_IMAGE}} . +``` + ## Task directory By default, tasks will be executed in the directory where the Taskfile is diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 5bc7dc94..5a3bd8d7 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,8 +1,8 @@ // @ts-check // Note: type annotations allow type checking and IDEs autocompletion -const lightCodeTheme = require('prism-react-renderer/themes/github'); -const darkCodeTheme = require('prism-react-renderer/themes/dracula'); +const lightCodeTheme = require('./src/themes/prismLight'); +const darkCodeTheme = require('./src/themes/prismDark'); const GITHUB_URL = 'https://github.com/go-task/task'; const TWITTER_URL = 'https://twitter.com/taskfiledev'; diff --git a/docs/src/themes/prismDark.js b/docs/src/themes/prismDark.js new file mode 100644 index 00000000..84af2732 --- /dev/null +++ b/docs/src/themes/prismDark.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const darkTheme = require('prism-react-renderer/themes/vsDark/index.cjs.js'); + +module.exports = { + plain: { + color: '#D4D4D4', + backgroundColor: '#212121' + }, + styles: [ + ...darkTheme.styles, + { + types: ['title'], + style: { + color: '#569CD6', + fontWeight: 'bold' + } + }, + { + types: ['property', 'parameter'], + style: { + color: '#9CDCFE' + } + }, + { + types: ['script'], + style: { + color: '#D4D4D4' + } + }, + { + types: ['boolean', 'arrow', 'atrule', 'tag'], + style: { + color: '#569CD6' + } + }, + { + types: ['number', 'color', 'unit'], + style: { + color: '#B5CEA8' + } + }, + { + types: ['font-matter'], + style: { + color: '#CE9178' + } + }, + { + types: ['keyword', 'rule'], + style: { + color: '#C586C0' + } + }, + { + types: ['regex'], + style: { + color: '#D16969' + } + }, + { + types: ['maybe-class-name'], + style: { + color: '#4EC9B0' + } + }, + { + types: ['constant'], + style: { + color: '#4FC1FF' + } + } + ] +}; diff --git a/docs/src/themes/prismLight.js b/docs/src/themes/prismLight.js new file mode 100644 index 00000000..062055d8 --- /dev/null +++ b/docs/src/themes/prismLight.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const lightTheme = require('prism-react-renderer/themes/github/index.cjs.js'); + +module.exports = { + ...lightTheme, + styles: [ + ...lightTheme.styles, + { + types: ['title'], + style: { + color: '#0550AE', + fontWeight: 'bold' + } + }, + { + types: ['parameter'], + style: { + color: '#953800' + } + }, + { + types: ['boolean', 'rule', 'color', 'number', 'constant', 'property'], + style: { + color: '#005CC5' + } + }, + { + types: ['atrule', 'tag'], + style: { + color: '#22863A' + } + }, + { + types: ['script'], + style: { + color: '#24292E' + } + }, + { + types: ['operator', 'unit', 'rule'], + style: { + color: '#D73A49' + } + }, + { + types: ['font-matter', 'string', 'attr-value'], + style: { + color: '#C6105F' + } + }, + { + types: ['class-name'], + style: { + color: '#116329' + } + }, + { + types: ['attr-name'], + style: { + color: '#0550AE' + } + }, + { + types: ['keyword'], + style: { + color: '#CF222E' + } + }, + { + types: ['function'], + style: { + color: '#8250DF' + } + }, + { + types: ['selector'], + style: { + color: '#6F42C1' + } + }, + { + types: ['variable'], + style: { + color: '#E36209' + } + }, + { + types: ['comment'], + style: { + color: '#6B6B6B' + } + } + ] +}; diff --git a/docs/static/img/og-image.png b/docs/static/img/og-image.png index d73f00b2..ea43f9be 100644 Binary files a/docs/static/img/og-image.png and b/docs/static/img/og-image.png differ diff --git a/docs/static/js/carbon.js b/docs/static/js/carbon.js index e0fa2871..f18744a2 100644 --- a/docs/static/js/carbon.js +++ b/docs/static/js/carbon.js @@ -1,6 +1,6 @@ (function () { function attachAd() { - const el = document.createElement('script'); + var el = document.createElement('script'); el.setAttribute('type', 'text/javascript'); el.setAttribute('id', '_carbonads_js'); el.setAttribute( @@ -9,7 +9,7 @@ ); el.setAttribute('async', 'async'); - const wrapper = document.getElementById('sidebar-ads'); + var wrapper = document.getElementById('sidebar-ads'); wrapper.innerHTML = ''; wrapper.appendChild(el); } @@ -17,8 +17,13 @@ setTimeout(function () { attachAd(); - window.addEventListener('popstate', function () { - attachAd(); - }); + var currentPath = window.location.pathname; + + setInterval(function () { + if (currentPath !== window.location.pathname) { + currentPath = window.location.pathname; + attachAd(); + } + }, 1000); }, 1000); })(); diff --git a/errors.go b/errors.go index 483ad565..cc2e65ed 100644 --- a/errors.go +++ b/errors.go @@ -20,6 +20,14 @@ func (err *taskNotFoundError) Error() string { return fmt.Sprintf(`task: Task %q not found`, err.taskName) } +type taskInternalError struct { + taskName string +} + +func (err *taskInternalError) Error() string { + return fmt.Sprintf(`task: Task "%s" is internal`, err.taskName) +} + type TaskRunError struct { taskName string err error diff --git a/help.go b/help.go index 0dc99718..85ce0ce7 100644 --- a/help.go +++ b/help.go @@ -52,7 +52,9 @@ func (e *Executor) printTasks(listAll bool) { func (e *Executor) allTaskNames() (tasks []*taskfile.Task) { tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) for _, task := range e.Taskfile.Tasks { - tasks = append(tasks, task) + if !task.Internal { + tasks = append(tasks, task) + } } sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task }) return @@ -61,7 +63,7 @@ func (e *Executor) allTaskNames() (tasks []*taskfile.Task) { func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) { tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) for _, task := range e.Taskfile.Tasks { - if task.Desc != "" { + if !task.Internal && task.Desc != "" { compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task}) if err == nil { task = compiledTask @@ -92,7 +94,7 @@ func (e *Executor) ListTaskNames(allTasks bool) { // create a string slice from all map values (*taskfile.Task) s := make([]string, 0, len(e.Taskfile.Tasks)) for _, t := range e.Taskfile.Tasks { - if allTasks || t.Desc != "" { + if (allTasks || t.Desc != "") && !t.Internal { s = append(s, strings.TrimRight(t.Task, ":")) } } diff --git a/task.go b/task.go index e3b3b6d1..0a075161 100644 --- a/task.go +++ b/task.go @@ -65,11 +65,16 @@ type Executor struct { func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { // check if given tasks exist for _, c := range calls { - if _, ok := e.Taskfile.Tasks[c.Task]; !ok { + t, ok := e.Taskfile.Tasks[c.Task] + if !ok { // FIXME: move to the main package e.ListTasksWithDesc() return &taskNotFoundError{taskName: c.Task} } + if t.Internal { + e.ListTasksWithDesc() + return &taskInternalError{taskName: c.Task} + } } if e.Summary { diff --git a/task_test.go b/task_test.go index 7ce4e19a..ca3e02d8 100644 --- a/task_test.go +++ b/task_test.go @@ -959,6 +959,86 @@ func TestIncludesRelativePath(t *testing.T) { assert.Contains(t, buff.String(), "testdata/includes_rel_path/common") } +func TestIncludesInternal(t *testing.T) { + const dir = "testdata/internal_task" + tests := []struct { + name string + task string + expectedErr bool + expectedOutput string + }{ + {"included internal task via task", "task-1", false, "Hello, World!\n"}, + {"included internal task via dep", "task-2", false, "Hello, World!\n"}, + { + "included internal direct", + "included:task-3", + true, + "task: No tasks with description available. Try --list-all to list all tasks\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + Silent: true, + } + assert.NoError(t, e.Setup()) + + err := e.Run(context.Background(), taskfile.Call{Task: test.task}) + if test.expectedErr { + assert.Error(t, err, test.expectedErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.expectedOutput, buff.String()) + }) + } +} + +func TestInternalTask(t *testing.T) { + const dir = "testdata/internal_task" + tests := []struct { + name string + task string + expectedErr bool + expectedOutput string + }{ + {"internal task via task", "task-1", false, "Hello, World!\n"}, + {"internal task via dep", "task-2", false, "Hello, World!\n"}, + { + "internal direct", + "task-3", + true, + "task: No tasks with description available. Try --list-all to list all tasks\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var buff bytes.Buffer + e := task.Executor{ + Dir: dir, + Stdout: &buff, + Stderr: &buff, + Silent: true, + } + assert.NoError(t, e.Setup()) + + err := e.Run(context.Background(), taskfile.Call{Task: test.task}) + if test.expectedErr { + assert.Error(t, err, test.expectedErr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.expectedOutput, buff.String()) + }) + } +} + func TestSupportedFileNames(t *testing.T) { fileNames := []string{ "Taskfile.yml", diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go index ec4ade08..fe83bd7d 100644 --- a/taskfile/included_taskfile.go +++ b/taskfile/included_taskfile.go @@ -16,6 +16,7 @@ type IncludedTaskfile struct { Taskfile string Dir string Optional bool + Internal bool AdvancedImport bool Vars *Vars BaseDir string // The directory from which the including taskfile was loaded; used to resolve relative paths @@ -101,6 +102,7 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err Taskfile string Dir string Optional bool + Internal bool Vars *Vars } if err := unmarshal(&includedTaskfile); err != nil { @@ -109,6 +111,7 @@ func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) err it.Taskfile = includedTaskfile.Taskfile it.Dir = includedTaskfile.Dir it.Optional = includedTaskfile.Optional + it.Internal = includedTaskfile.Internal it.AdvancedImport = true it.Vars = includedTaskfile.Vars return nil diff --git a/taskfile/merge.go b/taskfile/merge.go index a5731c71..baebad0e 100644 --- a/taskfile/merge.go +++ b/taskfile/merge.go @@ -9,7 +9,7 @@ import ( const NamespaceSeparator = ":" // Merge merges the second Taskfile into the first -func Merge(t1, t2 *Taskfile, namespaces ...string) error { +func Merge(t1, t2 *Taskfile, internal bool, namespaces ...string) error { if t1.Version != t2.Version { return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) } @@ -43,6 +43,8 @@ func Merge(t1, t2 *Taskfile, namespaces ...string) error { // have serious side-effects in the future, since we're editing // the original references instead of deep copying them. + v.Internal = v.Internal || internal + t1.Tasks[taskNameWithNamespace(k, namespaces...)] = v for _, dep := range v.Deps { diff --git a/taskfile/read/dotenv.go b/taskfile/read/dotenv.go index 70b9dc1d..caed7b1c 100644 --- a/taskfile/read/dotenv.go +++ b/taskfile/read/dotenv.go @@ -27,6 +27,9 @@ func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.V for _, dotEnvPath := range tf.Dotenv { dotEnvPath = tr.Replace(dotEnvPath) + if dotEnvPath == "" { + continue + } dotEnvPath = filepathext.SmartJoin(dir, dotEnvPath) if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) { diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go index 64f68e0c..220b66c4 100644 --- a/taskfile/read/taskfile.go +++ b/taskfile/read/taskfile.go @@ -78,6 +78,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { Taskfile: tr.Replace(includedTask.Taskfile), Dir: tr.Replace(includedTask.Dir), Optional: includedTask.Optional, + Internal: includedTask.Internal, AdvancedImport: includedTask.AdvancedImport, Vars: includedTask.Vars, BaseDir: includedTask.BaseDir, @@ -148,7 +149,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { } } - if err = taskfile.Merge(t, includedTaskfile, namespace); err != nil { + if err = taskfile.Merge(t, includedTaskfile, includedTask.Internal, namespace); err != nil { return err } return nil @@ -164,7 +165,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { if err != nil { return nil, err } - if err = taskfile.Merge(t, osTaskfile); err != nil { + if err = taskfile.Merge(t, osTaskfile, false); err != nil { return nil, err } } diff --git a/taskfile/task.go b/taskfile/task.go index 6fab26af..46548bbf 100644 --- a/taskfile/task.go +++ b/taskfile/task.go @@ -20,6 +20,7 @@ type Task struct { Env *Vars Silent bool Interactive bool + Internal bool Method string Prefix string IgnoreError bool @@ -64,6 +65,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error { Env *Vars Silent bool Interactive bool + Internal bool Method string Prefix string IgnoreError bool `yaml:"ignore_error"` @@ -86,6 +88,7 @@ func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error { t.Env = task.Env t.Silent = task.Silent t.Interactive = task.Interactive + t.Internal = task.Internal t.Method = task.Method t.Prefix = task.Prefix t.IgnoreError = task.IgnoreError diff --git a/testdata/includes_internal/Taskfile.yml b/testdata/includes_internal/Taskfile.yml new file mode 100644 index 00000000..f6c32167 --- /dev/null +++ b/testdata/includes_internal/Taskfile.yml @@ -0,0 +1,15 @@ +version: '3' + +includes: + included: + taskfile: Taskfile2.yml + internal: true + +tasks: + task-1: + cmds: + - task: included:default + + task-2: + deps: + - included:default diff --git a/testdata/includes_internal/Taskfile2.yml b/testdata/includes_internal/Taskfile2.yml new file mode 100644 index 00000000..61869afe --- /dev/null +++ b/testdata/includes_internal/Taskfile2.yml @@ -0,0 +1,6 @@ +version: '3' + +tasks: + task-3: + cmds: + - echo "Hello, World!" diff --git a/testdata/internal_task/Taskfile.yml b/testdata/internal_task/Taskfile.yml new file mode 100644 index 00000000..992cdb70 --- /dev/null +++ b/testdata/internal_task/Taskfile.yml @@ -0,0 +1,15 @@ +version: '3' + +tasks: + task-1: + cmds: + - task: task-3 + + task-2: + deps: + - task-3 + + task-3: + internal: true + cmds: + - echo "Hello, World!" diff --git a/variables.go b/variables.go index 80a232a3..fd2124f0 100644 --- a/variables.go +++ b/variables.go @@ -57,6 +57,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf Env: nil, Silent: origTask.Silent, Interactive: origTask.Interactive, + Internal: origTask.Internal, Method: r.Replace(origTask.Method), Prefix: r.Replace(origTask.Prefix), IgnoreError: origTask.IgnoreError,