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
*.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.
>
> Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
<!--
Thanks for your pull request, we really appreciate contributions!
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]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v5
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.55.2
@@ -34,7 +34,7 @@ jobs:
with:
python-version: 3.12
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'go-task/task'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Verify changed files
id: changed-files
@@ -25,7 +25,7 @@ jobs:
website/src/pages
- name: Install Task
uses: arduino/setup-task@v1
uses: arduino/setup-task@v2
with:
version: 3.x
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
*.out
# Graphvis files
*.gv
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

View File

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

View File

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

View File

@@ -1,5 +1,75 @@
# 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
- Added support for

View File

@@ -123,10 +123,53 @@ tasks:
cmds:
- go install github.com/goreleaser/goreleaser@latest
release:
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:
- 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:
desc: Publish release to npm

View File

@@ -11,6 +11,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/otiai10/copy"
"github.com/spf13/pflag"
)
const (
@@ -25,6 +26,16 @@ var (
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() {
if err := release(); err != nil {
fmt.Println(err)
@@ -33,7 +44,7 @@ func main() {
}
func release() error {
if len(os.Args) != 2 {
if len(pflag.Args()) != 1 {
return errors.New("error: expected version number")
}
@@ -42,11 +53,14 @@ func release() error {
return err
}
if err := bumpVersion(version, os.Args[1]); err != nil {
if err := bumpVersion(version, pflag.Arg(0)); err != nil {
return err
}
fmt.Println(version)
if versionFlag {
fmt.Println(version)
return nil
}
if err := changelog(version); err != nil {
return err

View File

@@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"log"
"os"
"strings"
@@ -18,6 +17,7 @@ import (
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -44,8 +44,12 @@ func main() {
}
func run() error {
log.SetFlags(0)
log.SetOutput(os.Stderr)
logger := &logger.Logger{
Stdout: os.Stdout,
Stderr: os.Stderr,
Verbose: flags.Verbose,
Color: flags.Color,
}
if err := flags.Validate(); err != nil {
return err
@@ -55,7 +59,7 @@ func run() error {
entrypoint := flags.Entrypoint
if flags.Version {
fmt.Printf("Task version: %s\n", ver.GetVersion())
fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
return nil
}
@@ -65,22 +69,16 @@ func run() error {
}
if flags.Experiments {
l := &logger.Logger{
Stdout: os.Stdout,
Stderr: os.Stderr,
Verbose: flags.Verbose,
Color: flags.Color,
}
return experiments.List(l)
return experiments.List(logger)
}
if flags.Init {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
return err
}
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
log.Fatal(err)
return err
}
return nil
}
@@ -128,15 +126,18 @@ func run() error {
OutputStyle: flags.Output,
TaskSorter: taskSorter,
}
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil {
return err
}
if err := e.Setup(); err != nil {
err := e.Setup()
if err != nil {
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
// taskfile is downloaded
@@ -144,6 +145,14 @@ func run() error {
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 {
return e.ListTaskNames(flags.ListAll)
}
@@ -178,7 +187,9 @@ func run() error {
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
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 {
e.InterceptInterruptSignals()

View File

@@ -12,7 +12,9 @@ function __task_list() {
local taskfile item task desc
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
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 (
CodeTaskfileNotFound int = iota + 100
CodeTaskfileAlreadyExists
CodeTaskfileInvalid
CodeTaskfileDecode
CodeTaskfileFetchFailed
CodeTaskfileNotTrusted
CodeTaskfileNotSecure
CodeTaskfileCacheNotFound
CodeTaskfileVersionCheckError
CodeTaskfileNetworkTimeout
CodeTaskfileInvalid
CodeTaskfileCycle
)
// Task related exit codes
@@ -56,3 +58,8 @@ func Is(err, target error) bool {
func As(err error, target any) bool {
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 {
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
go 1.21
go 1.21.0
require (
github.com/Ladicle/tabwriter v1.0.0
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/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/template v0.0.0-20240602015157-960e6f576656
github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.4
github.com/mitchellh/hashstructure/v2 v2.0.2
@@ -17,17 +21,18 @@ require (
github.com/stretchr/testify v1.9.0
github.com/zeebo/xxh3 v1.0.2
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
mvdan.cc/sh/v3 v3.8.0
)
require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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
)

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/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/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
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/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/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/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/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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/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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

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

View File

@@ -7,8 +7,8 @@ import (
"io"
"os"
"strings"
"text/tabwriter"
"github.com/Ladicle/tabwriter"
"golang.org/x/sync/errgroup"
"github.com/go-task/task/v3/internal/editors"
@@ -105,7 +105,8 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
for _, task := range tasks {
e.Logger.FOutf(w, logger.Yellow, "* ")
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 {
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,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger),
)

View File

@@ -3,14 +3,12 @@ package compiler
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"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}
// Replace values
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
// This stops empty interface errors when using the templater to replace values later
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 {
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 newVar.Value != nil || newVar.Sh == "" {
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) {
return map[string]string{
"TASK": t.Task,
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": c.Dir,
"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()
for k, v := range t.Env.ToCacheMap() {
str, isString := v.(string)
if !isString {
if !isTypeAllowed(v) {
continue
}
if _, alreadySet := os.LookupEnv(k); alreadySet {
continue
}
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
environ = append(environ, fmt.Sprintf("%s=%v", k, v))
}
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) {
s = filepath.ToSlash(s)
s = strings.ReplaceAll(s, " ", `\ `)
s = strings.ReplaceAll(s, "&", `\&`)
s = strings.ReplaceAll(s, "(", `\(`)
s = strings.ReplaceAll(s, ")", `\)`)
fields, err := shell.Fields(s, nil)
if err != nil {
return "", err

View File

@@ -7,8 +7,8 @@ import (
"path/filepath"
"slices"
"strings"
"text/tabwriter"
"github.com/Ladicle/tabwriter"
"github.com/joho/godotenv"
"github.com/spf13/pflag"
@@ -28,6 +28,7 @@ var (
GentleForce Experiment
RemoteTaskfiles Experiment
AnyVariables Experiment
MapVariables Experiment
)
func init() {
@@ -35,6 +36,7 @@ func init() {
GentleForce = New("GENTLE_FORCE")
RemoteTaskfiles = New("REMOTE_TASKFILES")
AnyVariables = New("ANY_VARIABLES", "1", "2")
MapVariables = New("MAP_VARIABLES", "1", "2")
}
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)
printExperiment(w, l, GentleForce)
printExperiment(w, l, RemoteTaskfiles)
printExperiment(w, l, AnyVariables)
printExperiment(w, l, MapVariables)
return w.Flush()
}

View File

@@ -3,6 +3,7 @@ package flags
import (
"errors"
"log"
"os"
"time"
"github.com/spf13/pflag"
@@ -64,10 +65,13 @@ var (
Experiments bool
Download bool
Offline bool
ClearCache bool
Timeout time.Duration
)
func init() {
log.SetFlags(0)
log.SetOutput(os.Stderr)
pflag.Usage = func() {
log.Print(usage)
pflag.PrintDefaults()
@@ -116,6 +120,7 @@ func init() {
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.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
}
pflag.Parse()
@@ -126,6 +131,10 @@ func Validate() error {
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 != "" {
log.Fatal("task: You can't set both --global and --dir")
return nil

View File

@@ -15,7 +15,7 @@ func Empty(*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) {

View File

@@ -52,6 +52,30 @@ func Red() PrintFunc {
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 {
if os.Getenv("FORCE_COLOR") != "" {
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 {
if l.AssumeYes {
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")
}
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)
input, err := reader.ReadString('\n')

View File

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

View File

@@ -7,9 +7,11 @@ import (
"io"
"testing"
"github.com/fatih/color"
"github.com/stretchr/testify/assert"
"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/output"
"github.com/go-task/task/v3/internal/templater"
@@ -107,7 +109,11 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
func TestPrefixed(t *testing.T) {
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)
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())
})
}
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"
"strings"
"github.com/go-task/task/v3/internal/logger"
"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) {
pw := &prefixWriter{writer: stdOut, prefix: prefix}
func NewPrefixed(logger *logger.Logger) Prefixed {
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() }
}
type prefixWriter struct {
writer io.Writer
prefix string
buff bytes.Buffer
writer io.Writer
prefixed *Prefixed
prefix string
buff bytes.Buffer
}
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 {
if line == "" {
return nil
@@ -63,6 +84,27 @@ func (pw *prefixWriter) writeLine(line string) error {
if !strings.HasSuffix(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
}

View File

@@ -18,3 +18,13 @@ func UniqueJoin[T cmp.Ordered](ss ...[]T) []T {
slices.Sort(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"
"runtime"
"strings"
"text/template"
"github.com/davecgh/go-spew/spew"
"mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax"
sprig "github.com/go-task/slim-sprig/v3"
"github.com/go-task/template"
)
var templateFuncs template.FuncMap
@@ -73,12 +73,16 @@ func init() {
return spew.Sdump(v)
},
}
// aliases
taskFuncs["q"] = taskFuncs["shellQuote"]
// Deprecated aliases for renamed functions.
taskFuncs["FromSlash"] = taskFuncs["fromSlash"]
taskFuncs["ToSlash"] = taskFuncs["toSlash"]
taskFuncs["ExeExt"] = taskFuncs["exeExt"]
templateFuncs = sprig.TxtFuncMap()
templateFuncs = template.FuncMap(sprig.TxtFuncMap())
for k, v := range taskFuncs {
templateFuncs[k] = v
}

View File

@@ -2,12 +2,13 @@ package templater
import (
"bytes"
"fmt"
"maps"
"strings"
"text/template"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/template"
)
// Cache is a help struct that allow us to call "replaceX" funcs multiple
@@ -29,6 +30,33 @@ func (r *Cache) Err() error {
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 {
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 {
if v.Ref != "" {
return ast.Var{Value: ResolveRef(v.Ref, cache)}
}
return ast.Var{
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Json: ReplaceWithExtra(v.Json, cache, extra),
Yaml: ReplaceWithExtra(v.Yaml, cache, extra),
}
}

View File

@@ -5,21 +5,25 @@ import (
"runtime/debug"
)
var version = ""
func GetVersion() string {
if version != "" {
return version
}
var (
version = ""
sum = ""
)
func init() {
info, ok := debug.ReadBuildInfo()
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 != "" {
ver += fmt.Sprintf(" (%s)", info.Main.Sum)
}
return ver
}
func GetVersion() string {
return version
}
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",
"version": "3.36.0",
"version": "3.38.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.36.0",
"version": "3.38.0",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"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 {
var err error
e.Taskfile, err = taskfile.Read(
reader := taskfile.NewReader(
node,
e.Insecure,
e.Download,
e.Offline,
e.Timeout,
e.TempDir,
e.TempDir.Remote,
e.Logger,
)
graph, err := reader.Read()
if err != nil {
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
return err
}
return nil
}
@@ -101,12 +104,15 @@ func (e *Executor) setupFuzzyModel() {
}
func (e *Executor) setupTempDir() error {
if e.TempDir != "" {
if e.TempDir != (TempDir{}) {
return nil
}
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"), "~") {
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
if err != nil {
@@ -114,9 +120,28 @@ func (e *Executor) setupTempDir() error {
}
projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir)
e.TempDir = filepathext.SmartJoin(tempDir, projectName)
e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} 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
@@ -152,7 +177,7 @@ func (e *Executor) setupOutput() error {
}
var err error
e.Output, err = output.BuildFor(&e.OutputStyle)
e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
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
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger),
)
@@ -46,7 +46,7 @@ func (e *Executor) statusOnError(t *ast.Task) error {
if 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 {
return err
}

29
task.go
View File

@@ -34,13 +34,18 @@ const (
MaximumTaskCall = 1000
)
type TempDir struct {
Remote string
Fingerprint string
}
// Executor executes a Taskfile
type Executor struct {
Taskfile *ast.Taskfile
Dir string
Entrypoint string
TempDir string
TempDir TempDir
Force bool
ForceAll bool
Insecure bool
@@ -183,16 +188,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
release := e.acquireConcurrencyLimit()
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 {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
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,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry),
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 {
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{
Dir: fct.Dir,
TempDir: filepathext.SmartJoin(fct.Dir, ".task"),
Dir: fct.Dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(fct.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"),
},
Entrypoint: fct.Entrypoint,
Stdout: 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"}))
}
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) {
tt := fileContentTest{
Dir: "testdata/env",
Target: "default",
TrimSpace: false,
Files: map[string]string{
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
"multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n",
},
}
tt.Run(t)
@@ -262,11 +275,14 @@ func TestStatus(t *testing.T) {
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"),
Stdout: &buff,
Stderr: &buff,
Silent: true,
Dir: dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
require.NoError(t, e.Setup())
// 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
tempdir := filepathext.SmartJoin(dir, ".task")
tempdir := task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}
e := task.Executor{
Dir: dir,
TempDir: tempdir,
@@ -475,7 +494,7 @@ func TestStatusChecksum(t *testing.T) {
// Capture the modification time, so we can ensure the checksum file
// 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)
time := s.ModTime()
@@ -483,7 +502,7 @@ func TestStatusChecksum(t *testing.T) {
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())
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)
assert.Equal(t, time, s.ModTime())
})
@@ -804,8 +823,11 @@ func TestStatusVariables(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"),
Dir: dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: &buff,
Stderr: &buff,
Silent: false,
@@ -953,11 +975,14 @@ func TestDryChecksum(t *testing.T) {
_ = os.Remove(checksumFile)
e := task.Executor{
Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"),
Stdout: io.Discard,
Stderr: io.Discard,
Dry: true,
Dir: dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: io.Discard,
Stderr: io.Discard,
Dry: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
@@ -1032,7 +1057,7 @@ func TestIncludesIncorrect(t *testing.T) {
err := e.Setup()
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) {
@@ -1199,15 +1224,17 @@ func TestIncludesInterpolation(t *testing.T) {
expectedErr bool
expectedOutput string
}{
{"include", "include", false, "includes_interpolation\n"},
{"include with dir", "include-with-dir", false, "included\n"},
{"include", "include", false, "include\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 {
t.Run(test.name, func(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Dir: filepath.Join(dir, test.name),
Stdout: &buff,
Stderr: &buff,
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) {
const dir = "testdata/internal_task"
tests := []struct {
@@ -1624,6 +1679,26 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.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) {
const dir = "testdata/deferred"
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
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
@@ -46,7 +45,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
c.Cmd = cmd
return nil
@@ -110,8 +109,8 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
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
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
// Dep is a task dependency
@@ -32,7 +32,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var task string
if err := node.Decode(&task); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
d.Task = task
return nil
@@ -45,7 +45,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
Silent bool
}
if err := node.Decode(&taskCall); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
d.Task = taskCall.Task
d.For = taskCall.For
@@ -54,5 +54,5 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
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
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
@@ -22,7 +21,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var from string
if err := node.Decode(&from); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
f.From = from
return nil
@@ -30,7 +29,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.SequenceNode:
var list []any
if err := node.Decode(&list); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
f.List = list
return nil
@@ -41,17 +40,19 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
Split string
As string
}
if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" {
f.Var = forStruct.Var
f.Split = forStruct.Split
f.As = forStruct.As
return nil
if err := node.Decode(&forStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line)
if forStruct.Var == "" {
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 {

View File

@@ -1,9 +1,9 @@
package ast
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
type Glob struct {
@@ -13,20 +13,22 @@ type Glob struct {
func (g *Glob) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
g.Glob = node.Value
return nil
case yaml.MappingNode:
var glob struct {
Exclude string
}
if err := node.Decode(&glob); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
g.Glob = glob.Exclude
g.Negate = true
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
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
omap "github.com/go-task/task/v3/internal/omap"
)
@@ -22,7 +21,7 @@ type Include struct {
// Includes represents information about included tasksfiles
type Includes struct {
omap.OrderedMap[string, Include]
omap.OrderedMap[string, *Include]
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@@ -38,15 +37,15 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
var v Include
if err := valueNode.Decode(&v); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
v.Namespace = keyNode.Value
includes.Set(keyNode.Value, v)
includes.Set(keyNode.Value, &v)
}
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
@@ -58,7 +57,7 @@ func (includes *Includes) Len() int {
}
// 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 {
return nil
}
@@ -71,7 +70,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
include.Taskfile = str
return nil
@@ -86,7 +85,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Vars *Vars
}
if err := node.Decode(&includedTaskfile); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
include.Taskfile = includedTaskfile.Taskfile
include.Dir = includedTaskfile.Dir
@@ -98,7 +97,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
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

View File

@@ -1,9 +1,9 @@
package ast
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
// Output of the Task output
@@ -25,7 +25,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var name string
if err := node.Decode(&name); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
s.Name = name
return nil
@@ -35,10 +35,10 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
Group *OutputGroup
}
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 {
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{
Name: "group",
@@ -47,7 +47,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
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.

View File

@@ -6,6 +6,7 @@ import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/goext"
)
@@ -30,7 +31,7 @@ type ErrInvalidPlatform struct {
}
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.
@@ -39,14 +40,14 @@ func (p *Platform) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var platform string
if err := node.Decode(&platform); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
if err := p.parsePlatform(platform); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
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)

View File

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

View File

@@ -1,14 +1,12 @@
package ast
import (
"errors"
"fmt"
"gopkg.in/yaml.v3"
)
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
var ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value")
"github.com/go-task/task/v3/errors"
)
// Precondition represents a precondition necessary for a task to run
type Precondition struct {
@@ -33,7 +31,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
p.Sh = cmd
p.Msg = fmt.Sprintf("`%s` failed", cmd)
@@ -45,7 +43,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
Msg string
}
if err := node.Decode(&sh); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
p.Sh = sh.Sh
p.Msg = sh.Msg
@@ -55,5 +53,5 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
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"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
// Task represents a task
type Task struct {
Task string
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Prompt string
Summary string
Requires *Requires
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool
Run string
Task string
Cmds []*Cmd
Deps []*Dep
Label string
Desc string
Prompt string
Summary string
Requires *Requires
Aliases []string
Sources []*Glob
Generates []*Glob
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Interactive bool
Internal bool
Method string
Prefix string
IgnoreError bool
Run string
Platforms []*Platform
Watch bool
Location *Location
// Populated during merging
Namespace string
IncludeVars *Vars
IncludedTaskfileVars *Vars
Platforms []*Platform
Location *Location
Watch bool
}
func (t *Task) Name() string {
@@ -52,6 +55,13 @@ func (t *Task) Name() string {
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.
func (t *Task) WildcardMatch(name string) (bool, []string) {
// Convert the name into a regex string
@@ -83,7 +93,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var cmd Cmd
if err := node.Decode(&cmd); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
t.Cmds = append(t.Cmds, &cmd)
return nil
@@ -92,7 +102,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
case yaml.SequenceNode:
var cmds []*Cmd
if err := node.Decode(&cmds); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
t.Cmds = cmds
return nil
@@ -130,11 +140,11 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Watch bool
}
if err := node.Decode(&task); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
if task.Cmd != 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}
} else {
@@ -169,7 +179,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
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
@@ -209,6 +219,7 @@ func (t *Task) DeepCopy() *Task {
Platforms: deepcopy.Slice(t.Platforms),
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
}
return c
}

View File

@@ -6,6 +6,8 @@ import (
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
// NamespaceSeparator contains the character that separates namespaces
@@ -13,6 +15,9 @@ const NamespaceSeparator = ":"
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
type Taskfile struct {
Location string
@@ -36,6 +41,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
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)
}
if len(t2.Dotenv) > 0 {
return ErrIncludedTaskfilesCantHaveDotenvs
}
if t2.Output.IsSet() {
t1.Output = t2.Output
}
@@ -45,9 +53,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Env == nil {
t1.Env = &Vars{}
}
t1.Vars.Merge(t2.Vars)
t1.Env.Merge(t2.Env)
t1.Tasks.Merge(t2.Tasks, include)
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
return nil
}
@@ -70,7 +78,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Interval time.Duration
}
if err := node.Decode(&taskfile); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
tf.Version = taskfile.Version
tf.Output = taskfile.Output
@@ -94,5 +102,5 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
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"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/omap"
)
@@ -44,8 +46,8 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
return matchingTasks
}
func (t1 *Tasks) Merge(t2 Tasks, include *Include) {
_ = t2.Range(func(k string, v *Task) error {
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
_ = t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged.
task := v.DeepCopy()
@@ -54,20 +56,25 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include) {
// taskfile are marked as 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 {
if dep != nil && dep.Task != "" {
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
}
}
// Add namespaces to task commands
for _, cmd := range task.Cmds {
if cmd != nil && cmd.Task != "" {
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
}
}
// Add namespaces to task aliases
for i, alias := range task.Aliases {
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
}
// Add namespace aliases
if include != nil {
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
taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace)
taskNameWithNamespace := taskNameWithNamespace(name, include.Namespace)
task.Namespace = include.Namespace
task.Task = taskNameWithNamespace
t1.Set(taskNameWithNamespace, task)
@@ -103,7 +120,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode:
tasks := omap.New[string, *Task]()
if err := node.Decode(&tasks); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
// nolint: errcheck
@@ -135,7 +152,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
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 {

View File

@@ -1,11 +1,11 @@
package ast
import (
"fmt"
"strings"
"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/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
func (vs *Vars) Merge(other *Vars) {
func (vs *Vars) Merge(other *Vars, include *Include) {
if vs == nil || other == nil {
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
@@ -77,19 +83,17 @@ type Var struct {
Live any
Sh string
Ref string
Json string
Yaml string
Dir string
}
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
if experiments.AnyVariables.Value == "1" {
if experiments.MapVariables.Value == "1" {
var value any
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 str, ok := value.(string); ok {
@@ -97,41 +101,41 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
v.Sh = str
return nil
}
if str, ok = strings.CutPrefix(str, "#"); ok {
v.Ref = str
return nil
}
}
v.Value = value
return nil
}
// 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 {
case yaml.MappingNode:
key := node.Content[0].Value
switch key {
case "sh", "ref", "map", "json", "yaml":
case "sh", "ref", "map":
var m struct {
Sh string
Ref string
Map any
Json string
Yaml string
Sh string
Ref string
Map any
}
if err := node.Decode(&m); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
v.Value = m.Map
v.Json = m.Json
v.Yaml = m.Yaml
return nil
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:
var value any
if err := node.Decode(&value); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
v.Value = value
return nil
@@ -141,24 +145,30 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
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:
var sh struct {
Sh string
key := node.Content[0].Value
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 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 {
return filepath.Join(c.dir, fmt.Sprintf("%s.yaml", c.key(node)))
return c.filePath(node, "yaml")
}
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
Location() string
Dir() string
Optional() bool
Remote() bool
ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error)
FilenameAndLastDir() (string, string)
}
func NewRootNode(
@@ -31,9 +31,8 @@ func NewRootNode(
timeout time.Duration,
) (Node, error) {
dir = getDefaultDir(entrypoint, dir)
// Check if there is something to read on STDIN
stat, _ := os.Stdin.Stat()
if (stat.Mode()&os.ModeCharDevice) == 0 && stat.Size() > 0 {
// If the entrypoint is "-", we read from stdin
if entrypoint == "-" {
return NewStdinNode(dir)
}
return NewNode(l, entrypoint, dir, insecure, timeout)

View File

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

View File

@@ -112,3 +112,7 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
entrypointDir := filepath.Dir(node.Entrypoint)
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())
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
}
func (node *StdinNode) FilenameAndLastDir() (string, string) {
return "", "__stdin__"
}

View File

@@ -6,9 +6,12 @@ import (
"os"
"time"
"github.com/dominikbraun/graph"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
"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/logger"
"github.com/go-task/task/v3/internal/templater"
@@ -24,33 +27,83 @@ Continue?`
Continue?`
)
// Read reads a Read for a given directory
// Uses current dir when dir is left empty. Uses Read.yml
// or Read.yaml when entrypoint is left empty
func Read(
// A Reader will recursively read Taskfiles from a given source using a directed
// acyclic graph (DAG).
type Reader struct {
graph *ast.TaskfileGraph
node Node
insecure bool
download bool
offline bool
timeout time.Duration
tempDir string
logger *logger.Logger
}
func NewReader(
node Node,
insecure bool,
download bool,
offline bool,
timeout time.Duration,
tempDir string,
l *logger.Logger,
) (*ast.Taskfile, error) {
var _taskfile func(Node) (*ast.Taskfile, error)
_taskfile = func(node Node) (*ast.Taskfile, error) {
tf, err := readTaskfile(node, download, offline, timeout, tempDir, l)
if err != nil {
return nil, err
}
logger *logger.Logger,
) *Reader {
return &Reader{
graph: ast.NewTaskfileGraph(),
node: node,
insecure: insecure,
download: download,
offline: offline,
timeout: timeout,
tempDir: tempDir,
logger: logger,
}
}
// Check that the Taskfile is set and has a schema version
if tf == nil || tf.Version == nil {
return nil, &errors.TaskfileVersionCheckError{URI: node.Location()}
}
func (r *Reader) Read() (*ast.TaskfileGraph, error) {
// Recursively loop through each Taskfile, adding vertices/edges to the graph
if err := r.include(r.node); err != nil {
return nil, err
}
err = tf.Includes.Range(func(namespace string, include ast.Include) error {
cache := &templater.Cache{Vars: tf.Vars}
include = ast.Include{
return r.graph, nil
}
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,
Taskfile: templater.Replace(include.Taskfile, cache),
Dir: templater.Replace(include.Dir, cache),
@@ -69,14 +122,13 @@ func Read(
return err
}
dir, err := node.ResolveDir(include.Dir)
include.Dir, err = node.ResolveDir(include.Dir)
if err != nil {
return err
}
includeReaderNode, err := NewNode(l, entrypoint, dir, insecure, timeout,
includeNode, err := NewNode(r.logger, entrypoint, include.Dir, r.insecure, r.timeout,
WithParent(node),
WithOptional(include.Optional),
)
if err != nil {
if include.Optional {
@@ -85,106 +137,72 @@ func Read(
return err
}
if err := checkCircularIncludes(includeReaderNode); err != nil {
// Recurse into the included Taskfile
if err := r.include(includeNode); err != nil {
return err
}
includedTaskfile, err := _taskfile(includeReaderNode)
if err != nil {
if include.Optional {
return nil
}
return err
// Create an edge between the Taskfiles
r.graph.Lock()
defer r.graph.Unlock()
edge, err := r.graph.Edge(node.Location(), includeNode.Location())
if err == graph.ErrEdgeNotFound {
// 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 len(includedTaskfile.Dotenv) > 0 {
return ErrIncludedTaskfilesCantHaveDotenvs
}
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
if errors.Is(err, graph.ErrEdgeCreatesCycle) {
return errors.TaskfileCycleError{
Source: node.Location(),
Destination: includeNode.Location(),
}
}
if err = tf.Merge(includedTaskfile, &include); err != nil {
return err
}
return nil
return err
})
if err != nil {
return nil, err
}
return nil
})
for _, task := range tf.Tasks.Values() {
// If the task is not defined, create a new one
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)
// Wait for all the go routines to finish
return g.Wait()
}
func readTaskfile(
node Node,
download,
offline bool,
timeout time.Duration,
tempDir string,
l *logger.Logger,
) (*ast.Taskfile, error) {
func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
var b []byte
var err error
var cache *Cache
if node.Remote() {
cache, err = NewCache(tempDir)
cache, err = NewCache(r.tempDir)
if err != nil {
return nil, err
}
}
// 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) {
return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()}
} else if err != nil {
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 {
downloaded := false
ctx, cf := context.WithTimeout(context.Background(), timeout)
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
defer cf()
// Read the file
@@ -192,16 +210,16 @@ func readTaskfile(
// If we timed out then we likely have a network issue
if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) {
// If a download was requested, then we can't use a cached copy
if download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: timeout}
if r.download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
}
// Search for any cached copies
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 {
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 {
return nil, err
} else {
@@ -210,7 +228,7 @@ func readTaskfile(
// If the node was remote, we need to check the checksum
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
checksum := checksum(b)
@@ -225,7 +243,7 @@ func readTaskfile(
prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location())
}
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()}
}
}
@@ -237,7 +255,7 @@ func readTaskfile(
return nil, err
}
// 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 {
return nil, err
}
@@ -245,33 +263,33 @@ func readTaskfile(
}
}
var t ast.Taskfile
if err := yaml.Unmarshal(b, &t); err != nil {
var tf ast.Taskfile
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}
}
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 {
if node == nil {
return errors.New("task: failed to check for include cycle: node was nil")
}
if node.Parent() == nil {
return errors.New("task: failed to check for include cycle: node.Parent was nil")
}
curNode := node
location := node.Location()
for curNode.Parent() != nil {
curNode = curNode.Parent()
curLocation := curNode.Location()
if curLocation == location {
return fmt.Errorf("task: include cycle detected between %s <--> %s",
curLocation,
node.Parent().Location(),
)
// Set the taskfile/task's locations
tf.Location = node.Location()
for _, task := range tf.Tasks.Values() {
// If the task is not defined, create a new one
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 nil
return &tf, nil
}

View File

@@ -16,9 +16,6 @@ import (
)
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{
"Taskfile.yml",
"taskfile.yml",
@@ -29,7 +26,6 @@ var (
"Taskfile.dist.yaml",
"taskfile.dist.yaml",
}
allowedContentTypes = []string{
"text/plain",
"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:
- task: local
- task: global
- task: multiple_type
local:
vars:
@@ -31,3 +32,11 @@ tasks:
BAR: overriden
cmds:
- 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:
included1:
taskfile: include/Taskfile.include.yml
taskfile: include/Taskfile.include1.yml
vars:
VAR_1: included1-var1
included2:
taskfile: include/Taskfile.include.yml
taskfile: include/Taskfile.include2.yml
vars:
VAR_1: included2-var1
included3:
taskfile: include/Taskfile.include.yml
taskfile: include/Taskfile.include3.yml
tasks:
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-sh
- task: ref-dep
- task: ref-resolver
- task: json
- task: yaml
map:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
cmds:
- task: print-var
- task: print-story
vars:
VAR:
ref: MAP
ref: .MAP
nested-map:
vars:
@@ -44,12 +44,12 @@ tasks:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF:
ref: MAP
ref: .MAP
cmds:
- task: print-var
- task: print-story
vars:
VAR:
ref: MAP_REF
ref: .MAP_REF
ref-sh:
vars:
@@ -58,48 +58,52 @@ tasks:
JSON:
json: "{{.JSON_STRING}}"
MAP_REF:
ref: JSON
ref: .JSON
cmds:
- task: print-var
- task: print-story
vars:
VAR:
ref: MAP_REF
ref: .MAP_REF
ref-dep:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
deps:
- task: print-story
vars:
VAR:
ref: .MAP
ref-resolver:
vars:
MAP:
map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}
MAP_REF:
ref: .MAP
cmds:
- task: print-var
vars:
VAR:
ref: MAP
ref: (index .MAP_REF.children 0).name
json:
vars:
JSON_STRING:
sh: cat example.json
JSON:
json: "{{.JSON_STRING}}"
ref: "fromJson .JSON_STRING"
cmds:
- task: print-var
- task: print-story
vars:
VAR:
ref: JSON
yaml:
vars:
YAML_STRING:
sh: cat example.yaml
YAML:
yaml: "{{.YAML_STRING}}"
cmds:
- task: print-var
vars:
VAR:
ref: YAML
ref: .JSON
print-var:
cmds:
- echo "{{.VAR}}"
print-story:
cmds:
- >-
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,
Requires: origTask.Requires,
Watch: origTask.Watch,
Namespace: origTask.Namespace,
}
new.Dir, err = execext.Expand(new.Dir)
if err != nil {
@@ -104,9 +105,9 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
}
new.Env = &ast.Vars{}
new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache))
new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache))
new.Env.Merge(templater.ReplaceVars(origTask.Env, cache))
new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil)
new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil)
new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil)
if evaluateShVars {
err = new.Env.Range(func(k string, v ast.Var) error {
// 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.Task = templater.Replace(cmd.Task, 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)
}
}
@@ -214,17 +204,6 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
newDep := dep.DeepCopy()
newDep.Task = templater.Replace(dep.Task, 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)
}
}
@@ -243,8 +222,8 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
}
if len(origTask.Status) > 0 {
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir, e.Dry)
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir, e.Dry)
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
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
we're up to.
<!--truncate-->
{/* truncate */}
## :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
to!
<!-- prettier-ignore-start -->
{/* prettier-ignore-start */}
[vscode-task]: https://github.com/go-task/vscode-task
[crowdin]: https://crowdin.com
[contributors]: https://github.com/go-task/task/graphs/contributors
@@ -139,4 +139,4 @@ to!
[experiments-project]: https://github.com/orgs/go-task/projects/1
[gentle-force-experiment]: https://github.com/go-task/task/issues/1200
[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 MASTODON_URL = 'https://fosstodon.org/@task';
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
## 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
- 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 */}
[experiments]: /experiments
[experiments-workflow]: /experiments#workflow
[task]: https://github.com/go-task/task
[vscode-task]: https://github.com/go-task/vscode-task
[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!
{/* prettier-ignore-start */}
[enabling-experiments]: /experiments/#enabling-experiments
[enabling-experiments]: ./experiments.mdx#enabling-experiments
{/* 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
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
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`
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
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
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
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 */}
[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
{/* prettier-ignore-end */}

View File

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

View File

@@ -7,6 +7,25 @@ sidebar_position: 15
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?
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
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:
```shell
@@ -146,6 +146,15 @@ can install Task from [winget-pkgs](https://github.com/microsoft/winget-pkgs).
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
### 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
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
[choco]: https://chocolatey.org/
[scoop]: https://scoop.sh/
[tea]: https://tea.xyz/
[pkgx]: https://pkgx.sh/
{/* prettier-ignore-end */}

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