Compare commits

...

101 Commits

Author SHA1 Message Date
Pete Davison
88b095020e v3.38.0 2024-06-30 14:50:47 +00:00
Pete Davison
cc14996b71 chore: changelog for #1656 2024-06-28 17:04:02 +00:00
Valentin Maerten
375106c988 fix: list-task with multiline desc (#1656)
* fix: list-task with multiline desc

* feat: display all lines aligned  in a table

* fix: display experiments

* use ladicle/tabwrite to handle color

* delete empty lines

Co-authored-by: Andrey Nering <andrey@nering.com.br>

* remove all /n and replace by space

---------

Co-authored-by: Andrey Nering <andrey@nering.com.br>
2024-06-28 17:59:46 +01:00
Pete Davison
6ce6a38899 chore: changelog for #1639 2024-06-28 16:44:53 +00:00
Valentin Maerten
76030c9146 feat(remote): add a command to clear the cache (#1639)
* feat(remote): add a command to clear the cache

* Update cmd/task/task.go

Co-authored-by: Andrey Nering <andrey@nering.com.br>

* rebase

---------

Co-authored-by: Andrey Nering <andrey@nering.com.br>
2024-06-28 17:42:16 +01:00
Pete Davison
a71020eab5 chore: update PR template to use comments instead of quotes 2024-06-28 16:22:49 +00:00
Pete Davison
6bef2ff8a9 chore: changelog for #1699 2024-06-28 16:16:58 +00:00
Vincent Smith
413dcd28a8 Add verbose/silent variables (#1669) 2024-06-28 17:13:52 +01:00
Pete Davison
da6f5c66a0 chore: changelog for #1636 2024-06-28 16:09:40 +00:00
Valentin Maerten
6012da7a21 feat(remote): prefix checksums/cached files with the filename (#1636)
* feat(remote): add the task filename in the checksum / cache filename

* prefix the filename with the lastDir from the path
2024-06-28 17:07:43 +01:00
Pete Davison
46c5eafe35 chore: changelog for #1661 2024-06-28 16:02:56 +00:00
Valentin Maerten
830b745112 feat(remote): global tempDir when the path is absolute (#1661)
* feat(remote): global tempDir is the path is absolute

* --wip-- [skip ci]

* fix lint

* rename checksum to fingerprint

* chore: Empty-Commit to trigger CI

* feat: add TASK_REMOTE_DIR

* handle relative path for TASK_REMOTE_DIR

* Remove unneedded extra blank lines

Co-authored-by: Andrey Nering <andrey@nering.com.br>

* add docs about TASK_REMOTE_DIR

---------

Co-authored-by: Andrey Nering <andrey@nering.com.br>
2024-06-28 17:01:11 +01:00
Pete Davison
b52d4e4f40 chore: changelog for #1655 2024-06-28 15:53:03 +00:00
Pete Davison
3aaa3223a0 fix: run once in shared dependencies (#1655)
* fix: run once in shared dependencies

* feat: add test
2024-06-28 16:50:02 +01:00
Andrey Nering
a9ff58d0fe chore: add changelog entry for #1699 2024-06-27 11:23:09 -03:00
Meng Zhuo
eeaebaf8c7 chore(goreleaser): release riscv64 binaries on linux (#1699) 2024-06-27 14:19:54 +00:00
dependabot[bot]
2213141fcb chore(deps): bump braces from 3.0.2 to 3.0.3 in /website (#1697)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 14:12:03 +00:00
dependabot[bot]
19956889a7 chore(deps): bump ws from 7.5.9 to 7.5.10 in /website (#1696)
Bumps [ws](https://github.com/websockets/ws) from 7.5.9 to 7.5.10.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 11:06:58 -03:00
Valentin Maerten
4c580ebf18 docs: add pacstall installation methode 2024-06-13 20:57:30 -03:00
Valentin Maerten
3dccde270a docs: improve install script 2024-06-13 20:57:30 -03:00
Pete Davison
53dd0b138a docs: taskfile versions (#1666) 2024-06-13 20:49:21 -03:00
Pete Davison
ea85909e8b chore: update deps 2024-06-09 20:30:43 +00:00
Pete Davison
6bf6fe7ead docs: ETA FAQ 2024-06-09 20:12:24 +00:00
Andrey Nering
f39c6352ac chore(website): make carbon work on blog pages 2024-06-05 21:51:49 -03:00
Andrey Nering
4294cc92b9 chore(website): add stack overflow and answer overflow to page footer 2024-06-05 21:35:15 -03:00
Pete Davison
40d77156df chore: changelog for #1572 2024-06-03 09:40:33 +00:00
Alexander Arvidsson
856ba3b8c2 feat: colorize tasks in prefixed output (#1572)
* feat: Colorize tasks in prefixed output

* chore: comment and style changes

* fix code tag has spaces in api reference

* fix: migrate to use logger for colors

* fix: Add bright colors to the color sequence

* fix: make colorized prefix logger standard
2024-06-03 10:37:24 +01:00
Pete Davison
0810ef01b0 fix: more docs typos 2024-06-03 09:28:53 +00:00
Pete Davison
527bbc3bf5 fix: docs typos/links 2024-06-03 09:06:34 +00:00
Andrey Nering
912bbcab8e chore: make github detect task as a go project again 2024-05-22 18:28:37 -03:00
Pete Davison
aa45491510 chore: changelog for #1663 2024-05-20 21:02:30 +00:00
Valentin Maerten
1e25ceab29 fix: version check (#1663)
* fix: version check

* refactor following review
2024-05-20 21:48:05 +01:00
Pete Davison
a74b0bc679 chore: changelog for #1654 2024-05-16 15:35:21 +00:00
Pete Davison
a3fce1c302 feat: variable references (#1654)
* feat: add references to the base code instead of the maps experiment

* feat: add template functions to ref resolver

* feat: tests

* docs: variable references

* feat: remove json and yaml keys from map variable experiment

* chore: typo
2024-05-16 16:20:59 +01:00
Pete Davison
7958cf50b3 chore: changelog for #1653 2024-05-16 10:16:49 +00:00
Pete Davison
b0efbad591 docs: template reference (#1653)
* chore: deprecation warnings for template functions

* docs: update reference pages
2024-05-16 11:11:52 +01:00
Valentin Maerten
30e9c7d4cd chore: update actions version because node 16 is deprecated (#1650) 2024-05-15 22:59:23 -03:00
dependabot[bot]
baa5e2c378 chore(deps): bump golang.org/x/term from 0.19.0 to 0.20.0 (#1651)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/term/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-15 22:58:26 -03:00
Andrey Nering
cc97e2da1d chore: add changelog for #1619 2024-05-15 22:27:29 -03:00
Andrey Nering
a55e21bbb7 chore: move changelog entries to the right section 🤦‍♂️ 2024-05-15 22:25:14 -03:00
Pete Davison
8d138a5eea feat: better yaml parsing and error handling (#1619) 2024-05-16 01:24:02 +00:00
Andrey Nering
635e3f4e7d chore: add changelog and documentation for #1624 2024-05-15 22:00:49 -03:00
Pete Davison
252d549e3f feat: task executable variable (#1624) 2024-05-15 21:50:18 -03:00
Andrey Nering
182d43e8d8 chore: added changelog for #1657 2024-05-15 21:47:38 -03:00
Pete Davison
f35e51e4e5 feat: better release task 2024-05-15 21:32:33 -03:00
Pete Davison
fb3c64c46e fix: prompt response should go on same line as message 2024-05-15 21:32:33 -03:00
Pete Davison
7535467f45 fix: prompt check shouldn't run if dry flag is true 2024-05-15 21:32:33 -03:00
Pete Davison
3e5cd6cdfd fix: prompt check should come after preconditions and fingerprinting 2024-05-15 21:32:33 -03:00
Pete Davison
dcc060af89 fix: missing additionalProperties false in schema 2024-05-15 21:32:33 -03:00
Pete Davison
55593090fa fix: typo in changelog 2024-05-13 08:54:27 +00:00
Pete Davison
57c094f415 v3.37.2 2024-05-12 19:36:09 +00:00
Pete Davison
2f4876b71c chore: changelog for #1649 2024-05-12 19:33:39 +00:00
Pete Davison
725f929778 fix: included variable merging (#1649) 2024-05-12 20:32:09 +01:00
Pete Davison
8266b28b48 chore: changelog for #1648 2024-05-12 19:27:39 +00:00
Pete Davison
f5c7472f64 fix: nil schema panic (#1648) 2024-05-12 20:25:54 +01:00
Pete Davison
ced3e7a579 fix: var_subkey schema 2024-05-10 16:41:02 +00:00
Orel Lazri
36dd71b122 fix(docs): add references to experiments links (#1644) 2024-05-09 21:30:20 +00:00
Andrey Nering
21531b6291 v3.37.1 2024-05-09 11:22:47 -03:00
Andrey Nering
bfc9d7847d fix: add changelog + fix for booleans for #1641 2024-05-09 11:21:12 -03:00
Valentin Maerten
3397f2855f fix: handle int and float env variable by converting them to string (#1641) 2024-05-09 11:14:38 -03:00
Jordan
78a69c4c3e chore: fix json schema typos (#1642) 2024-05-09 14:11:39 +00:00
Pete Davison
01716f55b3 chore: prep any variables for release (#1586)
* chore: release blog post

* chore: rename blog post to any-variables

* chore: update the release version in the blog

* chore: update blog date
2024-05-09 10:17:03 +01:00
Andrey Nering
ca364c20bb chore(goreleaser): fix deprecation warning 2024-05-08 21:40:50 -03:00
Andrey Nering
ee901fe568 v3.37.0 2024-05-08 21:32:16 -03:00
Pete Davison
7fa06eedf4 chore: changelog and docs for #1623 2024-05-08 15:49:01 +00:00
Pete Davison
651033c5a7 feat: stdin required -t - (#1623) 2024-05-08 16:44:05 +01:00
Valentin Maerten
17f6e816d8 fix(remote): do not display prompt if it's empty (#1634) 2024-05-05 16:10:32 +01:00
Pete Davison
cd259a741f chore: changelog for #1610 2024-04-29 21:32:42 +00:00
Valentin Maerten
c81dbda157 feat(remote): replace env variable in include remote URL (#1610)
* feat(remote): replace env variable in include remote URL

* use templating system instead of os.ExpandEnv

* lint
2024-04-29 22:27:30 +01:00
Michael Zhao
e23ef818ea docs: fix reference to GOOS and GOARCH link (#1628) 2024-04-29 15:01:18 -03:00
Pete Davison
ddd9964db7 feat: warn about move from any variables to map variables (#1618) 2024-04-24 21:40:52 +01:00
Pete Davison
a5b949f5dc chore: changelog for #1612 2024-04-24 19:50:03 +00:00
Pete Davison
630e58767b feat: ability to resolve refs using templating syntax (#1612)
* feat: resolve references using templating syntax

* refactor: moved when references are resolved to one place

* fix: linter

* docs: update map variables doc
2024-04-24 19:47:24 +00:00
Pete Davison
d87e5de56f chore: changelog for #1607 2024-04-24 17:35:48 +00:00
Pete Davison
f75aa1f84b feat: taskfile mutex for adding edge data 2024-04-24 18:33:56 +01:00
Pete Davison
53235f07ad feat: edge weight 2024-04-24 18:33:56 +01:00
Pete Davison
f19c520f23 feat: add support for multiple includes on a graph edge 2024-04-24 18:33:56 +01:00
Pete Davison
6951e5cd0c refactor: includes uses pointers 2024-04-24 18:33:56 +01:00
Andrey Nering
24059a4b76 chore(changelog): add entry for #1613 2024-04-23 22:58:56 -03:00
jwater7
fa022be1f9 chore(completions): support tilde home directory for zsh (#1613) 2024-04-24 01:57:43 +00:00
Andrey Nering
a3b9554efd chore: improve changelog for #1603 2024-04-23 22:49:12 -03:00
Tim Vergenz
16070c7a24 feat: add alias q for template function shellQuote (#1603)
Resolves #1601
2024-04-23 22:47:40 -03:00
Andrey Nering
72d9671fcf chore(website): disable translations for now (#1617) 2024-04-24 01:23:06 +00:00
Pete Davison
d01b3c8979 chore: changelog for #1563 2024-04-09 11:41:28 +00:00
Pete Davison
4024b4fa37 chore: remove code that outputs the graphviz file 2024-04-09 12:37:18 +01:00
Pete Davison
54c7f35b00 fix: linting issues 2024-04-09 12:37:18 +01:00
Pete Davison
3efb437c9a feat: merge concurrency 2024-04-09 12:37:18 +01:00
Pete Davison
e9448bd4be fix: advanced import operates on including file instead of included file 2024-04-09 12:37:18 +01:00
Pete Davison
8f3180a9fa fix: bug with merge code 2024-04-09 12:37:18 +01:00
Pete Davison
1d230af90d fix: advanced import resolving dynamic variables incorrectly 2024-04-09 12:37:18 +01:00
Pete Davison
fb9f6c20ab feat: merger 2024-04-09 12:37:18 +01:00
Pete Davison
6854b4c300 fix: include_with_vars test included the same file multiple times 2024-04-09 12:37:18 +01:00
Pete Davison
b10c573270 fix: missing task locations 2024-04-09 12:37:18 +01:00
Pete Davison
6ecfb634d2 fix: includes interpolation test 2024-04-09 12:37:18 +01:00
Pete Davison
6b3f8e29bb fix: optional includes 2024-04-09 12:37:18 +01:00
Pete Davison
220bf74a9e feat: better taskfile cycle error handling 2024-04-09 12:37:18 +01:00
Pete Davison
0a027df50d feat: better error handling for duplicate edges and fixed tests 2024-04-09 12:37:18 +01:00
Pete Davison
a50580b5a1 feat: dag reader 2024-04-09 12:37:18 +01:00
Pete Davison
1890722b75 chore: changelog for #1547 2024-04-09 11:28:12 +00:00
Pete Davison
1ff618cc17 feat: enable any variables without maps (#1547)
* feat: enable any variable experiment (without maps)

* chore: rename any_variables experiment to map_variables

* docs: create map variables experiment docs and update usage

* blog: any variables

* fix: links

* fix: warn about broken links instead of failing
2024-04-09 12:14:14 +01:00
Andrey Nering
eb2783fcce fix: fix bug for files with special chars &() (#1584) 2024-04-09 02:08:30 +00:00
128 changed files with 4308 additions and 2308 deletions

1
.gitattributes vendored
View File

@@ -1 +1,2 @@
* text=auto * text=auto
*.mdx -linguist-detectable

View File

@@ -1,5 +1,9 @@
> Thanks for your pull request, we really appreciate contributions! <!--
>
> Please understand that it may take some time to be reviewed. Thanks for your pull request, we really appreciate contributions!
>
> Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/). Please understand that it may take some time to be reviewed.
Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
-->

View File

@@ -16,14 +16,14 @@ jobs:
go-version: [1.21.x, 1.22.x] go-version: [1.21.x, 1.22.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.55.2 version: v1.55.2
@@ -34,7 +34,7 @@ jobs:
with: with:
python-version: 3.12 python-version: 3.12
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: install check-jsonschema - name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3' run: python -m pip install 'check-jsonschema==0.27.3'

View File

@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: 1.21.x go-version: 1.21.x

View File

@@ -18,13 +18,13 @@ jobs:
runs-on: ${{matrix.platform}} runs-on: ${{matrix.platform}}
steps: steps:
- name: Set up Go ${{matrix.go-version}} - name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download Go modules - name: Download Go modules
run: go mod download run: go mod download

View File

@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'go-task/task' if: github.repository == 'go-task/task'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Verify changed files - name: Verify changed files
id: changed-files id: changed-files
@@ -25,7 +25,7 @@ jobs:
website/src/pages website/src/pages
- name: Install Task - name: Install Task
uses: arduino/setup-task@v1 uses: arduino/setup-task@v2
with: with:
version: 3.x version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -10,6 +10,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Graphvis files
*.gv
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/ .glide/

View File

@@ -11,7 +11,7 @@ linters:
linters-settings: linters-settings:
goimports: goimports:
local-prefixes: github.com/go-task/task local-prefixes: github.com/go-task
gofmt: gofmt:
rewrite-rules: rewrite-rules:
- pattern: 'interface{}' - pattern: 'interface{}'

View File

@@ -11,11 +11,16 @@ builds:
- amd64 - amd64
- arm - arm
- arm64 - arm64
- riscv64
goarm: goarm:
- '6' - '6'
ignore: ignore:
- goos: darwin - goos: darwin
goarch: '386' goarch: '386'
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}' mod_timestamp: '{{ .CommitTimestamp }}'
@@ -71,7 +76,7 @@ brews:
description: Task runner / simpler Make alternative written in Go description: Task runner / simpler Make alternative written in Go
license: MIT license: MIT
homepage: https://taskfile.dev homepage: https://taskfile.dev
folder: Formula directory: Formula
repository: repository:
owner: go-task owner: go-task
name: homebrew-tap name: homebrew-tap

View File

@@ -1,5 +1,75 @@
# Changelog # Changelog
## v3.38.0 - 2024-06-30
- Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering).
- Some YAML parsing errors will now show in a more user friendly way (#1619 by
@pd93).
- Prefixed outputs will now be colorized by default (#1572 by
@AlexanderArvidsson)
- [References](https://taskfile.dev/usage/#referencing-other-variables) are now
generally available (no experiments required) (#1654 by @pd93).
- Templating functions can now be used in references (#1645, #1654 by @pd93).
- Added a new
[templating reference page](https://taskfile.dev/reference/templating/) to the
documentation (#1614, #1653 by @pd93).
- If using the
[Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1),
references are available by
[prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references)
(#1654 by @pd93).
- If using the
[Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2),
the `yaml` and `json` keys are no longer available (#1654 by @pd93).
- Added a new `TASK_REMOTE_DIR` environment variable to configure where cached
remote Taskfiles are stored (#1661 by @vmaerten).
- Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639
by @vmaerten).
- Improved the readability of cached remote Taskfile filenames (#1636 by
@vmaerten).
- Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by
@mengzhuo).
- Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith).
- Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93).
- Fixed JSON Schema to disallow invalid properties (#1657 by @pd93).
- Fixed version checks not working as intended (#872, #1663 by @vmaerten).
- Fixed a bug where included tasks were run multiple times even if `run: once`
was set (#852, #1655 by @pd93).
- Fixed some bugs related to column formatting in the terminal (#1350, #1637,
#1656 by @vmaerten).
## v3.37.2 - 2024-05-12
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
- Fixed a bug where includes Taskfile variable were not being merged correctly
(#1643, #1649 by @pd93).
## v3.37.1 - 2024-05-09
- Fix bug where non-string values (numbers, bools) added to `env:` weren't been
correctly exported (#1640, #1641 by @vmaerten and @andreynering).
## v3.37.0 - 2024-05-08
- Released the
[Any Variables experiment](https://taskfile.dev/blog/any-variables), but
[_without support for maps_](https://github.com/go-task/task/issues/1415#issuecomment-2044756925)
(#1415, #1547 by @pd93).
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
#1607 by @pd93).
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
@pd93).
- Fix error when a file or directory in the project contained a special char
like `&`, `(` or `)` (#1551, #1584 by @andreynering).
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)
- Added support for `~` on ZSH completions (#1613 by @jwater7).
- Added the ability to pass variables by reference using Go template syntax when
the
[Map Variables experiment](https://taskfile.dev/experiments/map-variables/) is
enabled (#1612 by @pd93).
- Added support for environment variables in the templating engine in `includes`
(#1610 by @vmaerten).
## v3.36.0 - 2024-04-08 ## v3.36.0 - 2024-04-08
- Added support for - Added support for

View File

@@ -123,10 +123,53 @@ tasks:
cmds: cmds:
- go install github.com/goreleaser/goreleaser@latest - go install github.com/goreleaser/goreleaser@latest
release: release:*:
desc: Prepare the project for a new release desc: Prepare the project for a new release
summary: |
This task will do the following:
- Update the version and date in the CHANGELOG.md file
- Update the version in the package.json and package-lock.json files
- Copy the latest docs to the "current" version on the website
- Commit the changes
- Create a new tag
- Push the commit/tag to the repository
- Create a GitHub release
To use the task, simply run "task release:<version>" where "<version>" is is one of:
- "major" - Bumps the major number
- "minor" - Bumps the minor number
- "patch" - Bumps the patch number
- A semver compatible version number (e.g. "1.2.3")
vars:
VERSION:
sh: "go run ./cmd/release --version {{index .MATCH 0}}"
COMPLETE_MESSAGE: |
Creating release with GoReleaser: https://github.com/go-task/task/actions/workflows/release.yml
Please wait for the CI to finish and then do the following:
- Copy the changelog for v{{.VERSION}} to the GitHub release
- Publish the package to NPM with `task npm:publish`
- Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml
preconditions:
- sh: test $(git rev-parse --abbrev-ref HEAD) = "main"
msg: "You must be on the main branch to release"
- sh: "[[ -z $(git diff --shortstat main) ]]"
msg: "You must have a clean working tree to release"
prompt: "Are you sure you want to release version {{.VERSION}}?"
cmds: cmds:
- go run ./cmd/release {{.CLI_ARGS}} - cmd: echo "Releasing v{{.VERSION}}"
silent: true
- "go run ./cmd/release {{.VERSION}}"
- "git add --all"
- "git commit -m v{{.VERSION}}"
- "git push"
- "git tag v{{.VERSION}}"
- "git push origin tag v{{.VERSION}}"
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
silent: true
npm:publish: npm:publish:
desc: Publish release to npm desc: Publish release to npm

View File

@@ -11,6 +11,7 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/otiai10/copy" "github.com/otiai10/copy"
"github.com/spf13/pflag"
) )
const ( const (
@@ -25,6 +26,16 @@ var (
versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`) versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`)
) )
// Flags
var (
versionFlag bool
)
func init() {
pflag.BoolVarP(&versionFlag, "version", "v", false, "resolved version number")
pflag.Parse()
}
func main() { func main() {
if err := release(); err != nil { if err := release(); err != nil {
fmt.Println(err) fmt.Println(err)
@@ -33,7 +44,7 @@ func main() {
} }
func release() error { func release() error {
if len(os.Args) != 2 { if len(pflag.Args()) != 1 {
return errors.New("error: expected version number") return errors.New("error: expected version number")
} }
@@ -42,11 +53,14 @@ func release() error {
return err return err
} }
if err := bumpVersion(version, os.Args[1]); err != nil { if err := bumpVersion(version, pflag.Arg(0)); err != nil {
return err return err
} }
fmt.Println(version) if versionFlag {
fmt.Println(version)
return nil
}
if err := changelog(version); err != nil { if err := changelog(version); err != nil {
return err return err

View File

@@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"strings" "strings"
@@ -18,6 +17,7 @@ import (
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version" ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -44,8 +44,12 @@ func main() {
} }
func run() error { func run() error {
log.SetFlags(0) logger := &logger.Logger{
log.SetOutput(os.Stderr) Stdout: os.Stdout,
Stderr: os.Stderr,
Verbose: flags.Verbose,
Color: flags.Color,
}
if err := flags.Validate(); err != nil { if err := flags.Validate(); err != nil {
return err return err
@@ -55,7 +59,7 @@ func run() error {
entrypoint := flags.Entrypoint entrypoint := flags.Entrypoint
if flags.Version { if flags.Version {
fmt.Printf("Task version: %s\n", ver.GetVersion()) fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
return nil return nil
} }
@@ -65,22 +69,16 @@ func run() error {
} }
if flags.Experiments { if flags.Experiments {
l := &logger.Logger{ return experiments.List(logger)
Stdout: os.Stdout,
Stderr: os.Stderr,
Verbose: flags.Verbose,
Color: flags.Color,
}
return experiments.List(l)
} }
if flags.Init { if flags.Init {
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
log.Fatal(err) return err
} }
if err := task.InitTaskfile(os.Stdout, wd); err != nil { if err := task.InitTaskfile(os.Stdout, wd); err != nil {
log.Fatal(err) return err
} }
return nil return nil
} }
@@ -128,15 +126,18 @@ func run() error {
OutputStyle: flags.Output, OutputStyle: flags.Output,
TaskSorter: taskSorter, TaskSorter: taskSorter,
} }
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus) listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil { if err := listOptions.Validate(); err != nil {
return err return err
} }
if err := e.Setup(); err != nil { err := e.Setup()
if err != nil {
return err return err
} }
if experiments.AnyVariables.Enabled {
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
}
// If the download flag is specified, we should stop execution as soon as // If the download flag is specified, we should stop execution as soon as
// taskfile is downloaded // taskfile is downloaded
@@ -144,6 +145,14 @@ func run() error {
return nil return nil
} }
if flags.ClearCache {
cache, err := taskfile.NewCache(e.TempDir.Remote)
if err != nil {
return err
}
return cache.Clear()
}
if (listOptions.ShouldListTasks()) && flags.Silent { if (listOptions.ShouldListTasks()) && flags.Silent {
return e.ListTaskNames(flags.ListAll) return e.ListTaskNames(flags.ListAll)
} }
@@ -178,7 +187,9 @@ func run() error {
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs}) globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll}) globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
e.Taskfile.Vars.Merge(globals) globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch { if !flags.Watch {
e.InterceptInterruptSignals() e.InterceptInterruptSignals()

View File

@@ -12,7 +12,9 @@ function __task_list() {
local taskfile item task desc local taskfile item task desc
cmd=(task) cmd=(task)
taskfile="${(v)opt_args[(i)-t|--taskfile]}" taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
taskfile=${taskfile//\~/$HOME}
if [[ -n "$taskfile" && -f "$taskfile" ]]; then if [[ -n "$taskfile" && -f "$taskfile" ]]; then
enabled=1 enabled=1

View File

@@ -0,0 +1,179 @@
package errors
import (
"bytes"
"embed"
"errors"
"fmt"
"regexp"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/quick"
"github.com/alecthomas/chroma/v2/styles"
"github.com/fatih/color"
"gopkg.in/yaml.v3"
)
//go:embed themes/*.xml
var embedded embed.FS
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
func init() {
r, err := embedded.Open("themes/task.xml")
if err != nil {
panic(err)
}
style, err := chroma.NewXMLStyle(r)
if err != nil {
panic(err)
}
styles.Register(style)
}
type (
TaskfileDecodeError struct {
Message string
Location string
Line int
Column int
Tag string
Snippet TaskfileSnippet
Err error
}
TaskfileSnippet struct {
Lines []string
StartLine int
EndLine int
Padding int
}
)
func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {
// If the error is already a DecodeError, return it
taskfileInvalidErr := &TaskfileDecodeError{}
if errors.As(err, &taskfileInvalidErr) {
return taskfileInvalidErr
}
return &TaskfileDecodeError{
Line: node.Line,
Column: node.Column,
Tag: node.ShortTag(),
Err: err,
}
}
func (err *TaskfileDecodeError) Error() string {
buf := &bytes.Buffer{}
// Print the error message
if err.Message != "" {
fmt.Fprintln(buf, color.RedString("err: %s", err.Message))
} else {
// Extract the errors from the TypeError
te := &yaml.TypeError{}
if errors.As(err.Err, &te) {
if len(te.Errors) > 1 {
fmt.Fprintln(buf, color.RedString("errs:"))
for _, message := range te.Errors {
fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
}
} else {
fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
}
} else {
// Otherwise print the error message normally
fmt.Fprintln(buf, color.RedString("err: %s", err.Err))
}
}
fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column))
// Print the snippet
maxLineNumberDigits := digits(err.Snippet.EndLine)
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
columnSpacer := strings.Repeat(" ", err.Column-1)
for i, line := range err.Snippet.Lines {
currentLine := err.Snippet.StartLine + i + 1
lineIndicator := " "
if currentLine == err.Line {
lineIndicator = ">"
}
columnIndicator := "^"
// Print each line
lineIndicator = color.RedString(lineIndicator)
columnIndicator = color.RedString(columnIndicator)
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line)
// Print the column indicator
if currentLine == err.Line {
fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator)
}
// If there are more lines to print, add a newline
if i < len(err.Snippet.Lines)-1 {
fmt.Fprintln(buf)
}
}
return buf.String()
}
func (err *TaskfileDecodeError) Unwrap() error {
return err.Err
}
func (err *TaskfileDecodeError) Code() int {
return CodeTaskfileDecode
}
func (err *TaskfileDecodeError) WithMessage(format string, a ...any) *TaskfileDecodeError {
err.Message = fmt.Sprintf(format, a...)
return err
}
func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {
err.Message = fmt.Sprintf("cannot unmarshal %s into %s", err.Tag, t)
return err
}
func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError {
buf := &bytes.Buffer{}
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
buf.WriteString(string(b))
}
lines := strings.Split(buf.String(), "\n")
start := max(err.Line-1-padding, 0)
end := min(err.Line+padding, len(lines)-1)
err.Location = location
err.Snippet = TaskfileSnippet{
Lines: lines[start:end],
StartLine: start,
EndLine: end,
Padding: padding,
}
return err
}
func extractTypeErrorMessage(message string) string {
matches := typeErrorRegex.FindStringSubmatch(message)
if len(matches) == 2 {
return matches[1]
}
return message
}
func digits(number int) int {
count := 0
for number != 0 {
number /= 10
count += 1
}
return count
}

View File

@@ -12,13 +12,15 @@ const (
const ( const (
CodeTaskfileNotFound int = iota + 100 CodeTaskfileNotFound int = iota + 100
CodeTaskfileAlreadyExists CodeTaskfileAlreadyExists
CodeTaskfileInvalid CodeTaskfileDecode
CodeTaskfileFetchFailed CodeTaskfileFetchFailed
CodeTaskfileNotTrusted CodeTaskfileNotTrusted
CodeTaskfileNotSecure CodeTaskfileNotSecure
CodeTaskfileCacheNotFound CodeTaskfileCacheNotFound
CodeTaskfileVersionCheckError CodeTaskfileVersionCheckError
CodeTaskfileNetworkTimeout CodeTaskfileNetworkTimeout
CodeTaskfileInvalid
CodeTaskfileCycle
) )
// Task related exit codes // Task related exit codes
@@ -56,3 +58,8 @@ func Is(err, target error) bool {
func As(err error, target any) bool { func As(err error, target any) bool {
return errors.As(err, target) return errors.As(err, target)
} }
// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package.
func Unwrap(err error) error {
return errors.Unwrap(err)
}

View File

@@ -174,3 +174,21 @@ func (err *TaskfileNetworkTimeoutError) Error() string {
func (err *TaskfileNetworkTimeoutError) Code() int { func (err *TaskfileNetworkTimeoutError) Code() int {
return CodeTaskfileNetworkTimeout return CodeTaskfileNetworkTimeout
} }
// TaskfileCycleError is returned when we detect that a Taskfile includes a
// set of Taskfiles that include each other in a cycle.
type TaskfileCycleError struct {
Source string
Destination string
}
func (err TaskfileCycleError) Error() string {
return fmt.Sprintf("task: include cycle detected between %s <--> %s",
err.Source,
err.Destination,
)
}
func (err TaskfileCycleError) Code() int {
return CodeTaskfileCycle
}

17
errors/themes/task.xml Normal file
View File

@@ -0,0 +1,17 @@
<style name="task">
<entry type="Background" style="bg:#eee8d5"/>
<entry type="Keyword" style="#859900"/>
<entry type="KeywordConstant" style=""/>
<entry type="KeywordNamespace" style="#dc322f"/>
<entry type="KeywordType" style=""/>
<entry type="Name" style="#268bd2"/>
<entry type="NameBuiltin" style="#cb4b16"/>
<entry type="NameClass" style="#cb4b16"/>
<entry type="NameTag" style=""/>
<entry type="Literal" style="#2aa198"/>
<entry type="LiteralNumber" style=""/>
<entry type="OperatorWord" style="#859900"/>
<entry type="Comment" style="italic #93a1a1"/>
<entry type="Generic" style="#d33682"/>
<entry type="Text" style="#586e75"/>
</style>

13
go.mod
View File

@@ -1,12 +1,16 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.21 go 1.21.0
require ( require (
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/semver/v3 v3.2.1
github.com/alecthomas/chroma/v2 v2.14.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.16.0 github.com/dominikbraun/graph v0.23.0
github.com/fatih/color v1.17.0
github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.0.0-20240602015157-960e6f576656
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.4 github.com/mattn/go-zglob v0.0.4
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
@@ -17,17 +21,18 @@ require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/zeebo/xxh3 v1.0.2 github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.7.0 golang.org/x/sync v0.7.0
golang.org/x/term v0.19.0 golang.org/x/term v0.21.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.8.0 mvdan.cc/sh/v3 v3.8.0
) )
require ( require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
) )

28
go.sum
View File

@@ -1,17 +1,33 @@
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
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/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.0.0-20240602015157-960e6f576656 h1:knZZ4zVdTBQnevBz0zSES++4Mr7wr+cHopLvHabIgkA=
github.com/go-task/template v0.0.0-20240602015157-960e6f576656/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
@@ -55,10 +71,10 @@ golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -4,15 +4,12 @@ import (
"fmt" "fmt"
"github.com/go-task/task/v3/internal/hash" "github.com/go-task/task/v3/internal/hash"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func (e *Executor) GetHash(t *ast.Task) (string, error) { func (e *Executor) GetHash(t *ast.Task) (string, error) {
r := t.Run r := slicesext.FirstNonZero(t.Run, e.Taskfile.Run)
if r == "" {
r = e.Taskfile.Run
}
var h hash.HashFunc var h hash.HashFunc
switch r { switch r {
case "always": case "always":

View File

@@ -7,8 +7,8 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"github.com/Ladicle/tabwriter"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/go-task/task/v3/internal/editors" "github.com/go-task/task/v3/internal/editors"
@@ -105,7 +105,8 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
for _, task := range tasks { for _, task := range tasks {
e.Logger.FOutf(w, logger.Yellow, "* ") e.Logger.FOutf(w, logger.Yellow, "* ")
e.Logger.FOutf(w, logger.Green, task.Task) e.Logger.FOutf(w, logger.Green, task.Task)
e.Logger.FOutf(w, logger.Default, ": \t%s", task.Desc) desc := strings.ReplaceAll(task.Desc, "\n", " ")
e.Logger.FOutf(w, logger.Default, ": \t%s", desc)
if len(task.Aliases) > 0 { if len(task.Aliases) > 0 {
e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", ")) e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", "))
} }
@@ -190,7 +191,7 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
} }
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), task, upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), task,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir), fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry), fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger), fingerprint.WithLogger(e.Logger),
) )

View File

@@ -3,14 +3,12 @@ package compiler
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -62,10 +60,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
cache := &templater.Cache{Vars: result} cache := &templater.Cache{Vars: result}
// Replace values // Replace values
newVar := templater.ReplaceVar(v, cache) newVar := templater.ReplaceVar(v, cache)
// If the variable is a reference, we can resolve it
if newVar.Ref != "" {
newVar.Value = result.Get(newVar.Ref).Value
}
// If the variable should not be evaluated, but is nil, set it to an empty string // If the variable should not be evaluated, but is nil, set it to an empty string
// This stops empty interface errors when using the templater to replace values later // This stops empty interface errors when using the templater to replace values later
if !evaluateShVars && newVar.Value == nil { if !evaluateShVars && newVar.Value == nil {
@@ -81,18 +75,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
if err := cache.Err(); err != nil { if err := cache.Err(); err != nil {
return err return err
} }
// Evaluate JSON
if newVar.Json != "" {
if err := json.Unmarshal([]byte(newVar.Json), &newVar.Value); err != nil {
return err
}
}
// Evaluate YAML
if newVar.Yaml != "" {
if err := yaml.Unmarshal([]byte(newVar.Yaml), &newVar.Value); err != nil {
return err
}
}
// If the variable is not dynamic, we can set it and return // If the variable is not dynamic, we can set it and return
if newVar.Value != nil || newVar.Sh == "" { if newVar.Value != nil || newVar.Sh == "" {
result.Set(k, ast.Var{Value: newVar.Value}) result.Set(k, ast.Var{Value: newVar.Value})
@@ -200,6 +182,7 @@ func (c *Compiler) ResetCache() {
func (c *Compiler) getSpecialVars(t *ast.Task) (map[string]string, error) { func (c *Compiler) getSpecialVars(t *ast.Task) (map[string]string, error) {
return map[string]string{ return map[string]string{
"TASK": t.Task, "TASK": t.Task,
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": c.Dir, "ROOT_DIR": c.Dir,
"TASKFILE": t.Location.Taskfile, "TASKFILE": t.Location.Taskfile,

16
internal/env/env.go vendored
View File

@@ -13,19 +13,25 @@ func Get(t *ast.Task) []string {
} }
environ := os.Environ() environ := os.Environ()
for k, v := range t.Env.ToCacheMap() { for k, v := range t.Env.ToCacheMap() {
str, isString := v.(string) if !isTypeAllowed(v) {
if !isString {
continue continue
} }
if _, alreadySet := os.LookupEnv(k); alreadySet { if _, alreadySet := os.LookupEnv(k); alreadySet {
continue continue
} }
environ = append(environ, fmt.Sprintf("%s=%v", k, v))
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
} }
return environ return environ
} }
func isTypeAllowed(v any) bool {
switch v.(type) {
case string, bool, int, float32, float64:
return true
default:
return false
}
}

View File

@@ -103,6 +103,9 @@ func IsExitError(err error) bool {
func Expand(s string) (string, error) { func Expand(s string) (string, error) {
s = filepath.ToSlash(s) s = filepath.ToSlash(s)
s = strings.ReplaceAll(s, " ", `\ `) s = strings.ReplaceAll(s, " ", `\ `)
s = strings.ReplaceAll(s, "&", `\&`)
s = strings.ReplaceAll(s, "(", `\(`)
s = strings.ReplaceAll(s, ")", `\)`)
fields, err := shell.Fields(s, nil) fields, err := shell.Fields(s, nil)
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -7,8 +7,8 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"text/tabwriter"
"github.com/Ladicle/tabwriter"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -28,6 +28,7 @@ var (
GentleForce Experiment GentleForce Experiment
RemoteTaskfiles Experiment RemoteTaskfiles Experiment
AnyVariables Experiment AnyVariables Experiment
MapVariables Experiment
) )
func init() { func init() {
@@ -35,6 +36,7 @@ func init() {
GentleForce = New("GENTLE_FORCE") GentleForce = New("GENTLE_FORCE")
RemoteTaskfiles = New("REMOTE_TASKFILES") RemoteTaskfiles = New("REMOTE_TASKFILES")
AnyVariables = New("ANY_VARIABLES", "1", "2") AnyVariables = New("ANY_VARIABLES", "1", "2")
MapVariables = New("MAP_VARIABLES", "1", "2")
} }
func New(xName string, enabledValues ...string) Experiment { func New(xName string, enabledValues ...string) Experiment {
@@ -101,6 +103,6 @@ func List(l *logger.Logger) error {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, ' ', 0)
printExperiment(w, l, GentleForce) printExperiment(w, l, GentleForce)
printExperiment(w, l, RemoteTaskfiles) printExperiment(w, l, RemoteTaskfiles)
printExperiment(w, l, AnyVariables) printExperiment(w, l, MapVariables)
return w.Flush() return w.Flush()
} }

View File

@@ -3,6 +3,7 @@ package flags
import ( import (
"errors" "errors"
"log" "log"
"os"
"time" "time"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -64,10 +65,13 @@ var (
Experiments bool Experiments bool
Download bool Download bool
Offline bool Offline bool
ClearCache bool
Timeout time.Duration Timeout time.Duration
) )
func init() { func init() {
log.SetFlags(0)
log.SetOutput(os.Stderr)
pflag.Usage = func() { pflag.Usage = func() {
log.Print(usage) log.Print(usage)
pflag.PrintDefaults() pflag.PrintDefaults()
@@ -116,6 +120,7 @@ func init() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.") pflag.BoolVar(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.") pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
} }
pflag.Parse() pflag.Parse()
@@ -126,6 +131,10 @@ func Validate() error {
return errors.New("task: You can't set both --download and --offline flags") return errors.New("task: You can't set both --download and --offline flags")
} }
if Download && ClearCache {
return errors.New("task: You can't set both --download and --clear-cache flags")
}
if Global && Dir != "" { if Global && Dir != "" {
log.Fatal("task: You can't set both --global and --dir") log.Fatal("task: You can't set both --global and --dir")
return nil return nil

View File

@@ -15,7 +15,7 @@ func Empty(*ast.Task) (string, error) {
} }
func Name(t *ast.Task) (string, error) { func Name(t *ast.Task) (string, error) {
return t.Task, nil return fmt.Sprintf("%s:%s", t.Location.Taskfile, t.LocalName()), nil
} }
func Hash(t *ast.Task) (string, error) { func Hash(t *ast.Task) (string, error) {

View File

@@ -52,6 +52,30 @@ func Red() PrintFunc {
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc() return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc()
} }
func BrightBlue() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc()
}
func BrightGreen() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc()
}
func BrightCyan() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc()
}
func BrightYellow() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc()
}
func BrightMagenta() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc()
}
func BrightRed() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc()
}
func envColor(env string, defaultColor color.Attribute) []color.Attribute { func envColor(env string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" { if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false color.NoColor = false
@@ -138,6 +162,10 @@ func (l *Logger) VerboseErrf(color Color, s string, args ...any) {
} }
} }
func (l *Logger) Warnf(message string, args ...any) {
l.Errf(Yellow, message, args...)
}
func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continueValues ...string) error { func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continueValues ...string) error {
if l.AssumeYes { if l.AssumeYes {
l.Outf(color, "%s [assuming yes]\n", prompt) l.Outf(color, "%s [assuming yes]\n", prompt)
@@ -152,7 +180,7 @@ func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continu
return errors.New("no continue values provided") return errors.New("no continue values provided")
} }
l.Outf(color, "%s [%s/%s]\n", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue)) l.Outf(color, "%s [%s/%s]: ", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
reader := bufio.NewReader(l.Stdin) reader := bufio.NewReader(l.Stdin)
input, err := reader.ReadString('\n') input, err := reader.ReadString('\n')

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -15,7 +16,7 @@ type Output interface {
type CloseFunc func(err error) error type CloseFunc func(err error) error
// Build the Output for the requested ast.Output. // Build the Output for the requested ast.Output.
func BuildFor(o *ast.Output) (Output, error) { func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) {
switch o.Name { switch o.Name {
case "interleaved", "": case "interleaved", "":
if err := checkOutputGroupUnset(o); err != nil { if err := checkOutputGroupUnset(o); err != nil {
@@ -32,7 +33,7 @@ func BuildFor(o *ast.Output) (Output, error) {
if err := checkOutputGroupUnset(o); err != nil { if err := checkOutputGroupUnset(o); err != nil {
return nil, err return nil, err
} }
return Prefixed{}, nil return NewPrefixed(logger), nil
default: default:
return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name) return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name)
} }

View File

@@ -7,9 +7,11 @@ import (
"io" "io"
"testing" "testing"
"github.com/fatih/color"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
@@ -107,7 +109,11 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
func TestPrefixed(t *testing.T) { func TestPrefixed(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Prefixed{} l := &logger.Logger{
Color: false,
}
var o output.Output = output.NewPrefixed(l)
w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil) w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil)
t.Run("simple use cases", func(t *testing.T) { t.Run("simple use cases", func(t *testing.T) {
@@ -132,3 +138,33 @@ func TestPrefixed(t *testing.T) {
assert.Equal(t, "[prefix] Test!\n", b.String()) assert.Equal(t, "[prefix] Test!\n", b.String())
}) })
} }
func TestPrefixedWithColor(t *testing.T) {
color.NoColor = false
var b bytes.Buffer
l := &logger.Logger{
Color: true,
}
var o output.Output = output.NewPrefixed(l)
writers := make([]io.Writer, 16)
for i := range writers {
writers[i], _, _ = o.WrapWriter(&b, io.Discard, fmt.Sprintf("prefix-%d", i), nil)
}
t.Run("colors should loop", func(t *testing.T) {
for i, w := range writers {
b.Reset()
color := output.PrefixColorSequence[i%len(output.PrefixColorSequence)]
var prefix bytes.Buffer
l.FOutf(&prefix, color, fmt.Sprintf("prefix-%d", i))
fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix.String(), prefix.String()), b.String())
}
})
}

View File

@@ -6,20 +6,36 @@ import (
"io" "io"
"strings" "strings"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
) )
type Prefixed struct{} type Prefixed struct {
logger *logger.Logger
seen map[string]uint
counter *uint
}
func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) { func NewPrefixed(logger *logger.Logger) Prefixed {
pw := &prefixWriter{writer: stdOut, prefix: prefix} var counter uint
return Prefixed{
seen: make(map[string]uint),
counter: &counter,
logger: logger,
}
}
func (p Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: &p}
return pw, pw, func(error) error { return pw.close() } return pw, pw, func(error) error { return pw.close() }
} }
type prefixWriter struct { type prefixWriter struct {
writer io.Writer writer io.Writer
prefix string prefixed *Prefixed
buff bytes.Buffer prefix string
buff bytes.Buffer
} }
func (pw *prefixWriter) Write(p []byte) (int, error) { func (pw *prefixWriter) Write(p []byte) (int, error) {
@@ -56,6 +72,11 @@ func (pw *prefixWriter) writeOutputLines(force bool) error {
} }
} }
var PrefixColorSequence = []logger.Color{
logger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red,
logger.BrightYellow, logger.BrightBlue, logger.BrightMagenta, logger.BrightCyan, logger.BrightGreen, logger.BrightRed,
}
func (pw *prefixWriter) writeLine(line string) error { func (pw *prefixWriter) writeLine(line string) error {
if line == "" { if line == "" {
return nil return nil
@@ -63,6 +84,27 @@ func (pw *prefixWriter) writeLine(line string) error {
if !strings.HasSuffix(line, "\n") { if !strings.HasSuffix(line, "\n") {
line += "\n" line += "\n"
} }
_, err := fmt.Fprintf(pw.writer, "[%s] %s", pw.prefix, line)
idx, ok := pw.prefixed.seen[pw.prefix]
if !ok {
idx = *pw.prefixed.counter
pw.prefixed.seen[pw.prefix] = idx
*pw.prefixed.counter++
}
if _, err := fmt.Fprint(pw.writer, "["); err != nil {
return nil
}
color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))]
pw.prefixed.logger.FOutf(pw.writer, color, pw.prefix)
if _, err := fmt.Fprint(pw.writer, "] "); err != nil {
return nil
}
_, err := fmt.Fprint(pw.writer, line)
return err return err
} }

View File

@@ -18,3 +18,13 @@ func UniqueJoin[T cmp.Ordered](ss ...[]T) []T {
slices.Sort(r) slices.Sort(r)
return slices.Compact(r) return slices.Compact(r)
} }
func FirstNonZero[T comparable](values ...T) T {
var zero T
for _, v := range values {
if v != zero {
return v
}
}
return zero
}

View File

@@ -4,13 +4,13 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"text/template"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
sprig "github.com/go-task/slim-sprig/v3" sprig "github.com/go-task/slim-sprig/v3"
"github.com/go-task/template"
) )
var templateFuncs template.FuncMap var templateFuncs template.FuncMap
@@ -73,12 +73,16 @@ func init() {
return spew.Sdump(v) return spew.Sdump(v)
}, },
} }
// aliases
taskFuncs["q"] = taskFuncs["shellQuote"]
// Deprecated aliases for renamed functions. // Deprecated aliases for renamed functions.
taskFuncs["FromSlash"] = taskFuncs["fromSlash"] taskFuncs["FromSlash"] = taskFuncs["fromSlash"]
taskFuncs["ToSlash"] = taskFuncs["toSlash"] taskFuncs["ToSlash"] = taskFuncs["toSlash"]
taskFuncs["ExeExt"] = taskFuncs["exeExt"] taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = sprig.TxtFuncMap() templateFuncs = template.FuncMap(sprig.TxtFuncMap())
for k, v := range taskFuncs { for k, v := range taskFuncs {
templateFuncs[k] = v templateFuncs[k] = v
} }

View File

@@ -2,12 +2,13 @@ package templater
import ( import (
"bytes" "bytes"
"fmt"
"maps" "maps"
"strings" "strings"
"text/template"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/template"
) )
// Cache is a help struct that allow us to call "replaceX" funcs multiple // Cache is a help struct that allow us to call "replaceX" funcs multiple
@@ -29,6 +30,33 @@ func (r *Cache) Err() error {
return r.err return r.err
} }
func ResolveRef(ref string, cache *Cache) any {
// If there is already an error, do nothing
if cache.err != nil {
return nil
}
// Initialize the cache map if it's not already initialized
if cache.cacheMap == nil {
cache.cacheMap = cache.Vars.ToCacheMap()
}
if ref == "." {
return cache.cacheMap
}
t, err := template.New("resolver").Funcs(templateFuncs).Parse(fmt.Sprintf("{{%s}}", ref))
if err != nil {
cache.err = err
return nil
}
val, err := t.Resolve(cache.cacheMap)
if err != nil {
cache.err = err
return nil
}
return val
}
func Replace[T any](v T, cache *Cache) T { func Replace[T any](v T, cache *Cache) T {
return ReplaceWithExtra(v, cache, nil) return ReplaceWithExtra(v, cache, nil)
} }
@@ -91,14 +119,15 @@ func ReplaceVar(v ast.Var, cache *Cache) ast.Var {
} }
func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var { func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {
if v.Ref != "" {
return ast.Var{Value: ResolveRef(v.Ref, cache)}
}
return ast.Var{ return ast.Var{
Value: ReplaceWithExtra(v.Value, cache, extra), Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra), Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live, Live: v.Live,
Ref: v.Ref, Ref: v.Ref,
Dir: v.Dir, Dir: v.Dir,
Json: ReplaceWithExtra(v.Json, cache, extra),
Yaml: ReplaceWithExtra(v.Yaml, cache, extra),
} }
} }

View File

@@ -5,21 +5,25 @@ import (
"runtime/debug" "runtime/debug"
) )
var version = "" var (
version = ""
func GetVersion() string { sum = ""
if version != "" { )
return version
}
func init() {
info, ok := debug.ReadBuildInfo() info, ok := debug.ReadBuildInfo()
if !ok || info.Main.Version == "" { if !ok || info.Main.Version == "" {
return "unknown" version = "unknown"
} else {
version = info.Main.Version
sum = info.Main.Sum
} }
}
ver := info.Main.Version
if info.Main.Sum != "" { func GetVersion() string {
ver += fmt.Sprintf(" (%s)", info.Main.Sum) return version
} }
return ver
func GetVersionWithSum() string {
return fmt.Sprintf("%s (%s)", version, sum)
} }

2
package-lock.json generated
View File

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

View File

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

View File

@@ -63,19 +63,22 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
} }
func (e *Executor) readTaskfile(node taskfile.Node) error { func (e *Executor) readTaskfile(node taskfile.Node) error {
var err error reader := taskfile.NewReader(
e.Taskfile, err = taskfile.Read(
node, node,
e.Insecure, e.Insecure,
e.Download, e.Download,
e.Offline, e.Offline,
e.Timeout, e.Timeout,
e.TempDir, e.TempDir.Remote,
e.Logger, e.Logger,
) )
graph, err := reader.Read()
if err != nil { if err != nil {
return err return err
} }
if e.Taskfile, err = graph.Merge(); err != nil {
return err
}
return nil return nil
} }
@@ -101,12 +104,15 @@ func (e *Executor) setupFuzzyModel() {
} }
func (e *Executor) setupTempDir() error { func (e *Executor) setupTempDir() error {
if e.TempDir != "" { if e.TempDir != (TempDir{}) {
return nil return nil
} }
if os.Getenv("TASK_TEMP_DIR") == "" { if os.Getenv("TASK_TEMP_DIR") == "" {
e.TempDir = filepathext.SmartJoin(e.Dir, ".task") e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") { } else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR")) tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
if err != nil { if err != nil {
@@ -114,9 +120,28 @@ func (e *Executor) setupTempDir() error {
} }
projectDir, _ := filepath.Abs(e.Dir) projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir) projectName := filepath.Base(projectDir)
e.TempDir = filepathext.SmartJoin(tempDir, projectName) e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else { } else {
e.TempDir = filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")) e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
}
}
if os.Getenv("TASK_REMOTE_DIR") != "" {
if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
remoteTempDir, err := execext.Expand(filepathext.SmartJoin(e.Dir, ".task"))
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
} }
return nil return nil
@@ -152,7 +177,7 @@ func (e *Executor) setupOutput() error {
} }
var err error var err error
e.Output, err = output.BuildFor(&e.OutputStyle) e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
return err return err
} }

View File

@@ -27,7 +27,7 @@ func (e *Executor) Status(ctx context.Context, calls ...*ast.Call) error {
// Check if the task is up-to-date // Check if the task is up-to-date
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t, isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir), fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry), fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger), fingerprint.WithLogger(e.Logger),
) )
@@ -46,7 +46,7 @@ func (e *Executor) statusOnError(t *ast.Task) error {
if method == "" { if method == "" {
method = e.Taskfile.Method method = e.Taskfile.Method
} }
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir, e.Dry) checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry)
if err != nil { if err != nil {
return err return err
} }

29
task.go
View File

@@ -34,13 +34,18 @@ const (
MaximumTaskCall = 1000 MaximumTaskCall = 1000
) )
type TempDir struct {
Remote string
Fingerprint string
}
// Executor executes a Taskfile // Executor executes a Taskfile
type Executor struct { type Executor struct {
Taskfile *ast.Taskfile Taskfile *ast.Taskfile
Dir string Dir string
Entrypoint string Entrypoint string
TempDir string TempDir TempDir
Force bool Force bool
ForceAll bool ForceAll bool
Insecure bool Insecure bool
@@ -183,16 +188,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
release := e.acquireConcurrencyLimit() release := e.acquireConcurrencyLimit()
defer release() defer release()
if t.Prompt != "" {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
return e.startExecution(ctx, t, func(ctx context.Context) error { return e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil { if err := e.runDeps(ctx, t); err != nil {
@@ -222,7 +217,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir), fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry), fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger), fingerprint.WithLogger(e.Logger),
) )
@@ -238,6 +233,16 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
} }
} }
if t.Prompt != "" && !e.Dry {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
if err := e.mkdir(t); err != nil { if err := e.mkdir(t); err != nil {
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err) e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
} }

View File

@@ -62,8 +62,11 @@ func (fct fileContentTest) Run(t *testing.T) {
} }
e := &task.Executor{ e := &task.Executor{
Dir: fct.Dir, Dir: fct.Dir,
TempDir: filepathext.SmartJoin(fct.Dir, ".task"), TempDir: task.TempDir{
Remote: filepathext.SmartJoin(fct.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"),
},
Entrypoint: fct.Entrypoint, Entrypoint: fct.Entrypoint,
Stdout: io.Discard, Stdout: io.Discard,
Stderr: io.Discard, Stderr: io.Discard,
@@ -95,14 +98,24 @@ func TestEmptyTask(t *testing.T) {
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"})) require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
} }
func TestEmptyTaskfile(t *testing.T) {
e := &task.Executor{
Dir: "testdata/empty_taskfile",
Stdout: io.Discard,
Stderr: io.Discard,
}
require.Error(t, e.Setup(), "e.Setup()")
}
func TestEnv(t *testing.T) { func TestEnv(t *testing.T) {
tt := fileContentTest{ tt := fileContentTest{
Dir: "testdata/env", Dir: "testdata/env",
Target: "default", Target: "default",
TrimSpace: false, TrimSpace: false,
Files: map[string]string{ Files: map[string]string{
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n", "local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n", "global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
"multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n",
}, },
} }
tt.Run(t) tt.Run(t)
@@ -262,11 +275,14 @@ func TestStatus(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := &task.Executor{ e := &task.Executor{
Dir: dir, Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"), TempDir: task.TempDir{
Stdout: &buff, Remote: filepathext.SmartJoin(dir, ".task"),
Stderr: &buff, Fingerprint: filepathext.SmartJoin(dir, ".task"),
Silent: true, },
Stdout: &buff,
Stderr: &buff,
Silent: true,
} }
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
// gen-foo creates foo.txt, and will always fail it's status check. // gen-foo creates foo.txt, and will always fail it's status check.
@@ -458,7 +474,10 @@ func TestStatusChecksum(t *testing.T) {
} }
var buff bytes.Buffer var buff bytes.Buffer
tempdir := filepathext.SmartJoin(dir, ".task") tempdir := task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
TempDir: tempdir, TempDir: tempdir,
@@ -475,7 +494,7 @@ func TestStatusChecksum(t *testing.T) {
// Capture the modification time, so we can ensure the checksum file // Capture the modification time, so we can ensure the checksum file
// is not regenerated when the hash hasn't changed. // is not regenerated when the hash hasn't changed.
s, err := os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task)) s, err := os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
require.NoError(t, err) require.NoError(t, err)
time := s.ModTime() time := s.ModTime()
@@ -483,7 +502,7 @@ func TestStatusChecksum(t *testing.T) {
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.task})) require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.task}))
assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String()) assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String())
s, err = os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task)) s, err = os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, time, s.ModTime()) assert.Equal(t, time, s.ModTime())
}) })
@@ -804,8 +823,11 @@ func TestStatusVariables(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"), TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: &buff, Stdout: &buff,
Stderr: &buff, Stderr: &buff,
Silent: false, Silent: false,
@@ -953,11 +975,14 @@ func TestDryChecksum(t *testing.T) {
_ = os.Remove(checksumFile) _ = os.Remove(checksumFile)
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"), TempDir: task.TempDir{
Stdout: io.Discard, Remote: filepathext.SmartJoin(dir, ".task"),
Stderr: io.Discard, Fingerprint: filepathext.SmartJoin(dir, ".task"),
Dry: true, },
Stdout: io.Discard,
Stderr: io.Discard,
Dry: true,
} }
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"})) require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
@@ -1032,7 +1057,7 @@ func TestIncludesIncorrect(t *testing.T) {
err := e.Setup() err := e.Setup()
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "task: Failed to parse testdata/includes_incorrect/incomplete.yml:") assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error())
} }
func TestIncludesEmptyMain(t *testing.T) { func TestIncludesEmptyMain(t *testing.T) {
@@ -1199,15 +1224,17 @@ func TestIncludesInterpolation(t *testing.T) {
expectedErr bool expectedErr bool
expectedOutput string expectedOutput string
}{ }{
{"include", "include", false, "includes_interpolation\n"}, {"include", "include", false, "include\n"},
{"include with dir", "include-with-dir", false, "included\n"}, {"include_with_env_variable", "include-with-env-variable", false, "include_with_env_variable\n"},
{"include_with_dir", "include-with-dir", false, "included\n"},
} }
t.Setenv("MODULE", "included")
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: filepath.Join(dir, test.name),
Stdout: &buff, Stdout: &buff,
Stderr: &buff, Stderr: &buff,
Silent: true, Silent: true,
@@ -1225,6 +1252,34 @@ func TestIncludesInterpolation(t *testing.T) {
} }
} }
func TestIncludedTaskfileVarMerging(t *testing.T) {
const dir = "testdata/included_taskfile_var_merging"
tests := []struct {
name string
task string
expectedOutput string
}{
{"foo", "foo:pwd", "included_taskfile_var_merging/foo\n"},
{"bar", "bar:pwd", "included_taskfile_var_merging/bar\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,
}
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &ast.Call{Task: test.task})
require.NoError(t, err)
assert.Contains(t, buff.String(), test.expectedOutput)
})
}
}
func TestInternalTask(t *testing.T) { func TestInternalTask(t *testing.T) {
const dir = "testdata/internal_task" const dir = "testdata/internal_task"
tests := []struct { tests := []struct {
@@ -1624,6 +1679,26 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestRunOnceSharedDeps(t *testing.T) {
const dir = "testdata/run_once_shared_deps"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
ForceAll: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "build"}))
rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`)
matches := rx.FindAllStringSubmatch(buff.String(), -1)
assert.Len(t, matches, 1)
assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`)
assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`)
}
func TestDeferredCmds(t *testing.T) { func TestDeferredCmds(t *testing.T) {
const dir = "testdata/deferred" const dir = "testdata/deferred"
var buff bytes.Buffer var buff bytes.Buffer
@@ -2386,3 +2461,48 @@ func TestWildcard(t *testing.T) {
}) })
} }
} }
func TestReference(t *testing.T) {
tests := []struct {
name string
call string
expectedOutput string
}{
{
name: "reference in command",
call: "ref-cmd",
expectedOutput: "1\n",
},
{
name: "reference in dependency",
call: "ref-dep",
expectedOutput: "1\n",
},
{
name: "reference using templating resolver",
call: "ref-resolver",
expectedOutput: "1\n",
},
{
name: "reference using templating resolver and dynamic var",
call: "ref-resolver-sh",
expectedOutput: "Alice has 3 children called Bob, Charlie, and Diane\n",
},
}
for _, test := range tests {
t.Run(test.call, func(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/var_references",
Stdout: &buff,
Stderr: &buff,
Silent: true,
Force: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.call}))
assert.Equal(t, test.expectedOutput, buff.String())
})
}
}

View File

@@ -1,10 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
@@ -46,7 +45,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var cmd string var cmd string
if err := node.Decode(&cmd); err != nil { if err := node.Decode(&cmd); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
c.Cmd = cmd c.Cmd = cmd
return nil return nil
@@ -110,8 +109,8 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line) return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command")
} }

View File

@@ -1,9 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
) )
// Dep is a task dependency // Dep is a task dependency
@@ -32,7 +32,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var task string var task string
if err := node.Decode(&task); err != nil { if err := node.Decode(&task); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
d.Task = task d.Task = task
return nil return nil
@@ -45,7 +45,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
Silent bool Silent bool
} }
if err := node.Decode(&taskCall); err != nil { if err := node.Decode(&taskCall); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
d.Task = taskCall.Task d.Task = taskCall.Task
d.For = taskCall.For d.For = taskCall.For
@@ -54,5 +54,5 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("cannot unmarshal %s into dependency", node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("dependency")
} }

View File

@@ -1,10 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
@@ -22,7 +21,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var from string var from string
if err := node.Decode(&from); err != nil { if err := node.Decode(&from); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
f.From = from f.From = from
return nil return nil
@@ -30,7 +29,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.SequenceNode: case yaml.SequenceNode:
var list []any var list []any
if err := node.Decode(&list); err != nil { if err := node.Decode(&list); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
f.List = list f.List = list
return nil return nil
@@ -41,17 +40,19 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
Split string Split string
As string As string
} }
if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" { if err := node.Decode(&forStruct); err != nil {
f.Var = forStruct.Var return errors.NewTaskfileDecodeError(err, node)
f.Split = forStruct.Split
f.As = forStruct.As
return nil
} }
if forStruct.Var == "" {
return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line) return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for")
}
f.Var = forStruct.Var
f.Split = forStruct.Split
f.As = forStruct.As
return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into for", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("for")
} }
func (f *For) DeepCopy() *For { func (f *For) DeepCopy() *For {

View File

@@ -1,9 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
) )
type Glob struct { type Glob struct {
@@ -13,20 +13,22 @@ type Glob struct {
func (g *Glob) UnmarshalYAML(node *yaml.Node) error { func (g *Glob) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.ScalarNode: case yaml.ScalarNode:
g.Glob = node.Value g.Glob = node.Value
return nil return nil
case yaml.MappingNode: case yaml.MappingNode:
var glob struct { var glob struct {
Exclude string Exclude string
} }
if err := node.Decode(&glob); err != nil { if err := node.Decode(&glob); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
g.Glob = glob.Exclude g.Glob = glob.Exclude
g.Negate = true g.Negate = true
return nil return nil
default:
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag())
} }
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("glob")
} }

129
taskfile/ast/graph.go Normal file
View File

@@ -0,0 +1,129 @@
package ast
import (
"fmt"
"os"
"sync"
"github.com/dominikbraun/graph"
"github.com/dominikbraun/graph/draw"
"golang.org/x/sync/errgroup"
)
type TaskfileGraph struct {
sync.Mutex
graph.Graph[string, *TaskfileVertex]
}
// A TaskfileVertex is a vertex on the Taskfile DAG.
type TaskfileVertex struct {
URI string
Taskfile *Taskfile
}
func taskfileHash(vertex *TaskfileVertex) string {
return vertex.URI
}
func NewTaskfileGraph() *TaskfileGraph {
return &TaskfileGraph{
sync.Mutex{},
graph.New(taskfileHash,
graph.Directed(),
graph.PreventCycles(),
graph.Rooted(),
),
}
}
func (tfg *TaskfileGraph) Visualize(filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return draw.DOT(tfg.Graph, f)
}
func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
hashes, err := graph.TopologicalSort(tfg.Graph)
if err != nil {
return nil, err
}
predecessorMap, err := tfg.PredecessorMap()
if err != nil {
return nil, err
}
// Loop over each vertex in reverse topological order except for the root vertex.
// This gives us a loop over every included Taskfile in an order which is safe to merge.
for i := len(hashes) - 1; i > 0; i-- {
hash := hashes[i]
// Get the included vertex
includedVertex, err := tfg.Vertex(hash)
if err != nil {
return nil, err
}
// Create an error group to wait for all the included Taskfiles to be merged with all its parents
var g errgroup.Group
// Loop over edge that leads to a vertex that includes the current vertex
for _, edge := range predecessorMap[hash] {
// Start a goroutine to process each included Taskfile
g.Go(func() error {
// Get the base vertex
vertex, err := tfg.Vertex(edge.Source)
if err != nil {
return err
}
// Get the merge options
includes, ok := edge.Properties.Data.([]*Include)
if !ok {
return fmt.Errorf("task: Failed to get merge options")
}
// Merge the included Taskfiles into the parent Taskfile
for _, include := range includes {
if err := vertex.Taskfile.Merge(
includedVertex.Taskfile,
include,
); err != nil {
return err
}
}
return nil
})
if err := g.Wait(); err != nil {
return nil, err
}
}
// Wait for all the go routines to finish
if err := g.Wait(); err != nil {
return nil, err
}
}
// Get the root vertex
rootVertex, err := tfg.Vertex(hashes[0])
if err != nil {
return nil, err
}
_ = rootVertex.Taskfile.Tasks.Range(func(name string, task *Task) error {
if task == nil {
task = &Task{}
rootVertex.Taskfile.Tasks.Set(name, task)
}
task.Task = name
return nil
})
return rootVertex.Taskfile, nil
}

View File

@@ -1,10 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
omap "github.com/go-task/task/v3/internal/omap" omap "github.com/go-task/task/v3/internal/omap"
) )
@@ -22,7 +21,7 @@ type Include struct {
// Includes represents information about included tasksfiles // Includes represents information about included tasksfiles
type Includes struct { type Includes struct {
omap.OrderedMap[string, Include] omap.OrderedMap[string, *Include]
} }
// UnmarshalYAML implements the yaml.Unmarshaler interface. // UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -38,15 +37,15 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
var v Include var v Include
if err := valueNode.Decode(&v); err != nil { if err := valueNode.Decode(&v); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Namespace = keyNode.Value v.Namespace = keyNode.Value
includes.Set(keyNode.Value, v) includes.Set(keyNode.Value, &v)
} }
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes")
} }
// Len returns the length of the map // Len returns the length of the map
@@ -58,7 +57,7 @@ func (includes *Includes) Len() int {
} }
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors // Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
func (includes *Includes) Range(f func(k string, v Include) error) error { func (includes *Includes) Range(f func(k string, v *Include) error) error {
if includes == nil { if includes == nil {
return nil return nil
} }
@@ -71,7 +70,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var str string var str string
if err := node.Decode(&str); err != nil { if err := node.Decode(&str); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
include.Taskfile = str include.Taskfile = str
return nil return nil
@@ -86,7 +85,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Vars *Vars Vars *Vars
} }
if err := node.Decode(&includedTaskfile); err != nil { if err := node.Decode(&includedTaskfile); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
include.Taskfile = includedTaskfile.Taskfile include.Taskfile = includedTaskfile.Taskfile
include.Dir = includedTaskfile.Dir include.Dir = includedTaskfile.Dir
@@ -98,7 +97,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("include")
} }
// DeepCopy creates a new instance of IncludedTaskfile and copies // DeepCopy creates a new instance of IncludedTaskfile and copies

View File

@@ -1,9 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
) )
// Output of the Task output // Output of the Task output
@@ -25,7 +25,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var name string var name string
if err := node.Decode(&name); err != nil { if err := node.Decode(&name); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
s.Name = name s.Name = name
return nil return nil
@@ -35,10 +35,10 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
Group *OutputGroup Group *OutputGroup
} }
if err := node.Decode(&tmp); err != nil { if err := node.Decode(&tmp); err != nil {
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err) return errors.NewTaskfileDecodeError(err, node)
} }
if tmp.Group == nil { if tmp.Group == nil {
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form") return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" key when in mapping form`)
} }
*s = Output{ *s = Output{
Name: "group", Name: "group",
@@ -47,7 +47,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("output")
} }
// OutputGroup is the style options specific to the Group style. // OutputGroup is the style options specific to the Group style.

View File

@@ -6,6 +6,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/goext" "github.com/go-task/task/v3/internal/goext"
) )
@@ -30,7 +31,7 @@ type ErrInvalidPlatform struct {
} }
func (err *ErrInvalidPlatform) Error() string { func (err *ErrInvalidPlatform) Error() string {
return fmt.Sprintf(`task: Invalid platform "%s"`, err.Platform) return fmt.Sprintf(`invalid platform "%s"`, err.Platform)
} }
// UnmarshalYAML implements yaml.Unmarshaler interface. // UnmarshalYAML implements yaml.Unmarshaler interface.
@@ -39,14 +40,14 @@ func (p *Platform) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var platform string var platform string
if err := node.Decode(&platform); err != nil { if err := node.Decode(&platform); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
if err := p.parsePlatform(platform); err != nil { if err := p.parsePlatform(platform); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into platform", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("platform")
} }
// parsePlatform takes a string representing an OS/Arch combination (or either on their own) // parsePlatform takes a string representing an OS/Arch combination (or either on their own)

View File

@@ -26,10 +26,10 @@ func TestPlatformParsing(t *testing.T) {
{Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"}, {Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"},
{Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"}, {Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"},
{Input: "invalid", Error: `task: Invalid platform "invalid"`}, {Input: "invalid", Error: `invalid platform "invalid"`},
{Input: "invalid/invalid", Error: `task: Invalid platform "invalid/invalid"`}, {Input: "invalid/invalid", Error: `invalid platform "invalid/invalid"`},
{Input: "windows/invalid", Error: `task: Invalid platform "windows/invalid"`}, {Input: "windows/invalid", Error: `invalid platform "windows/invalid"`},
{Input: "invalid/amd64", Error: `task: Invalid platform "invalid/amd64"`}, {Input: "invalid/amd64", Error: `invalid platform "invalid/amd64"`},
} }
for _, test := range tests { for _, test := range tests {

View File

@@ -1,14 +1,12 @@
package ast package ast
import ( import (
"errors"
"fmt" "fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
)
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML. "github.com/go-task/task/v3/errors"
var ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value") )
// Precondition represents a precondition necessary for a task to run // Precondition represents a precondition necessary for a task to run
type Precondition struct { type Precondition struct {
@@ -33,7 +31,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var cmd string var cmd string
if err := node.Decode(&cmd); err != nil { if err := node.Decode(&cmd); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
p.Sh = cmd p.Sh = cmd
p.Msg = fmt.Sprintf("`%s` failed", cmd) p.Msg = fmt.Sprintf("`%s` failed", cmd)
@@ -45,7 +43,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
Msg string Msg string
} }
if err := node.Decode(&sh); err != nil { if err := node.Decode(&sh); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
p.Sh = sh.Sh p.Sh = sh.Sh
p.Msg = sh.Msg p.Msg = sh.Msg
@@ -55,5 +53,5 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into precondition", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("precondition")
} }

View File

@@ -7,42 +7,45 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
// Task represents a task // Task represents a task
type Task struct { type Task struct {
Task string Task string
Cmds []*Cmd Cmds []*Cmd
Deps []*Dep Deps []*Dep
Label string Label string
Desc string Desc string
Prompt string Prompt string
Summary string Summary string
Requires *Requires Requires *Requires
Aliases []string Aliases []string
Sources []*Glob Sources []*Glob
Generates []*Glob Generates []*Glob
Status []string Status []string
Preconditions []*Precondition Preconditions []*Precondition
Dir string Dir string
Set []string Set []string
Shopt []string Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Dotenv []string Dotenv []string
Silent bool Silent bool
Interactive bool Interactive bool
Internal bool Internal bool
Method string Method string
Prefix string Prefix string
IgnoreError bool IgnoreError bool
Run string Run string
Platforms []*Platform
Watch bool
Location *Location
// Populated during merging
Namespace string
IncludeVars *Vars IncludeVars *Vars
IncludedTaskfileVars *Vars IncludedTaskfileVars *Vars
Platforms []*Platform
Location *Location
Watch bool
} }
func (t *Task) Name() string { func (t *Task) Name() string {
@@ -52,6 +55,13 @@ func (t *Task) Name() string {
return t.Task return t.Task
} }
func (t *Task) LocalName() string {
name := t.Task
name = strings.TrimPrefix(name, t.Namespace)
name = strings.TrimPrefix(name, ":")
return name
}
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values. // WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
func (t *Task) WildcardMatch(name string) (bool, []string) { func (t *Task) WildcardMatch(name string) (bool, []string) {
// Convert the name into a regex string // Convert the name into a regex string
@@ -83,7 +93,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var cmd Cmd var cmd Cmd
if err := node.Decode(&cmd); err != nil { if err := node.Decode(&cmd); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
t.Cmds = append(t.Cmds, &cmd) t.Cmds = append(t.Cmds, &cmd)
return nil return nil
@@ -92,7 +102,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
case yaml.SequenceNode: case yaml.SequenceNode:
var cmds []*Cmd var cmds []*Cmd
if err := node.Decode(&cmds); err != nil { if err := node.Decode(&cmds); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
t.Cmds = cmds t.Cmds = cmds
return nil return nil
@@ -130,11 +140,11 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Watch bool Watch bool
} }
if err := node.Decode(&task); err != nil { if err := node.Decode(&task); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
if task.Cmd != nil { if task.Cmd != nil {
if task.Cmds != nil { if task.Cmds != nil {
return fmt.Errorf("yaml: line %d: task cannot have both cmd and cmds", node.Line) return errors.NewTaskfileDecodeError(nil, node).WithMessage("task cannot have both cmd and cmds")
} }
t.Cmds = []*Cmd{task.Cmd} t.Cmds = []*Cmd{task.Cmd}
} else { } else {
@@ -169,7 +179,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("task")
} }
// DeepCopy creates a new instance of Task and copies // DeepCopy creates a new instance of Task and copies
@@ -209,6 +219,7 @@ func (t *Task) DeepCopy() *Task {
Platforms: deepcopy.Slice(t.Platforms), Platforms: deepcopy.Slice(t.Platforms),
Location: t.Location.DeepCopy(), Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(), Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
} }
return c return c
} }

View File

@@ -6,6 +6,8 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
) )
// NamespaceSeparator contains the character that separates namespaces // NamespaceSeparator contains the character that separates namespaces
@@ -13,6 +15,9 @@ const NamespaceSeparator = ":"
var V3 = semver.MustParse("3") var V3 = semver.MustParse("3")
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
// Taskfile is the abstract syntax tree for a Taskfile // Taskfile is the abstract syntax tree for a Taskfile
type Taskfile struct { type Taskfile struct {
Location string Location string
@@ -36,6 +41,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if !t1.Version.Equal(t2.Version) { if !t1.Version.Equal(t2.Version) {
return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
} }
if len(t2.Dotenv) > 0 {
return ErrIncludedTaskfilesCantHaveDotenvs
}
if t2.Output.IsSet() { if t2.Output.IsSet() {
t1.Output = t2.Output t1.Output = t2.Output
} }
@@ -45,9 +53,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Env == nil { if t1.Env == nil {
t1.Env = &Vars{} t1.Env = &Vars{}
} }
t1.Vars.Merge(t2.Vars) t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env) t1.Env.Merge(t2.Env, include)
t1.Tasks.Merge(t2.Tasks, include) t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
return nil return nil
} }
@@ -70,7 +78,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Interval time.Duration Interval time.Duration
} }
if err := node.Decode(&taskfile); err != nil { if err := node.Decode(&taskfile); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
tf.Version = taskfile.Version tf.Version = taskfile.Version
tf.Output = taskfile.Output tf.Output = taskfile.Output
@@ -94,5 +102,5 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile")
} }

View File

@@ -6,6 +6,8 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/omap"
) )
@@ -44,8 +46,8 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
return matchingTasks return matchingTasks
} }
func (t1 *Tasks) Merge(t2 Tasks, include *Include) { func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
_ = t2.Range(func(k string, v *Task) error { _ = t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can // We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged. // be changed elsewhere once the taskfile is merged.
task := v.DeepCopy() task := v.DeepCopy()
@@ -54,20 +56,25 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include) {
// taskfile are marked as internal // taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal) task.Internal = task.Internal || (include != nil && include.Internal)
// Add namespaces to dependencies, commands and aliases // Add namespaces to task dependencies
for _, dep := range task.Deps { for _, dep := range task.Deps {
if dep != nil && dep.Task != "" { if dep != nil && dep.Task != "" {
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace) dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
} }
} }
// Add namespaces to task commands
for _, cmd := range task.Cmds { for _, cmd := range task.Cmds {
if cmd != nil && cmd.Task != "" { if cmd != nil && cmd.Task != "" {
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace) cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
} }
} }
// Add namespaces to task aliases
for i, alias := range task.Aliases { for i, alias := range task.Aliases {
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace) task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
} }
// Add namespace aliases // Add namespace aliases
if include != nil { if include != nil {
for _, namespaceAlias := range include.Aliases { for _, namespaceAlias := range include.Aliases {
@@ -78,8 +85,18 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include) {
} }
} }
if include.AdvancedImport {
task.Dir = filepathext.SmartJoin(include.Dir, task.Dir)
if task.IncludeVars == nil {
task.IncludeVars = &Vars{}
}
task.IncludeVars.Merge(include.Vars, nil)
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
}
// Add the task to the merged taskfile // Add the task to the merged taskfile
taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace) taskNameWithNamespace := taskNameWithNamespace(name, include.Namespace)
task.Namespace = include.Namespace
task.Task = taskNameWithNamespace task.Task = taskNameWithNamespace
t1.Set(taskNameWithNamespace, task) t1.Set(taskNameWithNamespace, task)
@@ -103,7 +120,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode: case yaml.MappingNode:
tasks := omap.New[string, *Task]() tasks := omap.New[string, *Task]()
if err := node.Decode(&tasks); err != nil { if err := node.Decode(&tasks); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
// nolint: errcheck // nolint: errcheck
@@ -135,7 +152,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks")
} }
func taskNameWithNamespace(taskName string, namespace string) string { func taskNameWithNamespace(taskName string, namespace string) string {

View File

@@ -1,11 +1,11 @@
package ast package ast
import ( import (
"fmt"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/omap"
) )
@@ -45,11 +45,17 @@ func (vs *Vars) Range(f func(k string, v Var) error) error {
} }
// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors // Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors
func (vs *Vars) Merge(other *Vars) { func (vs *Vars) Merge(other *Vars, include *Include) {
if vs == nil || other == nil { if vs == nil || other == nil {
return return
} }
vs.OrderedMap.Merge(other.OrderedMap) _ = other.Range(func(key string, value Var) error {
if include != nil && include.AdvancedImport {
value.Dir = include.Dir
}
vs.Set(key, value)
return nil
})
} }
// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors // Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors
@@ -77,19 +83,17 @@ type Var struct {
Live any Live any
Sh string Sh string
Ref string Ref string
Json string
Yaml string
Dir string Dir string
} }
func (v *Var) UnmarshalYAML(node *yaml.Node) error { func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.AnyVariables.Enabled { if experiments.MapVariables.Enabled {
// This implementation is not backwards-compatible and replaces the 'sh' key with map variables // This implementation is not backwards-compatible and replaces the 'sh' key with map variables
if experiments.AnyVariables.Value == "1" { if experiments.MapVariables.Value == "1" {
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
// If the value is a string and it starts with $, then it's a shell command // If the value is a string and it starts with $, then it's a shell command
if str, ok := value.(string); ok { if str, ok := value.(string); ok {
@@ -97,41 +101,41 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
v.Sh = str v.Sh = str
return nil return nil
} }
if str, ok = strings.CutPrefix(str, "#"); ok {
v.Ref = str
return nil
}
} }
v.Value = value v.Value = value
return nil return nil
} }
// This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key // This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key
if experiments.AnyVariables.Value == "2" { if experiments.MapVariables.Value == "2" {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
key := node.Content[0].Value key := node.Content[0].Value
switch key { switch key {
case "sh", "ref", "map", "json", "yaml": case "sh", "ref", "map":
var m struct { var m struct {
Sh string Sh string
Ref string Ref string
Map any Map any
Json string
Yaml string
} }
if err := node.Decode(&m); err != nil { if err := node.Decode(&m); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Sh = m.Sh v.Sh = m.Sh
v.Ref = m.Ref v.Ref = m.Ref
v.Value = m.Map v.Value = m.Map
v.Json = m.Json
v.Yaml = m.Yaml
return nil return nil
default: default:
return fmt.Errorf(`yaml: line %d: %q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, node.Line, key) return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
} }
default: default:
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Value = value v.Value = value
return nil return nil
@@ -141,24 +145,30 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return err
}
v.Value = str
return nil
case yaml.MappingNode: case yaml.MappingNode:
var sh struct { key := node.Content[0].Value
Sh string switch key {
case "sh", "ref":
var m struct {
Sh string
Ref string
}
if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
return nil
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables")
} }
if err := node.Decode(&sh); err != nil {
return err default:
var value any
if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node)
} }
v.Sh = sh.Sh v.Value = value
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variable", node.Line, node.ShortTag())
} }

View File

@@ -50,9 +50,23 @@ func (c *Cache) key(node Node) string {
} }
func (c *Cache) cacheFilePath(node Node) string { func (c *Cache) cacheFilePath(node Node) string {
return filepath.Join(c.dir, fmt.Sprintf("%s.yaml", c.key(node))) return c.filePath(node, "yaml")
} }
func (c *Cache) checksumFilePath(node Node) string { func (c *Cache) checksumFilePath(node Node) string {
return filepath.Join(c.dir, fmt.Sprintf("%s.checksum", c.key(node))) return c.filePath(node, "checksum")
}
func (c *Cache) filePath(node Node, suffix string) string {
lastDir, filename := node.FilenameAndLastDir()
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)
}
return filepath.Join(c.dir, fmt.Sprintf("%s.%s.%s", prefix, c.key(node), suffix))
}
func (c *Cache) Clear() error {
return os.RemoveAll(c.dir)
} }

View File

@@ -17,10 +17,10 @@ type Node interface {
Parent() Node Parent() Node
Location() string Location() string
Dir() string Dir() string
Optional() bool
Remote() bool Remote() bool
ResolveEntrypoint(entrypoint string) (string, error) ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error) ResolveDir(dir string) (string, error)
FilenameAndLastDir() (string, string)
} }
func NewRootNode( func NewRootNode(
@@ -31,9 +31,8 @@ func NewRootNode(
timeout time.Duration, timeout time.Duration,
) (Node, error) { ) (Node, error) {
dir = getDefaultDir(entrypoint, dir) dir = getDefaultDir(entrypoint, dir)
// Check if there is something to read on STDIN // If the entrypoint is "-", we read from stdin
stat, _ := os.Stdin.Stat() if entrypoint == "-" {
if (stat.Mode()&os.ModeCharDevice) == 0 && stat.Size() > 0 {
return NewStdinNode(dir) return NewStdinNode(dir)
} }
return NewNode(l, entrypoint, dir, insecure, timeout) return NewNode(l, entrypoint, dir, insecure, timeout)

View File

@@ -2,22 +2,20 @@ package taskfile
type ( type (
NodeOption func(*BaseNode) NodeOption func(*BaseNode)
// BaseNode is a generic node that implements the Parent() and Optional() // BaseNode is a generic node that implements the Parent() methods of the
// methods of the NodeReader interface. It does not implement the Read() method // NodeReader interface. It does not implement the Read() method and it
// and it designed to be embedded in other node types so that this boilerplate // designed to be embedded in other node types so that this boilerplate code
// code does not need to be repeated. // does not need to be repeated.
BaseNode struct { BaseNode struct {
parent Node parent Node
optional bool dir string
dir string
} }
) )
func NewBaseNode(dir string, opts ...NodeOption) *BaseNode { func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
node := &BaseNode{ node := &BaseNode{
parent: nil, parent: nil,
optional: false, dir: dir,
dir: dir,
} }
// Apply options // Apply options
@@ -38,16 +36,6 @@ func (node *BaseNode) Parent() Node {
return node.parent return node.parent
} }
func WithOptional(optional bool) NodeOption {
return func(node *BaseNode) {
node.optional = optional
}
}
func (node *BaseNode) Optional() bool {
return node.optional
}
func (node *BaseNode) Dir() string { func (node *BaseNode) Dir() string {
return node.dir return node.dir
} }

View File

@@ -112,3 +112,7 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
entrypointDir := filepath.Dir(node.Entrypoint) entrypointDir := filepath.Dir(node.Entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *FileNode) FilenameAndLastDir() (string, string) {
return "", filepath.Base(node.Entrypoint)
}

View File

@@ -110,3 +110,8 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
entrypointDir := filepath.Dir(node.Dir()) entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *HTTPNode) FilenameAndLastDir() (string, string) {
dir, filename := filepath.Split(node.URL.Path)
return filepath.Base(dir), filename
}

View File

@@ -72,3 +72,7 @@ func (node *StdinNode) ResolveDir(dir string) (string, error) {
return filepathext.SmartJoin(node.Dir(), path), nil return filepathext.SmartJoin(node.Dir(), path), nil
} }
func (node *StdinNode) FilenameAndLastDir() (string, string) {
return "", "__stdin__"
}

View File

@@ -6,9 +6,12 @@ import (
"os" "os"
"time" "time"
"github.com/dominikbraun/graph"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
@@ -24,33 +27,83 @@ Continue?`
Continue?` Continue?`
) )
// Read reads a Read for a given directory // A Reader will recursively read Taskfiles from a given source using a directed
// Uses current dir when dir is left empty. Uses Read.yml // acyclic graph (DAG).
// or Read.yaml when entrypoint is left empty type Reader struct {
func Read( graph *ast.TaskfileGraph
node Node
insecure bool
download bool
offline bool
timeout time.Duration
tempDir string
logger *logger.Logger
}
func NewReader(
node Node, node Node,
insecure bool, insecure bool,
download bool, download bool,
offline bool, offline bool,
timeout time.Duration, timeout time.Duration,
tempDir string, tempDir string,
l *logger.Logger, logger *logger.Logger,
) (*ast.Taskfile, error) { ) *Reader {
var _taskfile func(Node) (*ast.Taskfile, error) return &Reader{
_taskfile = func(node Node) (*ast.Taskfile, error) { graph: ast.NewTaskfileGraph(),
tf, err := readTaskfile(node, download, offline, timeout, tempDir, l) node: node,
if err != nil { insecure: insecure,
return nil, err download: download,
} offline: offline,
timeout: timeout,
tempDir: tempDir,
logger: logger,
}
}
// Check that the Taskfile is set and has a schema version func (r *Reader) Read() (*ast.TaskfileGraph, error) {
if tf == nil || tf.Version == nil { // Recursively loop through each Taskfile, adding vertices/edges to the graph
return nil, &errors.TaskfileVersionCheckError{URI: node.Location()} if err := r.include(r.node); err != nil {
} return nil, err
}
err = tf.Includes.Range(func(namespace string, include ast.Include) error { return r.graph, nil
cache := &templater.Cache{Vars: tf.Vars} }
include = ast.Include{
func (r *Reader) include(node Node) error {
// Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{
URI: node.Location(),
Taskfile: nil,
}
// Add the included Taskfile to the DAG
// If the vertex already exists, we return early since its Taskfile has
// already been read and its children explored
if err := r.graph.AddVertex(vertex); err == graph.ErrVertexAlreadyExists {
return nil
} else if err != nil {
return err
}
// Read and parse the Taskfile from the file and add it to the vertex
var err error
vertex.Taskfile, err = r.readNode(node)
if err != nil {
return err
}
// Create an error group to wait for all included Taskfiles to be read
var g errgroup.Group
// Loop over each included taskfile
_ = vertex.Taskfile.Includes.Range(func(namespace string, include *ast.Include) error {
vars := compiler.GetEnviron()
vars.Merge(vertex.Taskfile.Vars, nil)
// Start a goroutine to process each included Taskfile
g.Go(func() error {
cache := &templater.Cache{Vars: vars}
include = &ast.Include{
Namespace: include.Namespace, Namespace: include.Namespace,
Taskfile: templater.Replace(include.Taskfile, cache), Taskfile: templater.Replace(include.Taskfile, cache),
Dir: templater.Replace(include.Dir, cache), Dir: templater.Replace(include.Dir, cache),
@@ -69,14 +122,13 @@ func Read(
return err return err
} }
dir, err := node.ResolveDir(include.Dir) include.Dir, err = node.ResolveDir(include.Dir)
if err != nil { if err != nil {
return err return err
} }
includeReaderNode, err := NewNode(l, entrypoint, dir, insecure, timeout, includeNode, err := NewNode(r.logger, entrypoint, include.Dir, r.insecure, r.timeout,
WithParent(node), WithParent(node),
WithOptional(include.Optional),
) )
if err != nil { if err != nil {
if include.Optional { if include.Optional {
@@ -85,106 +137,72 @@ func Read(
return err return err
} }
if err := checkCircularIncludes(includeReaderNode); err != nil { // Recurse into the included Taskfile
if err := r.include(includeNode); err != nil {
return err return err
} }
includedTaskfile, err := _taskfile(includeReaderNode) // Create an edge between the Taskfiles
if err != nil { r.graph.Lock()
if include.Optional { defer r.graph.Unlock()
return nil edge, err := r.graph.Edge(node.Location(), includeNode.Location())
} if err == graph.ErrEdgeNotFound {
return err // If the edge doesn't exist, create it
err = r.graph.AddEdge(
node.Location(),
includeNode.Location(),
graph.EdgeData([]*ast.Include{include}),
graph.EdgeWeight(1),
)
} else {
// If the edge already exists
edgeData := append(edge.Properties.Data.([]*ast.Include), include)
err = r.graph.UpdateEdge(
node.Location(),
includeNode.Location(),
graph.EdgeData(edgeData),
graph.EdgeWeight(len(edgeData)),
)
} }
if errors.Is(err, graph.ErrEdgeCreatesCycle) {
if len(includedTaskfile.Dotenv) > 0 { return errors.TaskfileCycleError{
return ErrIncludedTaskfilesCantHaveDotenvs Source: node.Location(),
} Destination: includeNode.Location(),
if include.AdvancedImport {
// nolint: errcheck
includedTaskfile.Vars.Range(func(k string, v ast.Var) error {
o := v
o.Dir = dir
includedTaskfile.Vars.Set(k, o)
return nil
})
// nolint: errcheck
includedTaskfile.Env.Range(func(k string, v ast.Var) error {
o := v
o.Dir = dir
includedTaskfile.Env.Set(k, o)
return nil
})
for _, task := range includedTaskfile.Tasks.Values() {
task.Dir = filepathext.SmartJoin(dir, task.Dir)
if task.IncludeVars == nil {
task.IncludeVars = &ast.Vars{}
}
task.IncludeVars.Merge(include.Vars)
task.IncludedTaskfileVars = includedTaskfile.Vars
} }
} }
return err
if err = tf.Merge(includedTaskfile, &include); err != nil {
return err
}
return nil
}) })
if err != nil { return nil
return nil, err })
}
for _, task := range tf.Tasks.Values() { // Wait for all the go routines to finish
// If the task is not defined, create a new one return g.Wait()
if task == nil {
task = &ast.Task{}
}
// Set the location of the taskfile for each task
if task.Location.Taskfile == "" {
task.Location.Taskfile = tf.Location
}
}
return tf, nil
}
return _taskfile(node)
} }
func readTaskfile( func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
node Node,
download,
offline bool,
timeout time.Duration,
tempDir string,
l *logger.Logger,
) (*ast.Taskfile, error) {
var b []byte var b []byte
var err error var err error
var cache *Cache var cache *Cache
if node.Remote() { if node.Remote() {
cache, err = NewCache(tempDir) cache, err = NewCache(r.tempDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
// If the file is remote and we're in offline mode, check if we have a cached copy // If the file is remote and we're in offline mode, check if we have a cached copy
if node.Remote() && offline { if node.Remote() && r.offline {
if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) { if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()} return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()}
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
l.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location()) r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location())
} else { } else {
downloaded := false downloaded := false
ctx, cf := context.WithTimeout(context.Background(), timeout) ctx, cf := context.WithTimeout(context.Background(), r.timeout)
defer cf() defer cf()
// Read the file // Read the file
@@ -192,16 +210,16 @@ func readTaskfile(
// If we timed out then we likely have a network issue // If we timed out then we likely have a network issue
if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) { if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) {
// If a download was requested, then we can't use a cached copy // If a download was requested, then we can't use a cached copy
if download { if r.download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: timeout} return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
} }
// Search for any cached copies // Search for any cached copies
if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) { if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: timeout, CheckedCache: true} return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout, CheckedCache: true}
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
l.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location()) r.logger.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location())
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} else { } else {
@@ -210,7 +228,7 @@ func readTaskfile(
// If the node was remote, we need to check the checksum // If the node was remote, we need to check the checksum
if node.Remote() && downloaded { if node.Remote() && downloaded {
l.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location()) r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location())
// Get the checksums // Get the checksums
checksum := checksum(b) checksum := checksum(b)
@@ -225,7 +243,7 @@ func readTaskfile(
prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location()) prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location())
} }
if prompt != "" { if prompt != "" {
if err := l.Prompt(logger.Yellow, prompt, "n", "y", "yes"); err != nil { if err := r.logger.Prompt(logger.Yellow, prompt, "n", "y", "yes"); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()} return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
} }
} }
@@ -237,7 +255,7 @@ func readTaskfile(
return nil, err return nil, err
} }
// Cache the file // Cache the file
l.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location()) r.logger.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location())
if err = cache.write(node, b); err != nil { if err = cache.write(node, b); err != nil {
return nil, err return nil, err
} }
@@ -245,33 +263,33 @@ func readTaskfile(
} }
} }
var t ast.Taskfile var tf ast.Taskfile
if err := yaml.Unmarshal(b, &t); err != nil { if err := yaml.Unmarshal(b, &tf); err != nil {
// Decode the taskfile and add the file info the any errors
taskfileInvalidErr := &errors.TaskfileDecodeError{}
if errors.As(err, &taskfileInvalidErr) {
return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2)
}
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
} }
t.Location = node.Location()
return &t, nil // Check that the Taskfile is set and has a schema version
} if tf.Version == nil {
return nil, &errors.TaskfileVersionCheckError{URI: node.Location()}
}
func checkCircularIncludes(node Node) error { // Set the taskfile/task's locations
if node == nil { tf.Location = node.Location()
return errors.New("task: failed to check for include cycle: node was nil") for _, task := range tf.Tasks.Values() {
} // If the task is not defined, create a new one
if node.Parent() == nil { if task == nil {
return errors.New("task: failed to check for include cycle: node.Parent was nil") task = &ast.Task{}
} }
curNode := node // Set the location of the taskfile for each task
location := node.Location() if task.Location.Taskfile == "" {
for curNode.Parent() != nil { task.Location.Taskfile = tf.Location
curNode = curNode.Parent()
curLocation := curNode.Location()
if curLocation == location {
return fmt.Errorf("task: include cycle detected between %s <--> %s",
curLocation,
node.Parent().Location(),
)
} }
} }
return nil
return &tf, nil
} }

View File

@@ -16,9 +16,6 @@ import (
) )
var ( var (
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
defaultTaskfiles = []string{ defaultTaskfiles = []string{
"Taskfile.yml", "Taskfile.yml",
"taskfile.yml", "taskfile.yml",
@@ -29,7 +26,6 @@ var (
"Taskfile.dist.yaml", "Taskfile.dist.yaml",
"taskfile.dist.yaml", "taskfile.dist.yaml",
} }
allowedContentTypes = []string{ allowedContentTypes = []string{
"text/plain", "text/plain",
"text/yaml", "text/yaml",

13
testdata/desc/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
version: 3
tasks:
build:
aliases:
- b
desc: |
Multi-line escription with alias which is super long long long long long long
another line
third line long long long long long long long long
test:
aliases:
- t
desc: Single line description with alias

0
testdata/empty_taskfile/Taskfile.yml vendored Normal file
View File

View File

@@ -14,6 +14,7 @@ tasks:
cmds: cmds:
- task: local - task: local
- task: global - task: global
- task: multiple_type
local: local:
vars: vars:
@@ -31,3 +32,11 @@ tasks:
BAR: overriden BAR: overriden
cmds: cmds:
- echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > global.txt - echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > global.txt
multiple_type:
env:
FOO: 1
BAR: true
BAZ: 1.1
cmds:
- echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > multiple_type.txt

View File

@@ -2,15 +2,15 @@ version: "3"
includes: includes:
included1: included1:
taskfile: include/Taskfile.include.yml taskfile: include/Taskfile.include1.yml
vars: vars:
VAR_1: included1-var1 VAR_1: included1-var1
included2: included2:
taskfile: include/Taskfile.include.yml taskfile: include/Taskfile.include2.yml
vars: vars:
VAR_1: included2-var1 VAR_1: included2-var1
included3: included3:
taskfile: include/Taskfile.include.yml taskfile: include/Taskfile.include3.yml
tasks: tasks:
task1: 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:
foo:
taskfile: ./foo/Taskfile.yaml
bar:
taskfile: ./bar/Taskfile.yaml
tasks:
stub:
cmds:
- echo 0

View File

@@ -0,0 +1,11 @@
version: "3"
vars:
DIR: bar
tasks:
pwd:
dir: ./{{ .DIR }}
cmds:
- echo "{{ .DIR }}"
- pwd

View File

@@ -0,0 +1,11 @@
version: "3"
vars:
DIR: foo
tasks:
pwd:
dir: ./{{ .DIR }}
cmds:
- echo "{{ .DIR }}"
- pwd

View File

@@ -1,10 +0,0 @@
version: "3"
vars:
MODULE_NAME: included
includes:
include: './{{.MODULE_NAME}}/Taskfile.yml'
include-with-dir:
taskfile: './{{.MODULE_NAME}}/Taskfile.yml'
dir: '{{.MODULE_NAME}}'

View File

@@ -0,0 +1,7 @@
version: "3"
vars:
MODULE_NAME: included
includes:
include: '../{{.MODULE_NAME}}/Taskfile.yml'

View File

@@ -0,0 +1,9 @@
version: "3"
vars:
MODULE_NAME: included
includes:
include-with-dir:
taskfile: '../{{.MODULE_NAME}}/Taskfile.yml'
dir: '../{{.MODULE_NAME}}'

View File

@@ -0,0 +1,4 @@
version: "3"
includes:
include-with-env-variable: '../{{.MODULE}}/Taskfile.yml'

View File

@@ -0,0 +1,11 @@
version: '3'
includes:
service-a: ./service-a
service-b: ./service-b
tasks:
build:
deps:
- service-a:build
- service-b:build

View File

@@ -0,0 +1,9 @@
version: '3'
tasks:
build:
run: once
cmds:
- echo "build library"
sources:
- src/**/*

View File

@@ -0,0 +1,15 @@
version: '3'
includes:
library:
taskfile: ../library/Taskfile.yml
dir: ../library
tasks:
build:
run: once
deps: [library:build]
cmds:
- echo "build a"
sources:
- src/**/*

View File

@@ -0,0 +1 @@
package main

View File

@@ -0,0 +1,15 @@
version: '3'
includes:
library:
taskfile: ../library/Taskfile.yml
dir: ../library
tasks:
build:
run: once
deps: [library:build]
cmds:
- echo "build b"
sources:
- src/**/*

View File

@@ -0,0 +1 @@
package main

74
testdata/var_references/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
version: '3'
vars:
GLOBAL_VAR: [1, 2, 2, 2, 3, 3, 4, 5]
tasks:
default:
- task: ref-cmd
- task: ref-dep
- task: ref-resolver
- task: ref-resolver-sh
ref-cmd:
vars:
VAR_REF:
ref: .GLOBAL_VAR
cmds:
- task: print-first
vars:
VAR:
ref: .VAR_REF
ref-dep:
vars:
VAR_REF:
ref: .GLOBAL_VAR
deps:
- task: print-first
vars:
VAR:
ref: .VAR_REF
ref-resolver:
vars:
VAR_REF:
ref: .GLOBAL_VAR
cmds:
- task: print-var
vars:
VAR:
ref: (index .VAR_REF 0)
ref-resolver-sh:
vars:
JSON_STRING:
sh: echo '{"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}'
JSON:
ref: "fromJson .JSON_STRING"
VAR_REF:
ref: .JSON
cmds:
- task: print-story
vars:
VAR:
ref: .VAR_REF
print-var:
cmds:
- echo "{{.VAR}}"
print-first:
cmds:
- echo "{{index .VAR 0}}"
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

@@ -8,18 +8,18 @@ tasks:
- task: ref - task: ref
- task: ref-sh - task: ref-sh
- task: ref-dep - task: ref-dep
- task: ref-resolver
- task: json - task: json
- task: yaml
map: map:
vars: vars:
MAP: MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
cmds: cmds:
- task: print-var - task: print-story
vars: vars:
VAR: VAR:
ref: MAP ref: .MAP
nested-map: nested-map:
vars: vars:
@@ -44,12 +44,12 @@ tasks:
MAP: MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF: MAP_REF:
ref: MAP ref: .MAP
cmds: cmds:
- task: print-var - task: print-story
vars: vars:
VAR: VAR:
ref: MAP_REF ref: .MAP_REF
ref-sh: ref-sh:
vars: vars:
@@ -58,48 +58,52 @@ tasks:
JSON: JSON:
json: "{{.JSON_STRING}}" json: "{{.JSON_STRING}}"
MAP_REF: MAP_REF:
ref: JSON ref: .JSON
cmds: cmds:
- task: print-var - task: print-story
vars: vars:
VAR: VAR:
ref: MAP_REF ref: .MAP_REF
ref-dep: ref-dep:
vars: vars:
MAP: MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
deps: 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 - task: print-var
vars: vars:
VAR: VAR:
ref: MAP ref: (index .MAP_REF.children 0).name
json: json:
vars: vars:
JSON_STRING: JSON_STRING:
sh: cat example.json sh: cat example.json
JSON: JSON:
json: "{{.JSON_STRING}}" ref: "fromJson .JSON_STRING"
cmds: cmds:
- task: print-var - task: print-story
vars: vars:
VAR: VAR:
ref: JSON ref: .JSON
yaml:
vars:
YAML_STRING:
sh: cat example.yaml
YAML:
yaml: "{{.YAML_STRING}}"
cmds:
- task: print-var
vars:
VAR:
ref: YAML
print-var: print-var:
cmds:
- echo "{{.VAR}}"
print-story:
cmds: cmds:
- >- - >-
echo "{{.VAR.name}} has {{len .VAR.children}} children called echo "{{.VAR.name}} has {{len .VAR.children}} children called

View File

@@ -72,6 +72,7 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
Location: origTask.Location, Location: origTask.Location,
Requires: origTask.Requires, Requires: origTask.Requires,
Watch: origTask.Watch, Watch: origTask.Watch,
Namespace: origTask.Namespace,
} }
new.Dir, err = execext.Expand(new.Dir) new.Dir, err = execext.Expand(new.Dir)
if err != nil { if err != nil {
@@ -104,9 +105,9 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
} }
new.Env = &ast.Vars{} new.Env = &ast.Vars{}
new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache)) new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil)
new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache)) new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil)
new.Env.Merge(templater.ReplaceVars(origTask.Env, cache)) new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil)
if evaluateShVars { if evaluateShVars {
err = new.Env.Range(func(k string, v ast.Var) error { err = new.Env.Range(func(k string, v ast.Var) error {
// If the variable is not dynamic, we can set it and return // If the variable is not dynamic, we can set it and return
@@ -164,17 +165,6 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
newCmd.Cmd = templater.Replace(cmd.Cmd, cache) newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
newCmd.Task = templater.Replace(cmd.Task, cache) newCmd.Task = templater.Replace(cmd.Task, cache)
newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache) newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache)
// Loop over the command's variables and resolve any references to other variables
err := cmd.Vars.Range(func(k string, v ast.Var) error {
if v.Ref != "" {
refVal := vars.Get(v.Ref)
newCmd.Vars.Set(k, refVal)
}
return nil
})
if err != nil {
return nil, err
}
new.Cmds = append(new.Cmds, newCmd) new.Cmds = append(new.Cmds, newCmd)
} }
} }
@@ -214,17 +204,6 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
newDep := dep.DeepCopy() newDep := dep.DeepCopy()
newDep.Task = templater.Replace(dep.Task, cache) newDep.Task = templater.Replace(dep.Task, cache)
newDep.Vars = templater.ReplaceVars(dep.Vars, cache) newDep.Vars = templater.ReplaceVars(dep.Vars, cache)
// Loop over the dep's variables and resolve any references to other variables
err := dep.Vars.Range(func(k string, v ast.Var) error {
if v.Ref != "" {
refVal := vars.Get(v.Ref)
newDep.Vars.Set(k, refVal)
}
return nil
})
if err != nil {
return nil, err
}
new.Deps = append(new.Deps, newDep) new.Deps = append(new.Deps, newDep)
} }
} }
@@ -243,8 +222,8 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
} }
if len(origTask.Status) > 0 { if len(origTask.Status) > 0 {
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir, e.Dry) timestampChecker := fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir, e.Dry) checksumChecker := fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} { for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
value, err := checker.Value(&new) value, err := checker.Value(&new)

View File

@@ -16,7 +16,7 @@ communicate these kinds of thoughts to the community. So, with that in mind,
this is the first (hopefully of many) blog posts talking about Task and what this is the first (hopefully of many) blog posts talking about Task and what
we're up to. we're up to.
<!--truncate--> {/* truncate */}
## :calendar: So, what have we been up to? ## :calendar: So, what have we been up to?
@@ -122,7 +122,7 @@ I plan to write more of these blog posts in the future on a variety of
Task-related topics, so make sure to check in occasionally and see what we're up Task-related topics, so make sure to check in occasionally and see what we're up
to! to!
<!-- prettier-ignore-start --> {/* prettier-ignore-start */}
[vscode-task]: https://github.com/go-task/vscode-task [vscode-task]: https://github.com/go-task/vscode-task
[crowdin]: https://crowdin.com [crowdin]: https://crowdin.com
[contributors]: https://github.com/go-task/task/graphs/contributors [contributors]: https://github.com/go-task/task/graphs/contributors
@@ -139,4 +139,4 @@ to!
[experiments-project]: https://github.com/orgs/go-task/projects/1 [experiments-project]: https://github.com/orgs/go-task/projects/1
[gentle-force-experiment]: https://github.com/go-task/task/issues/1200 [gentle-force-experiment]: https://github.com/go-task/task/issues/1200
[remote-taskfiles-experiment]: https://github.com/go-task/task/issues/1317 [remote-taskfiles-experiment]: https://github.com/go-task/task/issues/1317
<!-- prettier-ignore-end --> {/* prettier-ignore-end */}

View File

@@ -0,0 +1,181 @@
---
title: Any Variables
description: Task variables are no longer limited to strings!
slug: any-variables
authors: [pd93]
tags: [experiments, variables]
image: https://i.imgur.com/mErPwqL.png
hide_table_of_contents: false
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
Task has always had variables, but even though you were able to define them
using different YAML types, they would always be converted to strings by Task.
This limited users to string manipulation and encouraged messy workarounds for
simple problems. Starting from [v3.37.0][v3.37.0], this is no longer the case!
Task now supports most variable types, including **booleans**, **integers**,
**floats** and **arrays**!
{/* truncate */}
## What's the big deal?
These changes allow you to use variables in a much more natural way and opens up
a wide variety of sprig functions that were previously useless. Take a look at
some of the examples below for some inspiration.
### Evaluating booleans
No more comparing strings to "true" or "false". Now you can use actual boolean
values in your templates:
<Tabs defaultValue="2"
values={[
{label: 'Before', value: '1'},
{label: 'After', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
BOOL: true # <-- Parsed as a string even though its a YAML boolean
cmds:
- '{{if eq .BOOL "true"}}echo foo{{end}}'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
BOOL: true # <-- Parsed as a boolean
cmds:
- '{{if .BOOL}}echo foo{{end}}' # <-- No need to compare to "true"
```
</TabItem></Tabs>
### Arithmetic
You can now perform basic arithmetic operations on integer and float variables:
```yaml
version: 3
tasks:
foo:
vars:
INT: 10
FLOAT: 3.14159
cmds:
- 'echo {{add .INT .FLOAT}}'
```
You can use any of the following arithmetic functions: `add`, `sub`, `mul`,
`div`, `mod`, `max`, `min`, `floor`, `ceil`, `round` and `randInt`. Check out
the [slim-sprig math documentation][slim-sprig-math] for more information.
### Arrays
You can now range over arrays inside templates and use list-based functions:
```yaml
version: 3
tasks:
foo:
vars:
ARRAY: [1, 2, 3]
cmds:
- 'echo {{range .ARRAY}}{{.}}{{end}}'
```
You can use any of the following list-based functions: `first`, `rest`, `last`,
`initial`, `append`, `prepend`, `concat`, `reverse`, `uniq`, `without`, `has`,
`compact`, `slice` and `chunk`. Check out the [slim-sprig lists
documentation][slim-sprig-list] for more information.
### Looping over variables using `for`
Previously, you would have to use a delimiter separated string to loop over an
arbitrary list of items in a variable and split them by using the `split` subkey
to specify the delimiter. However, we have now added support for looping over
"collection-type" variables using the `for` keyword, so now you are able to loop
over list variables directly:
<Tabs defaultValue="2"
values={[
{label: 'Before', value: '1'},
{label: 'After', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
LIST: 'foo,bar,baz'
cmds:
- for:
var: LIST
split: ','
cmd: echo {{.ITEM}}
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
LIST: ['foo', 'bar', 'baz']
cmds:
- for:
var: LIST
cmd: echo {{.ITEM}}
```
</TabItem></Tabs>
## What about maps?
Maps were originally included in the Any Variables experiment. However, they
weren't quite ready yet. Instead of making you wait for everything to be ready
at once, we have released support for all other variable types and we will
continue working on map support in the new "[Map Variables][map-variables]"
experiment.
:::note
If you were previously using maps with the Any Variables experiment and wish to
continue using them, you will need to enable the new [Map Variables
experiment][map-variables] instead.
:::
We're looking for feedback on a couple of different proposals, so please give
them a go and let us know what you think. :pray:
{/* prettier-ignore-start */}
[v3.37.0]: https://github.com/go-task/task/releases/tag/v3.37.0
[slim-sprig-math]: https://go-task.github.io/slim-sprig/math.html
[slim-sprig-list]: https://go-task.github.io/slim-sprig/lists.html
[map-variables]: /experiments/map-variables
{/* prettier-ignore-end */}

View File

@@ -2,3 +2,5 @@ export const GITHUB_URL = 'https://github.com/go-task/task';
export const TWITTER_URL = 'https://twitter.com/taskfiledev'; export const TWITTER_URL = 'https://twitter.com/taskfiledev';
export const MASTODON_URL = 'https://fosstodon.org/@task'; export const MASTODON_URL = 'https://fosstodon.org/@task';
export const DISCORD_URL = 'https://discord.gg/6TY36E39UK'; export const DISCORD_URL = 'https://discord.gg/6TY36E39UK';
export const STACK_OVERFLOW = 'https://stackoverflow.com/questions/tagged/taskfile';
export const ANSWER_OVERFLOW = 'https://www.answeroverflow.com/c/974121106208354339';

View File

@@ -5,6 +5,76 @@ sidebar_position: 14
# Changelog # Changelog
## v3.38.0 - 2024-06-30
- Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering).
- Some YAML parsing errors will now show in a more user friendly way (#1619 by
@pd93).
- Prefixed outputs will now be colorized by default (#1572 by
@AlexanderArvidsson)
- [References](https://taskfile.dev/usage/#referencing-other-variables) are now
generally available (no experiments required) (#1654 by @pd93).
- Templating functions can now be used in references (#1645, #1654 by @pd93).
- Added a new
[templating reference page](https://taskfile.dev/reference/templating/) to the
documentation (#1614, #1653 by @pd93).
- If using the
[Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1),
references are available by
[prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references)
(#1654 by @pd93).
- If using the
[Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2),
the `yaml` and `json` keys are no longer available (#1654 by @pd93).
- Added a new `TASK_REMOTE_DIR` environment variable to configure where cached
remote Taskfiles are stored (#1661 by @vmaerten).
- Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639
by @vmaerten).
- Improved the readability of cached remote Taskfile filenames (#1636 by
@vmaerten).
- Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by
@mengzhuo).
- Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith).
- Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93).
- Fixed JSON Schema to disallow invalid properties (#1657 by @pd93).
- Fixed version checks not working as intended (#872, #1663 by @vmaerten).
- Fixed a bug where included tasks were run multiple times even if `run: once`
was set (#852, #1655 by @pd93).
- Fixed some bugs related to column formatting in the terminal (#1350, #1637,
#1656 by @vmaerten).
## v3.37.2 - 2024-05-12
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
- Fixed a bug where includes Taskfile variable were not being merged correctly
(#1643, #1649 by @pd93).
## v3.37.1 - 2024-05-09
- Fix bug where non-string values (numbers, bools) added to `env:` weren't been
correctly exported (#1640, #1641 by @vmaerten and @andreynering).
## v3.37.0 - 2024-05-08
- Released the
[Any Variables experiment](https://taskfile.dev/blog/any-variables), but
[_without support for maps_](https://github.com/go-task/task/issues/1415#issuecomment-2044756925)
(#1415, #1547 by @pd93).
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
#1607 by @pd93).
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
@pd93).
- Fix error when a file or directory in the project contained a special char
like `&`, `(` or `)` (#1551, #1584 by @andreynering).
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)
- Added support for `~` on ZSH completions (#1613 by @jwater7).
- Added the ability to pass variables by reference using Go template syntax when
the
[Map Variables experiment](https://taskfile.dev/experiments/map-variables/) is
enabled (#1612 by @pd93).
- Added support for environment variables in the templating engine in `includes`
(#1610 by @vmaerten).
## v3.36.0 - 2024-04-08 ## v3.36.0 - 2024-04-08
- Added support for - Added support for

View File

@@ -148,6 +148,8 @@ If you have questions, feel free to ask them in the `#help` forum channel on our
--- ---
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[experiments]: /experiments
[experiments-workflow]: /experiments#workflow
[task]: https://github.com/go-task/task [task]: https://github.com/go-task/task
[vscode-task]: https://github.com/go-task/vscode-task [vscode-task]: https://github.com/go-task/vscode-task
[go]: https://go.dev [go]: https://go.dev

View File

@@ -0,0 +1,23 @@
---
slug: /deprecations/template-functions/
---
# Template Functions
:::warning
This deprecation breaks the following functionality:
- A small set of templating functions
:::
The following templating functions are deprecated. Any replacement functions are
listed besides the function being removed.
| Deprecated function | Replaced by |
| ------------------- | ----------- |
| `IsSH` | - |
| `FromSlash` | `fromSlash` |
| `ToSlash` | `toSlash` |
| `ExeExt` | `exeExt` |

View File

@@ -1,346 +0,0 @@
---
slug: /experiments/any-variables/
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Any Variables (#1415)
:::caution
All experimental features are subject to breaking changes and/or removal _at any
time_. We strongly recommend that you do not use these features in a production
environment. They are intended for testing and feedback only.
:::
Currently, Task only supports string variables. This experiment allows you to
specify and use the following variable types:
- `string`
- `bool`
- `int`
- `float`
- `array`
- `map`
This allows you to have a lot more flexibility in how you use variables in
Task's templating engine. There are two active proposals for this experiment.
Click on the tabs below to switch between them.
<Tabs defaultValue="1" queryString="proposal"
values={[
{label: 'Proposal 1', value: '1'},
{label: 'Proposal 2', value: '2'}
]}>
<TabItem value="1">
:::warning
This experiment proposal breaks the following functionality:
- Dynamically defined variables (using the `sh` keyword)
:::
:::info
To enable this experiment, set the environment variable:
`TASK_X_ANY_VARIABLES=1`. Check out [our guide to enabling experiments
][enabling-experiments] for more information.
:::
## Maps
This proposal removes support for the `sh` keyword in favour of a new syntax for
dynamically defined variables, This allows you to define a map directly as you
would for any other type:
```yaml
version: 3
tasks:
foo:
vars:
FOO: {a: 1, b: 2, c: 3} # <-- Directly defined map on the `FOO` key
cmds:
- 'echo {{.FOO.a}}'
```
## Migration
Taskfiles with dynamically defined variables via the `sh` subkey will no longer
work with this experiment enabled. In order to keep using dynamically defined
variables, you will need to migrate your Taskfile to use the new syntax.
Previously, you might have defined a dynamic variable like this:
```yaml
version: 3
tasks:
foo:
vars:
CALCULATED_VAR:
sh: 'echo hello'
cmds:
- 'echo {{.CALCULATED_VAR}}'
```
With this experiment enabled, you will need to remove the `sh` subkey and define
your command as a string that begins with a `$`. This will instruct Task to
interpret the string as a command instead of a literal value and the variable
will be populated with the output of the command. For example:
```yaml
version: 3
tasks:
foo:
vars:
CALCULATED_VAR: '$echo hello'
cmds:
- 'echo {{.CALCULATED_VAR}}'
```
If your current Taskfile contains a string variable that begins with a `$`, you
will now need to escape the `$` with a backslash (`\`) to stop Task from
executing it as a command.
</TabItem>
<TabItem value="2">
:::info
To enable this experiment, set the environment variable:
`TASK_X_ANY_VARIABLES=2`. Check out [our guide to enabling experiments
][enabling-experiments] for more information.
:::
## Maps
This proposal maintains backwards-compatibility and the `sh` subkey and adds
another new `map` subkey for defining map variables:
```yaml
version: 3
tasks:
foo:
vars:
FOO:
map: {a: 1, b: 2, c: 3} # <-- Defined using the `map' subkey instead of directly on 'FOO'
BAR: true # <-- Other types of variables are still defined directly on the key
BAZ:
sh: 'echo Hello Task' # <-- The `sh` subkey is still supported
cmds:
- 'echo {{.FOO.a}}'
```
## Parsing JSON and YAML
In addition to the new `map` keyword, this proposal also adds support for the
`json` and `yaml` keywords for parsing JSON and YAML strings into real
objects/arrays. This is similar to the `fromJSON` template function, but means
that you only have to parse the JSON/YAML once when you declare the variable,
instead of every time you want to access a value.
Before:
```yaml
version: 3
tasks:
foo:
vars:
FOO: '{"a": 1, "b": 2, "c": 3}' # <-- JSON string
cmds:
- 'echo {{(fromJSON .FOO).a}}' # <-- Parse JSON string every time you want to access a value
- 'echo {{(fromJSON .FOO).b}}'
```
After:
```yaml
version: 3
tasks:
foo:
vars:
FOO:
json: '{"a": 1, "b": 2, "c": 3}' # <-- JSON string parsed once
cmds:
- 'echo {{.FOO.a}}' # <-- Access values directly
- 'echo {{.FOO.b}}'
```
## Variables by reference
Lastly, this proposal adds support for defining and passing variables by
reference. This is really important now that variables can be types other than a
string. Previously, to send a variable from one task to another, you would have
to use the templating system to pass it:
```yaml
version: 3
tasks:
foo:
vars:
FOO: [A, B, C] # <-- FOO is defined as an array
cmds:
- task: bar
vars:
FOO: '{{.FOO}}' # <-- FOO gets converted to a string when passed to bar
bar:
cmds:
- 'echo {{index .FOO 0}}' # <-- FOO is a string so the task outputs '91' which is the ASCII code for '[' instead of the expected 'A'
```
Unfortunately, this results in the value always being passed as a string as this
is the output type of the templater and operations on the passed variable may
not behave as expected. With this proposal, you can now pass variables by
reference using the `ref` subkey:
```yaml
version: 3
tasks:
foo:
vars:
FOO: [A, B, C] # <-- FOO is defined as an array
cmds:
- task: bar
vars:
FOO:
ref: FOO # <-- FOO gets passed by reference to bar and maintains its type
bar:
cmds:
- 'echo {{index .FOO 0}}' # <-- FOO is still a map so the task outputs 'A' as expected
```
This means that the type of the variable is maintained when it is passed to
another Task. This also works the same way when calling `deps` and when defining
a variable and can be used in any combination:
```yaml
version: 3
tasks:
foo:
vars:
FOO: [A, B, C] # <-- FOO is defined as an array
BAR:
ref: FOO # <-- BAR is defined as a reference to FOO
deps:
- task: bar
vars:
BAR:
ref: BAR # <-- BAR gets passed by reference to bar and maintains its type
bar:
cmds:
- 'echo {{index .BAR 0}}' # <-- BAR still refers to FOO so the task outputs 'A'
```
</TabItem></Tabs>
---
## Common to both proposals
Both proposals add support for all other variable types by directly defining
them in the Taskfile. For example:
### Evaluating booleans
```yaml
version: 3
tasks:
foo:
vars:
BOOL: false
cmds:
- '{{if .BOOL}}echo foo{{end}}'
```
### Arithmetic
```yaml
version: 3
tasks:
foo:
vars:
INT: 10
FLOAT: 3.14159
cmds:
- 'echo {{add .INT .FLOAT}}'
```
### Ranging
```yaml
version: 3
tasks:
foo:
vars:
ARRAY: [1, 2, 3]
cmds:
- 'echo {{range .ARRAY}}{{.}}{{end}}'
```
There are many more templating functions which can be used with the new types of
variables. For a full list, see the [slim-sprig][slim-sprig] documentation.
## Looping over variables
Previously, you would have to use a delimiter separated string to loop over an
arbitrary list of items in a variable and split them by using the `split` subkey
to specify the delimiter:
```yaml
version: 3
tasks:
foo:
vars:
LIST: 'foo,bar,baz'
cmds:
- for:
var: LIST
split: ','
cmd: echo {{.ITEM}}
```
Both of these proposals add support for looping over "collection-type" variables
using the `for` keyword, so now you are able to loop over a map/array variable
directly:
```yaml
version: 3
tasks:
foo:
vars:
LIST: [foo, bar, baz]
cmds:
- for:
var: LIST
cmd: echo {{.ITEM}}
```
When looping over a map we also make an additional `{{.KEY}}` variable availabe
that holds the string value of the map key. Remember that maps are unordered, so
the order in which the items are looped over is random.
{/* prettier-ignore-start */}
[enabling-experiments]: /experiments/#enabling-experiments
[slim-sprig]: https://go-task.github.io/slim-sprig/
{/* prettier-ignore-end */}

View File

@@ -45,5 +45,5 @@ if you want to adopt the new behavior, you can continue to use the `--force`
flag as you do now! flag as you do now!
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[enabling-experiments]: /experiments/#enabling-experiments [enabling-experiments]: ./experiments.mdx#enabling-experiments
{/* prettier-ignore-end */} {/* prettier-ignore-end */}

View File

@@ -0,0 +1,245 @@
---
slug: /experiments/map-variables/
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Map Variables (#1585)
:::caution
All experimental features are subject to breaking changes and/or removal _at any
time_. We strongly recommend that you do not use these features in a production
environment. They are intended for testing and feedback only.
:::
Currently, Task supports all variable types except for maps. This experiment
adds two different proposals for map variables. Click on the tabs below to
switch between them.
<Tabs defaultValue="1" queryString="proposal"
values={[
{label: 'Proposal 1', value: '1'},
{label: 'Proposal 2', value: '2'}
]}>
<TabItem value="1">
:::warning
This experiment proposal breaks the following functionality:
- Dynamically defined variables (using the `sh` keyword)
:::
:::info
To enable this experiment, set the environment variable:
`TASK_X_MAP_VARIABLES=1`. Check out [our guide to enabling experiments
][enabling-experiments] for more information.
:::
This proposal removes support for the `sh` and `ref` keywords in favour of a new
syntax for dynamically defined variables and references. This allows you to
define a map directly as you would for any other type:
```yaml
version: 3
tasks:
foo:
vars:
FOO: {a: 1, b: 2, c: 3} # <-- Directly defined map on the `FOO` key
cmds:
- 'echo {{.FOO.a}}'
```
## Migration
Taskfiles with dynamically defined variables via the `sh` subkey or references
defined with `ref` will no longer work with this experiment enabled. In order to
keep using these features, you will need to migrate your Taskfile to use the new
syntax.
### Dynamic Variables
Previously, you had to define dynamic variables using the `sh` subkey. With this
experiment enabled, you will need to remove the `sh` subkey and define your
command as a string that begins with a `$`. This will instruct Task to interpret
the string as a command instead of a literal value and the variable will be
populated with the output of the command. For example:
<Tabs defaultValue="2"
values={[
{label: 'Before', value: '1'},
{label: 'After', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
CALCULATED_VAR:
sh: 'echo hello'
cmds:
- 'echo {{.CALCULATED_VAR}}'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
CALCULATED_VAR: '$echo hello' # <-- Prefix dynamic variable with a `$`
cmds:
- 'echo {{.CALCULATED_VAR}}'
```
</TabItem></Tabs>
### References
<Tabs defaultValue="2"
values={[
{label: 'Before', value: '1'},
{label: 'After', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
VAR: 42
VAR_REF:
ref: '.FOO'
cmds:
- 'echo {{.VAR_REF}}'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
VAR: 42
VAR_REF: '#.FOO' # <-- Prefix reference with a `#`
cmds:
- 'echo {{.VAR_REF}}'
```
</TabItem></Tabs>
If your current Taskfile contains a string variable that begins with a `$` or a
`#`, you will now need to escape it with a backslash (`\`) to stop Task from
interpreting it as a command or reference.
</TabItem>
<TabItem value="2">
:::info
To enable this experiment, set the environment variable:
`TASK_X_MAP_VARIABLES=2`. Check out [our guide to enabling experiments
][enabling-experiments] for more information.
:::
This proposal maintains backwards-compatibility and the `sh` subkey and adds
another new `map` subkey for defining map variables:
```yaml
version: 3
tasks:
foo:
vars:
FOO:
map: {a: 1, b: 2, c: 3} # <-- Defined using the `map' subkey instead of directly on 'FOO'
BAR: true # <-- Other types of variables are still defined directly on the key
BAZ:
sh: 'echo Hello Task' # <-- The `sh` subkey is still supported
QUX:
ref: '.BAZ' # <-- The `ref` subkey is still supported
cmds:
- 'echo {{.FOO.a}}'
```
</TabItem></Tabs>
## Looping over maps
This experiment also adds support for looping over maps using the `for` keyword,
just like arrays. In addition to the `{{.ITEM}}` variable being populated when
looping over a map, we also make an additional `{{.KEY}}` variable available
that holds the string value of the map key.
<Tabs defaultValue="1" queryString="proposal"
values={[
{label: 'Proposal 1', value: '1'},
{label: 'Proposal 2', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
MAP: {a: 1, b: 2, c: 3}
cmds:
- for:
var: MAP
cmd: 'echo "{{.KEY}}: {{.ITEM}}"'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
map:
MAP: {a: 1, b: 2, c: 3}
cmds:
- for:
var: MAP
cmd: 'echo "{{.KEY}}: {{.ITEM}}"'
```
:::note
Remember that maps are unordered, so
the order in which the items are looped over is random.
:::
</TabItem></Tabs>
{/* prettier-ignore-start */}
[enabling-experiments]: ./experiments.mdx#enabling-experiments
{/* prettier-ignore-end */}

View File

@@ -48,6 +48,20 @@ tasks:
and you run `task my-remote-namespace:hello`, it will print the text: "Hello and you run `task my-remote-namespace:hello`, it will print the text: "Hello
from the remote Taskfile!" to your console. from the remote Taskfile!" to your console.
The Taskfile location is processed by the templating system, so you can
reference environment variables in your URL if you need to add authentication.
For example:
```yaml
version: '3'
includes:
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
```
`TOKEN=my-token task my-remote-namespace:hello` will be resolved by Task to
`https://my-token@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml`
## Security ## Security
Running commands from sources that you do not control is always a potential Running commands from sources that you do not control is always a potential
@@ -91,14 +105,21 @@ internet and cached locally. If for whatever reason, you lose access to the
internet, you will still be able to run your tasks by specifying the `--offline` internet, you will still be able to run your tasks by specifying the `--offline`
flag. This will tell Task to use the latest cached version of the file instead flag. This will tell Task to use the latest cached version of the file instead
of trying to download it. You are able to use the `--download` flag to update of trying to download it. You are able to use the `--download` flag to update
the cached version of the remote files without running any tasks. the cached version of the remote files without running any tasks. You are able
to use the `--clear-cache` flag to clear all cached version of the remote files
without running any tasks.
By default, Task will timeout requests to download remote files after 10 seconds By default, Task will timeout requests to download remote files after 10 seconds
and look for a cached copy instead. This timeout can be configured by setting and look for a cached copy instead. This timeout can be configured by setting
the `--timeout` flag and specifying a duration. For example, `--timeout 5s` will the `--timeout` flag and specifying a duration. For example, `--timeout 5s` will
set the timeout to 5 seconds. set the timeout to 5 seconds.
By default, the cache is stored in the Task temp directory, represented by the
`TASK_TEMP_DIR` [environment variable](../reference/environment.mdx) You can
override the location of the cache by setting the `TASK_REMOTE_DIR` environment
variable. This way, you can share the cache between different projects.
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[enabling-experiments]: /experiments/#enabling-experiments [enabling-experiments]: ./experiments.mdx#enabling-experiments
[man-in-the-middle-attacks]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack [man-in-the-middle-attacks]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
{/* prettier-ignore-end */} {/* prettier-ignore-end */}

View File

@@ -38,5 +38,5 @@ information.
\{Short explanation of how users should migrate to the new behavior\} \{Short explanation of how users should migrate to the new behavior\}
{/* prettier-ignore-start */} {/* prettier-ignore-start */}
[enabling-experiments]: /experiments/#enabling-experiments [enabling-experiments]: ./experiments.mdx#enabling-experiments
{/* prettier-ignore-end */} {/* prettier-ignore-end */}

View File

@@ -7,6 +7,25 @@ sidebar_position: 15
This page contains a list of frequently asked questions about Task. This page contains a list of frequently asked questions about Task.
## When will \<feature\> be released? / ETAs
Task is _free_ and _open source_ project maintained by a small group of
volunteers with full time jobs and lives outside of the project. Because of
this, it is difficult to predict how much time we will be able to dedicate to
the project in advance and we don't want to make any promises that we can't
keep. For this reason, we are unable to provide ETAs for new features or
releases. We make a "best effort" to provide regular releases and fix bugs in a
timely fashion, but sometimes our personal lives must take priority.
ETAs are probably the number one question we (and maintainers of other open
source projects) get asked. We understand that you are passionate about the
project, but it can be overwhelming to be asked this question so often. Please
be patient and avoid asking for ETAs.
The best way to speed things up is to contribute to the project yourself. We
always appreciate new contributors. If you are interested in contributing, check
out the [contributing guide](./contributing.mdx).
## Why won't my task update my shell environment? ## Why won't my task update my shell environment?
This is a limitation of how shells work. Task runs as a subprocess of your This is a limitation of how shells work. Task runs as a subprocess of your

View File

@@ -31,7 +31,7 @@ brew install go-task
### pkgx ### pkgx
If you're on macOS or Linux and have [pkgx](https://pkgx.sh/) installed, getting Task is as If you're on macOS or Linux and have [pkgx][pkgx] installed, getting Task is as
simple as running: simple as running:
```shell ```shell
@@ -146,6 +146,15 @@ can install Task from [winget-pkgs](https://github.com/microsoft/winget-pkgs).
winget install Task.Task winget install Task.Task
``` ```
### Pacstall
If you are using Debian or Ubuntu, and have [Pacstall](https://pacstall.dev/) installed, you can install Task by running:
```shell
pacstall -I go-task-deb
```
This installation method is community owned. After a new release of Task, it may take some time until it's available in [Pacstall](https://pacstall.dev/packages/go-task-deb).
## Get The Binary ## Get The Binary
### Binary ### Binary
@@ -185,6 +194,14 @@ default.
::: :::
By default, it installs the latest version available.
You can also specify a tag (available in [releases](https://github.com/go-task/task/releases))
to install a specific version:
```shell
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d v3.36.0
```
### GitHub Actions ### GitHub Actions
If you want to install Task in GitHub Actions you can try using If you want to install Task in GitHub Actions you can try using
@@ -299,5 +316,5 @@ Invoke-Expression -Command path/to/task.ps1
[godownloader]: https://github.com/goreleaser/godownloader [godownloader]: https://github.com/goreleaser/godownloader
[choco]: https://chocolatey.org/ [choco]: https://chocolatey.org/
[scoop]: https://scoop.sh/ [scoop]: https://scoop.sh/
[tea]: https://tea.xyz/ [pkgx]: https://pkgx.sh/
{/* prettier-ignore-end */} {/* prettier-ignore-end */}

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