Compare commits

...

112 Commits

Author SHA1 Message Date
Andrey Nering
56f3735b38 v3.39.1 2024-09-18 22:22:56 -03:00
Andrey Nering
23d578ac8c chore: add changelog entry for #1814 2024-09-18 22:20:51 -03:00
Valentin Maerten
1bf850592c fix: interpolate vars in defer (#1814)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2024-09-18 22:17:53 -03:00
Samuel Marks
0be05795b9 docs: exit code zero is not an error (#1811) 2024-09-18 22:16:59 -03:00
Valentin Maerten
08a2a91180 chore: changelog for #1809 2024-09-18 16:53:34 +02:00
Valentin Maerten
84cc5e57b0 fix(completions): zsh and powershell completions (#1809) 2024-09-18 10:46:02 -04:00
Valentin Maerten
5aa68e47e5 chore: changelog for #1806 2024-09-14 11:17:21 +02:00
Valentin Maerten
15aa4b86af fix: double help is printed when --help is provided (#1806) 2024-09-14 05:16:47 -04:00
Valentin Maerten
114d5e1404 fix(renovate): switch mode from silent to full 2024-09-09 21:55:52 +02:00
Andrey Nering
8ab5fe0e80 chore(website): disable crowdin completely (#1793) 2024-09-07 21:29:03 -03:00
Andrey Nering
c89a6add48 chore(website): upgrade docusaurus to the latest version (#1792) 2024-09-08 00:10:04 +00:00
Andrey Nering
888071e234 chore(website): added bluesky url to the footer 2024-09-07 20:58:29 -03:00
Andrey Nering
ff2e0f846a chore: update project taskfile 2024-09-07 19:31:01 -03:00
Valentin Maerten
3c177d3fdc chore: changelog for #1783 2024-09-07 23:09:01 +02:00
Valentin Maerten
41bc490e0f chore: configure Renovate (#1783) 2024-09-07 17:07:53 -04:00
Pete Davison
f8e3742d11 fix: add schema version to .goreleaser.yml 2024-09-07 20:34:10 +00:00
Pete Davison
a6100b39f8 fix: deprecated goreleaser field 2024-09-07 20:29:43 +00:00
Pete Davison
1275ab1b5b v3.39.0 2024-09-07 20:05:46 +00:00
dependabot[bot]
0c05dcbe0f chore(deps): bump github.com/mattn/go-zglob from 0.0.5 to 0.0.6 (#1791)
Bumps [github.com/mattn/go-zglob](https://github.com/mattn/go-zglob) from 0.0.5 to 0.0.6.
- [Commits](https://github.com/mattn/go-zglob/compare/v0.0.5...v0.0.6)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-zglob
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-07 21:00:52 +01:00
Pete Davison
e9983e299f chore: changelog for #1713 2024-09-07 20:00:00 +00:00
dependabot[bot]
a450f2daea chore(deps): bump golang.org/x/term from 0.23.0 to 0.24.0 (#1790)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.23.0 to 0.24.0.
- [Commits](https://github.com/golang/term/compare/v0.23.0...v0.24.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-09-07 20:55:27 +01:00
Valentin Maerten
c77c8a419b refactor: check if the remote exists just before reading it (#1713)
* refactor: check if the remote exists in the read to avoid doing it in offline mode

* fix: timeout error was not working

* fix: use cached copy if available
2024-09-07 20:54:05 +01:00
Andrey Nering
a233b52c65 chore: add changelog for #1777, #1778 2024-09-06 10:48:25 -03:00
Valentin Maerten
0e2c9cc88f fix: include flatten with a default task (#1778) 2024-09-06 10:44:28 -03:00
dependabot[bot]
dd9cec611a chore(deps): bump micromatch from 4.0.5 to 4.0.8 in /website (#1789)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 13:43:32 +00:00
dependabot[bot]
6985413f93 chore(deps): bump webpack from 5.91.0 to 5.94.0 in /website (#1776)
Bumps [webpack](https://github.com/webpack/webpack) from 5.91.0 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.91.0...v5.94.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 10:41:39 -03:00
dependabot[bot]
cf77768c82 chore(deps): bump github.com/Masterminds/semver/v3 from 3.2.1 to 3.3.0 (#1779)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/Masterminds/semver/releases)
- [Changelog](https://github.com/Masterminds/semver/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Masterminds/semver/compare/v3.2.1...v3.3.0)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  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-09-06 10:40:55 -03:00
dependabot[bot]
6c3b13b676 chore(deps): bump github.com/mattn/go-zglob from 0.0.4 to 0.0.5 (#1780)
Bumps [github.com/mattn/go-zglob](https://github.com/mattn/go-zglob) from 0.0.4 to 0.0.5.
- [Commits](https://github.com/mattn/go-zglob/compare/v0.0.4...v0.0.5)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-zglob
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 10:40:31 -03:00
Pete Davison
ad45c7aeb3 chore: changelog for #1784 2024-09-02 21:45:19 +00:00
Pete Davison
e4b4d04abd fix: matrix loops should be deterministic (#1784) 2024-09-02 22:43:54 +01:00
Valentin Maerten
a3bdb6c40a chore: changelog for #1782 2024-09-02 22:10:05 +02:00
Valentin Maerten
eb39dd94d0 fix(completion): display aliases in fish completion (#1782) 2024-09-02 16:06:01 -04:00
Pete Davison
21cd573770 chore: changelog for #1767 2024-09-02 19:32:19 +00:00
Pete Davison
281d259e6e feat: loop over a matrix (#1767) 2024-09-02 20:29:00 +01:00
Pete Davison
1cb5daf73e chore: changelog for #1157 2024-09-02 19:28:41 +00:00
Pete Davison
3747b2ab7f feat: completion command (#1157) 2024-09-02 19:21:53 +00:00
Andrey Nering
d727ef5393 website: add @vmaerten as a maintainer 2024-09-02 16:04:52 -03:00
Valentin Maerten
a72b65b3b2 chore: changelog for #1704 2024-08-26 23:19:05 +02:00
Valentin Maerten
ef3b853728 feat: add option to declare an included Taskfile as flatten (#1704) 2024-08-26 17:17:39 -04:00
Valentin Maerten
f302b50519 chore: changelog for #1715 2024-08-25 23:06:58 +02:00
Valentin Maerten
c243b0ec7e fix(remote): TASK_REMOTE_DIR does not work when absolute (#1715) 2024-08-25 17:03:28 -04:00
Thanu Poptiphueng
32158dac87 docs: fix variable name (#1754) 2024-08-24 22:24:42 -03:00
Andrey Nering
0a59890a46 chore(dev): add .vscode/extensions.json with recommended extensions 2024-08-24 22:16:22 -03:00
Andrey Nering
defbcf6acd chore: add changelog for #1764 2024-08-24 21:57:09 -03:00
Daniel Story
045d054a5f feat: add ALIAS special var (#1764) 2024-08-24 21:50:45 -03:00
dependabot[bot]
0941de3318 chore(deps): bump mvdan.cc/sh/v3 from 3.8.0 to 3.9.0 (#1765)
Bumps [mvdan.cc/sh/v3](https://github.com/mvdan/sh) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/mvdan/sh/releases)
- [Changelog](https://github.com/mvdan/sh/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdan/sh/compare/v3.8.0...v3.9.0)

---
updated-dependencies:
- dependency-name: mvdan.cc/sh/v3
  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-08-19 13:30:05 -03:00
Andrey Nering
b259edeb65 feat(defer): expose EXIT_CODE special variable to defer: (#1762)
Co-authored-by: Dor Sahar <dorsahar@icloud.com>
2024-08-14 22:53:14 -03:00
dependabot[bot]
35119c12ab chore(deps): bump axios from 1.6.2 to 1.7.4 in /website (#1760)
Bumps [axios](https://github.com/axios/axios) from 1.6.2 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.2...v1.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 11:46:36 -03:00
Pete Davison
f6ff775d11 chore: changelog for #1758 2024-08-14 13:39:56 +00:00
Pete Davison
5e9851f42f Update minimum go version (#1758)
* feat: update minimum version to 1.22

* refactor: use int range iterator

* refactor: loop variables

* refactor: replace slicesext.FirstNonZero with cmp.Or

* refactor: use slices.Concat instead of append

* fix: unused param

* fix: linting
2024-08-14 08:37:05 -05:00
dependabot[bot]
51c569ef37 chore(deps): bump golang.org/x/term from 0.21.0 to 0.23.0 (#1751)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/term/compare/v0.21.0...v0.23.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-08-11 11:31:48 -03:00
dependabot[bot]
1ca432a80d chore(deps): bump golang.org/x/sync from 0.7.0 to 0.8.0 (#1752)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.7.0 to 0.8.0.
- [Commits](https://github.com/golang/sync/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  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-08-11 11:28:10 -03:00
Ryan Halliday
e781b3d4e0 docs: update syslist links to the new location (#1747) 2024-08-09 13:14:03 +00:00
JonZeolla
81ff1cdea0 docs: fix special variables link (#1730) 2024-07-25 17:19:41 +00:00
Pete Davison
1f2cbfb932 chore: changelog for #1633 2024-07-16 22:48:15 +00:00
Valentin Maerten
4b6c79aca5 feat: experiment taskfile envs take precedence over os envs (#1633)
* feat: experiment taskfile envs take precedence over os envs

* fix test

* fix typo

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

* docs: add p about default

---------

Co-authored-by: Andrey Nering <andrey@nering.com.br>
2024-07-16 23:44:34 +01:00
Pete Davison
5739495739 chore: changelog for #1719 2024-07-16 20:07:02 +00:00
Valentin Maerten
9d72fa3250 ci: add new workflow to check if versioned_docs has been modified (#1719) 2024-07-16 21:03:50 +01:00
Pete Davison
4123ffc780 chore: update go-task/template to tagged version 2024-07-16 17:51:55 +00:00
Valentin Maerten
cdafc67bef docs: add CLI_SILENT and CLI_VERBOSE in the docs (#1717) 2024-07-08 21:18:21 +00:00
Valentin Maerten
9ee4f21d62 fix: --version when a version is provided with -ldflags (#1711) 2024-07-05 14:53:36 -03:00
Alessio Perugini
133086d647 docs: update setup-task version (#1710) 2024-07-05 11:38:44 +00:00
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
138 changed files with 5922 additions and 2844 deletions

View File

@@ -8,6 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_style = tab indent_style = tab
[*.{md,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}] [*.{md,mdx,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

1
.gitattributes vendored
View File

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

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: [andreynering, pd93] github: [andreynering, pd93, vmaerten]
open_collective: task open_collective: task
custom: https://taskfile.dev/donate/ custom: https://taskfile.dev/donate/

View File

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

48
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,48 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"group:allNonMajor",
"schedule:monthly"
],
"mode": "full",
"reviewers": ["team:developer"],
"packageRules": [
{
"matchManagers": ["github-actions"],
"groupName": "Github Action",
"labels": ["area: github actions", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
},
{
"matchManagers": ["npm", "nvm"],
"groupName": "Website",
"labels": ["lang: javascript", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
},
{
"matchManagers": ["gomod"],
"groupName": "golang",
"labels": ["lang: go", "area: dependencies"],
"matchPackageNames": [
"*"
],
"matchUpdateTypes": [
"minor",
"patch"
]
}
]
}

View File

@@ -13,19 +13,19 @@ jobs:
name: Lint name: Lint
strategy: strategy:
matrix: matrix:
go-version: [1.21.x, 1.22.x] go-version: [1.22.x, 1.23.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.55.2 version: v1.60.1
lint-jsonschema: lint-jsonschema:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -34,10 +34,25 @@ jobs:
with: with:
python-version: 3.12 python-version: 3.12
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: install check-jsonschema - name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3' run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema) - name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/static/schema.json run: check-jsonschema --check-metaschema website/static/schema.json
check_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v44
with:
files: website/versioned_docs/**
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.')

View File

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

View File

@@ -13,18 +13,18 @@ jobs:
name: Test name: Test
strategy: strategy:
matrix: matrix:
go-version: [1.21.x, 1.22.x] go-version: [1.22.x, 1.23.x]
platform: [ubuntu-latest, macos-latest, windows-latest] platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}} runs-on: ${{matrix.platform}}
steps: steps:
- name: Set up Go ${{matrix.go-version}} - name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Download Go modules - name: Download Go modules
run: go mod download run: go mod download

View File

@@ -1,38 +0,0 @@
name: Upload Source Documents
on:
push:
branches:
- main
workflow_dispatch:
jobs:
push_files_to_crowdin:
name: Push files to Crowdin
if: github.repository == 'go-task/task'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Verify changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: |
website/docs
website/blog
website/i18n/en
website/src/pages
- name: Install Task
uses: arduino/setup-task@v1
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload source documents
if: steps.changed-files.outputs.any_changed == 'true'
run: task crowdin:push
env:
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
working-directory: ./website

3
.gitignore vendored
View File

@@ -24,8 +24,7 @@ dist/
# editors # editors
.idea/ .idea/
.vscode/* .vscode/settings.json
!.vscode/*-sample.json
.fleet/ .fleet/
# exuberant ctags # exuberant ctags

View File

@@ -1,3 +1,6 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
builds: builds:
- binary: task - binary: task
main: ./cmd/task main: ./cmd/task
@@ -11,11 +14,16 @@ builds:
- amd64 - amd64
- arm - arm
- arm64 - arm64
- riscv64
goarm: goarm:
- '6' - '6'
ignore: ignore:
- goos: darwin - goos: darwin
goarch: '386' goarch: '386'
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}' mod_timestamp: '{{ .CommitTimestamp }}'
@@ -41,7 +49,7 @@ release:
draft: true draft: true
snapshot: snapshot:
name_template: "{{.Tag}}" version_template: "{{.Tag}}"
checksum: checksum:
name_template: "task_checksums.txt" name_template: "task_checksums.txt"

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"editorconfig.editorconfig",
"golang.go",
"task.vscode-task"
]
}

View File

@@ -1,5 +1,84 @@
# Changelog # Changelog
## v3.39.1 - 2024-09-18
- Added Renovate configuration to automatically create PRs to keep dependencies
up to date (#1783 by @vmaerten).
- Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten).
- Fixed a bug where ZSH and PowerShell completions did not work when using the
recommended method. (#1813, #1809 by @vmaerten and @shirayu)
- Fix variables not working properly for a `defer:` statement (#1803, #1814 by
@vmaerten and @andreynering).
## v3.39.0 - 2024-09-07
- Added
[Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence)
(#1038, #1633 by @vmaerten).
- Added a CI lint job to ensure that the docs are updated correctly (#1719 by
@vmaerten).
- Updated minimum required Go version to 1.22 (#1758 by @pd93).
- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes
with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).
- Expose a new `ALIAS` special variable, which will contain the alias used to
call the current task. Falls back to the task name. (#1764 by @DanStory).
- Fixed `TASK_REMOTE_DIR` environment variable not working when the path was
absolute. (#1715 by @vmaerten).
- Added an option to declare an included Taskfile as flattened (#1704 by
@vmaerten).
- Added a new
[`--completion` flag](https://taskfile.dev/installation/#setup-completions) to
output completion scripts for various shells (#293, #1157 by @pd93).
- This is now the preferred way to install completions.
- The completion scripts in the `completion` directory
[are now deprecated](https://taskfile.dev/deprecations/completion-scripts/).
- Added the ability to
[loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix)
(#1766, #1767, #1784 by @pd93).
- Fixed a bug in fish completion where aliases were not displayed (#1781, #1782
by @vmaerten).
- Fixed panic when having a flattened included Taskfile that contains a
`default` task (#1777, #1778 by @vmaerten).
- Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten).
## 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 ## v3.37.2 - 2024-05-12
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93). - Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
@@ -20,7 +99,7 @@
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563, - Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
#1607 by @pd93). #1607 by @pd93).
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by - Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
@pd03). @pd93).
- Fix error when a file or directory in the project contained a special char - Fix error when a file or directory in the project contained a special char
like `&`, `(` or `)` (#1551, #1584 by @andreynering). like `&`, `(` or `)` (#1551, #1584 by @andreynering).
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt) - Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import (
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version" ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -58,7 +59,7 @@ func run() error {
entrypoint := flags.Entrypoint entrypoint := flags.Entrypoint
if flags.Version { if flags.Version {
fmt.Printf("Task version: %s\n", ver.GetVersion()) fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
return nil return nil
} }
@@ -82,6 +83,15 @@ func run() error {
return nil return nil
} }
if flags.Completion != "" {
script, err := task.Completion(flags.Completion)
if err != nil {
return err
}
fmt.Println(script)
return nil
}
if flags.Global { if flags.Global {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
@@ -125,16 +135,15 @@ func run() error {
OutputStyle: flags.Output, OutputStyle: flags.Output,
TaskSorter: taskSorter, TaskSorter: taskSorter,
} }
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus) listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil { if err := listOptions.Validate(); err != nil {
return err return err
} }
if err := e.Setup(); err != nil { err := e.Setup()
if err != nil {
return err return err
} }
if experiments.AnyVariables.Enabled { 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") 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")
} }
@@ -145,6 +154,14 @@ func run() error {
return nil return nil
} }
if flags.ClearCache {
cache, err := taskfile.NewCache(e.TempDir.Remote)
if err != nil {
return err
}
return cache.Clear()
}
if (listOptions.ShouldListTasks()) && flags.Silent { if (listOptions.ShouldListTasks()) && flags.Silent {
return e.ListTaskNames(flags.ListAll) return e.ListTaskNames(flags.ListAll)
} }
@@ -179,6 +196,8 @@ func run() error {
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs}) globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll}) globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
e.Taskfile.Vars.Merge(globals, nil) e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch { if !flags.Watch {

34
completion.go Normal file
View File

@@ -0,0 +1,34 @@
package task
import (
_ "embed"
"fmt"
)
//go:embed completion/bash/task.bash
var completionBash string
//go:embed completion/fish/task.fish
var completionFish string
//go:embed completion/ps/task.ps1
var completionPowershell string
//go:embed completion/zsh/_task
var completionZsh string
func Completion(completion string) (string, error) {
// Get the file extension for the selected shell
switch completion {
case "bash":
return completionBash, nil
case "fish":
return completionFish, nil
case "powershell":
return completionPowershell, nil
case "zsh":
return completionZsh, nil
default:
return "", fmt.Errorf("unknown shell: %s", completion)
}
}

View File

@@ -10,7 +10,7 @@ function __task_get_tasks --description "Prints all available tasks with their d
end end
# Grab names and descriptions (if any) of the tasks # Grab names and descriptions (if any) of the tasks
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(aliases.*/\1\t\2/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0) set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
if test $output if test $output
echo $output echo $output
end end

View File

@@ -1,6 +1,5 @@
#compdef task #compdef task
compdef _task task
local context state state_descr line
typeset -A opt_args typeset -A opt_args
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}" _GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
@@ -39,26 +38,33 @@ function __task_list() {
_describe 'Task to run' scripts _describe 'Task to run' scripts
} }
_arguments \ _task() {
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \ _arguments \
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \ '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \ '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
'(-c --color)'{-c,--color}'[colored output]' \ '(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \ '(-c --color)'{-c,--color}'[colored output]' \
'(--dry)--dry[dry-run mode, compile and print tasks only]' \ '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \ '(--dry)--dry[dry-run mode, compile and print tasks only]' \
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \ '(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \ '(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
'(-s --silent)'{-s,--silent}'[disable echoing]' \ '(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \ '(-s --silent)'{-s,--silent}'[disable echoing]' \
'(--summary)--summary[show summary\: field from tasks instead of running them]' \ '(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \ '(--summary)--summary[show summary\: field from tasks instead of running them]' \
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \ '(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \ '(-v --verbose)'{-v,--verbose}'[verbose mode]' \
+ '(operation)' \ '(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
{-l,--list}'[list describable tasks]' \ + '(operation)' \
{-a,--list-all}'[list all tasks]' \ {-l,--list}'[list describable tasks]' \
{-i,--init}'[create new Taskfile.yml]' \ {-a,--list-all}'[list all tasks]' \
'(-*)'{-h,--help}'[show help]' \ {-i,--init}'[create new Taskfile.yml]' \
'(-*)--version[show version and exit]' \ '(-*)'{-h,--help}'[show help]' \
'*: :__task_list' '(-*)--version[show version and exit]' \
'*: :__task_list'
}
# don't run the completion function when being source-ed or eval-ed
if [ "$funcstack[1]" = "_task" ]; then
_task "$@"
fi

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,14 +12,14 @@ const (
const ( const (
CodeTaskfileNotFound int = iota + 100 CodeTaskfileNotFound int = iota + 100
CodeTaskfileAlreadyExists CodeTaskfileAlreadyExists
CodeTaskfileInvalid CodeTaskfileDecode
CodeTaskfileFetchFailed CodeTaskfileFetchFailed
CodeTaskfileNotTrusted CodeTaskfileNotTrusted
CodeTaskfileNotSecure CodeTaskfileNotSecure
CodeTaskfileCacheNotFound CodeTaskfileCacheNotFound
CodeTaskfileVersionCheckError CodeTaskfileVersionCheckError
CodeTaskfileNetworkTimeout CodeTaskfileNetworkTimeout
_ // CodeTaskfileDuplicateInclude CodeTaskfileInvalid
CodeTaskfileCycle CodeTaskfileCycle
) )
@@ -58,3 +58,8 @@ func Is(err, target error) bool {
func As(err error, target any) bool { func As(err error, target any) bool {
return errors.As(err, target) return errors.As(err, target)
} }
// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package.
func Unwrap(err error) error {
return errors.Unwrap(err)
}

View File

@@ -80,6 +80,19 @@ func (err *TaskNameConflictError) Code() int {
return CodeTaskNameConflict return CodeTaskNameConflict
} }
type TaskNameFlattenConflictError struct {
TaskName string
Include string
}
func (err *TaskNameFlattenConflictError) Error() string {
return fmt.Sprintf(`task: Found multiple tasks (%s) included by "%s""`, err.TaskName, err.Include)
}
func (err *TaskNameFlattenConflictError) Code() int {
return CodeTaskNameConflict
}
// TaskCalledTooManyTimesError is returned when the maximum task call limit is // TaskCalledTooManyTimesError is returned when the maximum task call limit is
// exceeded. This is to prevent infinite loops and cyclic dependencies. // exceeded. This is to prevent infinite loops and cyclic dependencies.
type TaskCalledTooManyTimesError struct { type TaskCalledTooManyTimesError struct {

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>

22
go.mod
View File

@@ -1,16 +1,18 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.21.0 go 1.22.0
require ( require (
github.com/Masterminds/semver/v3 v3.2.1 github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0 github.com/dominikbraun/graph v0.23.0
github.com/fatih/color v1.16.0 github.com/fatih/color v1.17.0
github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 github.com/go-task/template v0.1.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.4 github.com/mattn/go-zglob v0.0.6
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.0 github.com/otiai10/copy v1.14.0
github.com/radovskyb/watcher v1.0.7 github.com/radovskyb/watcher v1.0.7
@@ -18,18 +20,20 @@ require (
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/zeebo/xxh3 v1.0.2 github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.7.0 golang.org/x/sync v0.8.0
golang.org/x/term v0.19.0 golang.org/x/term v0.24.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.8.0 mvdan.cc/sh/v3 v3.9.0
) )
require ( require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
) )

50
go.sum
View File

@@ -1,21 +1,33 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 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/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 h1:JBbiZ2CXIZ9Upe3O2yI5+3ksWoa7hNVNi4BINs8TIrs= github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
@@ -29,10 +41,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
@@ -55,18 +69,18 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8= mvdan.cc/sh/v3 v3.9.0 h1:it14fyjCdQUk4jf/aYxLO3FG8jFarR9GzMCtnlvvD7c=
mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY= mvdan.cc/sh/v3 v3.9.0/go.mod h1:cdBk8bgoiBI7lSZqK5JhUuq7OB64VQ7fgm85xelw3Nk=

View File

@@ -1,6 +1,7 @@
package task package task
import ( import (
"cmp"
"fmt" "fmt"
"github.com/go-task/task/v3/internal/hash" "github.com/go-task/task/v3/internal/hash"
@@ -8,11 +9,7 @@ import (
) )
func (e *Executor) GetHash(t *ast.Task) (string, error) { func (e *Executor) GetHash(t *ast.Task) (string, error) {
r := t.Run r := cmp.Or(t.Run, e.Taskfile.Run)
if r == "" {
r = e.Taskfile.Run
}
var h hash.HashFunc var h hash.HashFunc
switch r { switch r {
case "always": case "always":

35
help.go
View File

@@ -7,8 +7,8 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"text/tabwriter"
"github.com/Ladicle/tabwriter"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/go-task/task/v3/internal/editors" "github.com/go-task/task/v3/internal/editors"
@@ -105,7 +105,8 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
for _, task := range tasks { for _, task := range tasks {
e.Logger.FOutf(w, logger.Yellow, "* ") e.Logger.FOutf(w, logger.Yellow, "* ")
e.Logger.FOutf(w, logger.Green, task.Task) e.Logger.FOutf(w, logger.Green, task.Task)
e.Logger.FOutf(w, logger.Default, ": \t%s", task.Desc) desc := strings.ReplaceAll(task.Desc, "\n", " ")
e.Logger.FOutf(w, logger.Default, ": \t%s", desc)
if len(task.Aliases) > 0 { if len(task.Aliases) > 0 {
e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", ")) e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", "))
} }
@@ -159,23 +160,21 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
} }
var g errgroup.Group var g errgroup.Group
for i := range tasks { for i := range tasks {
task := tasks[i]
j := i
aliases := []string{} aliases := []string{}
if len(task.Aliases) > 0 { if len(tasks[i].Aliases) > 0 {
aliases = task.Aliases aliases = tasks[i].Aliases
} }
g.Go(func() error { g.Go(func() error {
o.Tasks[j] = editors.Task{ o.Tasks[i] = editors.Task{
Name: task.Name(), Name: tasks[i].Name(),
Desc: task.Desc, Desc: tasks[i].Desc,
Summary: task.Summary, Summary: tasks[i].Summary,
Aliases: aliases, Aliases: aliases,
UpToDate: false, UpToDate: false,
Location: &editors.Location{ Location: &editors.Location{
Line: task.Location.Line, Line: tasks[i].Location.Line,
Column: task.Location.Column, Column: tasks[i].Location.Column,
Taskfile: task.Location.Taskfile, Taskfile: tasks[i].Location.Taskfile,
}, },
} }
@@ -185,12 +184,12 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
// Get the fingerprinting method to use // Get the fingerprinting method to use
method := e.Taskfile.Method method := e.Taskfile.Method
if task.Method != "" { if tasks[i].Method != "" {
method = task.Method method = tasks[i].Method
} }
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), task, upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), tasks[i],
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir), fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry), fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger), fingerprint.WithLogger(e.Logger),
) )
@@ -198,7 +197,7 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
return err return err
} }
o.Tasks[j].UpToDate = upToDate o.Tasks[i].UpToDate = upToDate
return nil return nil
}) })

View File

@@ -3,14 +3,12 @@ package compiler
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -48,7 +46,7 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, err
func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) { func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) {
result := GetEnviron() result := GetEnviron()
if t != nil { if t != nil {
specialVars, err := c.getSpecialVars(t) specialVars, err := c.getSpecialVars(t, call)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -77,18 +75,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
if err := cache.Err(); err != nil { if err := cache.Err(); err != nil {
return err return err
} }
// Evaluate JSON
if newVar.Json != "" {
if err := json.Unmarshal([]byte(newVar.Json), &newVar.Value); err != nil {
return err
}
}
// Evaluate YAML
if newVar.Yaml != "" {
if err := yaml.Unmarshal([]byte(newVar.Yaml), &newVar.Value); err != nil {
return err
}
}
// If the variable is not dynamic, we can set it and return // If the variable is not dynamic, we can set it and return
if newVar.Value != nil || newVar.Sh == "" { if newVar.Value != nil || newVar.Sh == "" {
result.Set(k, ast.Var{Value: newVar.Value}) result.Set(k, ast.Var{Value: newVar.Value})
@@ -193,9 +179,11 @@ func (c *Compiler) ResetCache() {
c.dynamicCache = nil c.dynamicCache = nil
} }
func (c *Compiler) getSpecialVars(t *ast.Task) (map[string]string, error) { func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
return map[string]string{ return map[string]string{
"TASK": t.Task, "TASK": t.Task,
"ALIAS": call.Task,
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": c.Dir, "ROOT_DIR": c.Dir,
"TASKFILE": t.Location.Taskfile, "TASKFILE": t.Location.Taskfile,

View File

@@ -85,7 +85,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
case reflect.Struct: case reflect.Struct:
// Loop over each field and call traverseFunc recursively // Loop over each field and call traverseFunc recursively
for i := 0; i < v.NumField(); i += 1 { for i := range v.NumField() {
if err := traverseFunc(copy.Field(i), v.Field(i)); err != nil { if err := traverseFunc(copy.Field(i), v.Field(i)); err != nil {
return err return err
} }
@@ -95,7 +95,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
// Create an empty copy from the original value's type // Create an empty copy from the original value's type
copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap())) copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap()))
// Loop over each element and call traverseFunc recursively // Loop over each element and call traverseFunc recursively
for i := 0; i < v.Len(); i += 1 { for i := range v.Len() {
if err := traverseFunc(copy.Index(i), v.Index(i)); err != nil { if err := traverseFunc(copy.Index(i), v.Index(i)); err != nil {
return err return err
} }

9
internal/env/env.go vendored
View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -11,15 +12,15 @@ func Get(t *ast.Task) []string {
if t.Env == nil { if t.Env == nil {
return nil return nil
} }
environ := os.Environ() environ := os.Environ()
for k, v := range t.Env.ToCacheMap() { for k, v := range t.Env.ToCacheMap() {
if !isTypeAllowed(v) { if !isTypeAllowed(v) {
continue continue
} }
if !experiments.EnvPrecedence.Enabled {
if _, alreadySet := os.LookupEnv(k); alreadySet { if _, alreadySet := os.LookupEnv(k); alreadySet {
continue continue
}
} }
environ = append(environ, fmt.Sprintf("%s=%v", k, v)) environ = append(environ, fmt.Sprintf("%s=%v", k, v))
} }

View File

@@ -90,14 +90,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return r.Run(ctx, p) return r.Run(ctx, p)
} }
// IsExitError returns true the given error is an exis status error
func IsExitError(err error) bool {
if _, ok := interp.IsExitStatus(err); ok {
return true
}
return false
}
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field // Expand is a helper to mvdan.cc/shell.Fields that returns the first field
// if available. // if available.
func Expand(s string) (string, error) { func Expand(s string) (string, error) {

View File

@@ -7,8 +7,8 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"text/tabwriter"
"github.com/Ladicle/tabwriter"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -29,6 +29,7 @@ var (
RemoteTaskfiles Experiment RemoteTaskfiles Experiment
AnyVariables Experiment AnyVariables Experiment
MapVariables Experiment MapVariables Experiment
EnvPrecedence Experiment
) )
func init() { func init() {
@@ -37,6 +38,7 @@ func init() {
RemoteTaskfiles = New("REMOTE_TASKFILES") RemoteTaskfiles = New("REMOTE_TASKFILES")
AnyVariables = New("ANY_VARIABLES", "1", "2") AnyVariables = New("ANY_VARIABLES", "1", "2")
MapVariables = New("MAP_VARIABLES", "1", "2") MapVariables = New("MAP_VARIABLES", "1", "2")
EnvPrecedence = New("ENV_PRECEDENCE")
} }
func New(xName string, enabledValues ...string) Experiment { func New(xName string, enabledValues ...string) Experiment {
@@ -70,6 +72,7 @@ func getEnvFilePath() string {
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError) fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.") fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`) fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
fs.Usage = func() {}
_ = fs.Parse(os.Args[1:]) _ = fs.Parse(os.Args[1:])
// If the directory is set, find a .env file in that directory. // If the directory is set, find a .env file in that directory.
if dir != "" { if dir != "" {
@@ -104,5 +107,6 @@ func List(l *logger.Logger) error {
printExperiment(w, l, GentleForce) printExperiment(w, l, GentleForce)
printExperiment(w, l, RemoteTaskfiles) printExperiment(w, l, RemoteTaskfiles)
printExperiment(w, l, MapVariables) printExperiment(w, l, MapVariables)
printExperiment(w, l, EnvPrecedence)
return w.Flush() return w.Flush()
} }

View File

@@ -38,6 +38,7 @@ var (
Version bool Version bool
Help bool Help bool
Init bool Init bool
Completion string
List bool List bool
ListAll bool ListAll bool
ListJson bool ListJson bool
@@ -65,6 +66,7 @@ var (
Experiments bool Experiments bool
Download bool Download bool
Offline bool Offline bool
ClearCache bool
Timeout time.Duration Timeout time.Duration
) )
@@ -79,6 +81,7 @@ func init() {
pflag.BoolVar(&Version, "version", false, "Show Task version.") pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.")
pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.") pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.")
pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.") pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.")
pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.") pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.")
@@ -119,6 +122,7 @@ func init() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.") pflag.BoolVar(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.") pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
} }
pflag.Parse() pflag.Parse()
@@ -129,6 +133,10 @@ func Validate() error {
return errors.New("task: You can't set both --download and --offline flags") return errors.New("task: You can't set both --download and --offline flags")
} }
if Download && ClearCache {
return errors.New("task: You can't set both --download and --clear-cache flags")
}
if Global && Dir != "" { if Global && Dir != "" {
log.Fatal("task: You can't set both --global and --dir") log.Fatal("task: You can't set both --global and --dir")
return nil return nil

View File

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

View File

@@ -52,6 +52,30 @@ func Red() PrintFunc {
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc() return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc()
} }
func BrightBlue() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc()
}
func BrightGreen() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc()
}
func BrightCyan() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc()
}
func BrightYellow() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc()
}
func BrightMagenta() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc()
}
func BrightRed() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc()
}
func envColor(env string, defaultColor color.Attribute) []color.Attribute { func envColor(env string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" { if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false color.NoColor = false
@@ -65,7 +89,7 @@ func envColor(env string, defaultColor color.Attribute) []color.Attribute {
// Otherwise, split by semicolons (ANSI color codes) and use them as is. // Otherwise, split by semicolons (ANSI color codes) and use them as is.
attributeStrs := strings.Split(override, ",") attributeStrs := strings.Split(override, ",")
if len(attributeStrs) == 3 { if len(attributeStrs) == 3 {
attributeStrs = append([]string{"38", "2"}, attributeStrs...) attributeStrs = slices.Concat([]string{"38", "2"}, attributeStrs)
} else { } else {
attributeStrs = strings.Split(override, ";") attributeStrs = strings.Split(override, ";")
} }
@@ -156,7 +180,7 @@ func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continu
return errors.New("no continue values provided") return errors.New("no continue values provided")
} }
l.Outf(color, "%s [%s/%s]\n", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue)) l.Outf(color, "%s [%s/%s]: ", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
reader := bufio.NewReader(l.Stdin) reader := bufio.NewReader(l.Stdin)
input, err := reader.ReadString('\n') input, err := reader.ReadString('\n')

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package templater
import ( import (
"bytes" "bytes"
"fmt"
"maps" "maps"
"strings" "strings"
@@ -40,7 +41,15 @@ func ResolveRef(ref string, cache *Cache) any {
cache.cacheMap = cache.Vars.ToCacheMap() cache.cacheMap = cache.Vars.ToCacheMap()
} }
val, err := template.ResolveRef(ref, cache.cacheMap) 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 { if err != nil {
cache.err = err cache.err = err
return nil return nil
@@ -119,8 +128,6 @@ func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var
Live: v.Live, Live: v.Live,
Ref: v.Ref, Ref: v.Ref,
Dir: v.Dir, Dir: v.Dir,
Json: ReplaceWithExtra(v.Json, cache, extra),
Yaml: ReplaceWithExtra(v.Yaml, cache, extra),
} }
} }

View File

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

2
package-lock.json generated
View File

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

View File

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

View File

@@ -1,13 +1,11 @@
package task package task
import ( import (
"context"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call *ast.Call) error { func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 { if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil return nil
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"sync" "sync"
@@ -69,7 +70,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
e.Download, e.Download,
e.Offline, e.Offline,
e.Timeout, e.Timeout,
e.TempDir, e.TempDir.Remote,
e.Logger, e.Logger,
) )
graph, err := reader.Read() graph, err := reader.Read()
@@ -95,7 +96,7 @@ func (e *Executor) setupFuzzyModel() {
words = append(words, taskName) words = append(words, taskName)
for _, task := range e.Taskfile.Tasks.Values() { for _, task := range e.Taskfile.Tasks.Values() {
words = append(words, task.Aliases...) words = slices.Concat(words, task.Aliases)
} }
} }
@@ -104,12 +105,15 @@ func (e *Executor) setupFuzzyModel() {
} }
func (e *Executor) setupTempDir() error { func (e *Executor) setupTempDir() error {
if e.TempDir != "" { if e.TempDir != (TempDir{}) {
return nil return nil
} }
if os.Getenv("TASK_TEMP_DIR") == "" { if os.Getenv("TASK_TEMP_DIR") == "" {
e.TempDir = filepathext.SmartJoin(e.Dir, ".task") e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") { } else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR")) tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
if err != nil { if err != nil {
@@ -117,9 +121,28 @@ func (e *Executor) setupTempDir() error {
} }
projectDir, _ := filepath.Abs(e.Dir) projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir) projectName := filepath.Base(projectDir)
e.TempDir = filepathext.SmartJoin(tempDir, projectName) e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else { } else {
e.TempDir = filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")) e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
}
}
if os.Getenv("TASK_REMOTE_DIR") != "" {
if filepath.IsAbs(os.Getenv("TASK_REMOTE_DIR")) || strings.HasPrefix(os.Getenv("TASK_REMOTE_DIR"), "~") {
remoteTempDir, err := execext.Expand(os.Getenv("TASK_REMOTE_DIR"))
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
} }
return nil return nil
@@ -155,7 +178,7 @@ func (e *Executor) setupOutput() error {
} }
var err error var err error
e.Output, err = output.BuildFor(&e.OutputStyle) e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
return err return err
} }

View File

@@ -8,24 +8,25 @@ import (
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
) )
const interruptSignalsCount = 3
// NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals // NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals
// so the Task process is not killed immediately and processes running have // so the Task process is not killed immediately and processes running have
// time to do cleanup work. // time to do cleanup work.
func (e *Executor) InterceptInterruptSignals() { func (e *Executor) InterceptInterruptSignals() {
ch := make(chan os.Signal, 3) ch := make(chan os.Signal, interruptSignalsCount)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM) signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
for i := 1; i <= 3; i++ { for i := range interruptSignalsCount {
sig := <-ch sig := <-ch
if i < 3 { if i+1 >= interruptSignalsCount {
e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig) e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig)
continue os.Exit(1)
} }
e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig) e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig)
os.Exit(1)
} }
}() }()
} }

View File

@@ -20,9 +20,7 @@ import (
"time" "time"
) )
var ( var SLEEPIT, _ = filepath.Abs("./bin/sleepit")
SLEEPIT, _ = filepath.Abs("./bin/sleepit")
)
func TestSignalSentToProcessGroup(t *testing.T) { func TestSignalSentToProcessGroup(t *testing.T) {
task, err := getTaskPath() task, err := getTaskPath()
@@ -147,7 +145,7 @@ func TestSignalSentToProcessGroup(t *testing.T) {
// where the negative PID means the corresponding process group. Note that // where the negative PID means the corresponding process group. Note that
// this negative PID works only as long as the caller of the kill(2) system // this negative PID works only as long as the caller of the kill(2) system
// call has a different PID, which is the case for this test. // call has a different PID, which is the case for this test.
for i := 1; i <= tc.sendSigs; i++ { for range tc.sendSigs - 1 {
if err := syscall.Kill(-sut.Process.Pid, syscall.SIGINT); err != nil { if err := syscall.Kill(-sut.Process.Pid, syscall.SIGINT); err != nil {
t.Fatalf("sending INT signal to the process group: %v", err) t.Fatalf("sending INT signal to the process group: %v", err)
} }

View File

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

73
task.go
View File

@@ -11,6 +11,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler" "github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
@@ -34,13 +36,18 @@ const (
MaximumTaskCall = 1000 MaximumTaskCall = 1000
) )
type TempDir struct {
Remote string
Fingerprint string
}
// Executor executes a Taskfile // Executor executes a Taskfile
type Executor struct { type Executor struct {
Taskfile *ast.Taskfile Taskfile *ast.Taskfile
Dir string Dir string
Entrypoint string Entrypoint string
TempDir string TempDir TempDir
Force bool Force bool
ForceAll bool ForceAll bool
Insecure bool Insecure bool
@@ -183,16 +190,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
release := e.acquireConcurrencyLimit() release := e.acquireConcurrencyLimit()
defer release() defer release()
if t.Prompt != "" {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
return e.startExecution(ctx, t, func(ctx context.Context) error { return e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil { if err := e.runDeps(ctx, t); err != nil {
@@ -205,7 +202,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
return err return err
} }
if err := e.areTaskRequiredVarsSet(ctx, t, call); err != nil { if err := e.areTaskRequiredVarsSet(t, call); err != nil {
return err return err
} }
@@ -222,7 +219,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir), fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry), fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger), fingerprint.WithLogger(e.Logger),
) )
@@ -238,13 +235,25 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
} }
} }
if t.Prompt != "" && !e.Dry {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
if err := e.mkdir(t); err != nil { if err := e.mkdir(t); err != nil {
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err) e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
} }
var deferredExitCode uint8
for i := range t.Cmds { for i := range t.Cmds {
if t.Cmds[i].Defer { if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i) defer e.runDeferred(t, call, i, &deferredExitCode)
continue continue
} }
@@ -253,9 +262,13 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
} }
if execext.IsExitError(err) && t.IgnoreError { exitCode, isExitError := interp.IsExitStatus(err)
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) if isExitError {
continue if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue
}
deferredExitCode = exitCode
} }
if call.Indirect { if call.Indirect {
@@ -307,10 +320,26 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) { func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i]
vars, _ := e.Compiler.FastGetVariables(origTask, call)
cache := &templater.Cache{Vars: vars}
extra := map[string]any{}
if deferredExitCode != nil && *deferredExitCode > 0 {
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
}
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
if err := e.runCommand(ctx, t, call, i); err != nil { if err := e.runCommand(ctx, t, call, i); err != nil {
e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error()) e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
} }
@@ -367,7 +396,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
if closeErr := close(err); closeErr != nil { if closeErr := close(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr) e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
} }
if execext.IsExitError(err) && cmd.IgnoreError { if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err) e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
return nil return nil
} }
@@ -489,14 +518,12 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks // Compile the list of tasks
for i := range tasks { for i := range tasks {
idx := i
task := tasks[idx]
g.Go(func() error { g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: task.Task}) compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
if err != nil { if err != nil {
return err return err
} }
tasks[idx] = compiledTask tasks[i] = compiledTask
return nil return nil
}) })
} }

View File

@@ -19,6 +19,7 @@ import (
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -60,17 +61,19 @@ func (fct fileContentTest) Run(t *testing.T) {
for f := range fct.Files { for f := range fct.Files {
_ = os.Remove(filepathext.SmartJoin(fct.Dir, f)) _ = os.Remove(filepathext.SmartJoin(fct.Dir, f))
} }
e := &task.Executor{ e := &task.Executor{
Dir: fct.Dir, Dir: fct.Dir,
TempDir: filepathext.SmartJoin(fct.Dir, ".task"), TempDir: task.TempDir{
Remote: filepathext.SmartJoin(fct.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"),
},
Entrypoint: fct.Entrypoint, Entrypoint: fct.Entrypoint,
Stdout: io.Discard, Stdout: io.Discard,
Stderr: io.Discard, Stderr: io.Discard,
} }
require.NoError(t, e.Setup(), "e.Setup()") require.NoError(t, e.Setup(), "e.Setup()")
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: fct.Target}), "e.Run(target)") require.NoError(t, e.Run(context.Background(), &ast.Call{Task: fct.Target}), "e.Run(target)")
for name, expectContent := range fct.Files { for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) { t.Run(fct.name(name), func(t *testing.T) {
path := filepathext.SmartJoin(e.Dir, name) path := filepathext.SmartJoin(e.Dir, name)
@@ -105,6 +108,7 @@ func TestEmptyTaskfile(t *testing.T) {
} }
func TestEnv(t *testing.T) { func TestEnv(t *testing.T) {
t.Setenv("QUX", "from_os")
tt := fileContentTest{ tt := fileContentTest{
Dir: "testdata/env", Dir: "testdata/env",
Target: "default", Target: "default",
@@ -113,9 +117,21 @@ func TestEnv(t *testing.T) {
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n", "local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n", "global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
"multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n", "multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n",
"not-overriden.txt": "QUX='from_os'\n",
}, },
} }
tt.Run(t) tt.Run(t)
t.Setenv("TASK_X_ENV_PRECEDENCE", "1")
experiments.EnvPrecedence = experiments.New("ENV_PRECEDENCE")
ttt := fileContentTest{
Dir: "testdata/env",
Target: "overriden",
TrimSpace: false,
Files: map[string]string{
"overriden.txt": "QUX='from_taskfile'\n",
},
}
ttt.Run(t)
} }
func TestVars(t *testing.T) { func TestVars(t *testing.T) {
@@ -272,11 +288,14 @@ func TestStatus(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := &task.Executor{ e := &task.Executor{
Dir: dir, Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"), TempDir: task.TempDir{
Stdout: &buff, Remote: filepathext.SmartJoin(dir, ".task"),
Stderr: &buff, Fingerprint: filepathext.SmartJoin(dir, ".task"),
Silent: true, },
Stdout: &buff,
Stderr: &buff,
Silent: true,
} }
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
// gen-foo creates foo.txt, and will always fail it's status check. // gen-foo creates foo.txt, and will always fail it's status check.
@@ -468,7 +487,10 @@ func TestStatusChecksum(t *testing.T) {
} }
var buff bytes.Buffer var buff bytes.Buffer
tempdir := filepathext.SmartJoin(dir, ".task") tempdir := task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
TempDir: tempdir, TempDir: tempdir,
@@ -485,7 +507,7 @@ func TestStatusChecksum(t *testing.T) {
// Capture the modification time, so we can ensure the checksum file // Capture the modification time, so we can ensure the checksum file
// is not regenerated when the hash hasn't changed. // is not regenerated when the hash hasn't changed.
s, err := os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task)) s, err := os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
require.NoError(t, err) require.NoError(t, err)
time := s.ModTime() time := s.ModTime()
@@ -493,7 +515,7 @@ func TestStatusChecksum(t *testing.T) {
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.task})) require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.task}))
assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String()) assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String())
s, err = os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task)) s, err = os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, time, s.ModTime()) assert.Equal(t, time, s.ModTime())
}) })
@@ -803,7 +825,8 @@ func TestListDescInterpolation(t *testing.T) {
t.Error(err) t.Error(err)
} }
assert.Contains(t, buff.String(), "bar") assert.Contains(t, buff.String(), "foo-var")
assert.Contains(t, buff.String(), "bar-var")
} }
func TestStatusVariables(t *testing.T) { func TestStatusVariables(t *testing.T) {
@@ -814,8 +837,11 @@ func TestStatusVariables(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"), TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: &buff, Stdout: &buff,
Stderr: &buff, Stderr: &buff,
Silent: false, Silent: false,
@@ -963,11 +989,14 @@ func TestDryChecksum(t *testing.T) {
_ = os.Remove(checksumFile) _ = os.Remove(checksumFile)
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"), TempDir: task.TempDir{
Stdout: io.Discard, Remote: filepathext.SmartJoin(dir, ".task"),
Stderr: io.Discard, Fingerprint: filepathext.SmartJoin(dir, ".task"),
Dry: true, },
Stdout: io.Discard,
Stderr: io.Discard,
Dry: true,
} }
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"})) require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
@@ -1042,7 +1071,7 @@ func TestIncludesIncorrect(t *testing.T) {
err := e.Setup() err := e.Setup()
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "task: Failed to parse testdata/includes_incorrect/incomplete.yml:") assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error())
} }
func TestIncludesEmptyMain(t *testing.T) { func TestIncludesEmptyMain(t *testing.T) {
@@ -1201,6 +1230,45 @@ func TestIncludesInternal(t *testing.T) {
} }
} }
func TestIncludesFlatten(t *testing.T) {
const dir = "testdata/includes_flatten"
tests := []struct {
name string
taskfile string
task string
expectedErr bool
expectedOutput string
}{
{name: "included flatten", taskfile: "Taskfile.yml", task: "gen", expectedOutput: "gen from included\n"},
{name: "included flatten with default", taskfile: "Taskfile.yml", task: "default", expectedOutput: "default from included flatten\n"},
{name: "included flatten can call entrypoint tasks", taskfile: "Taskfile.yml", task: "from_entrypoint", expectedOutput: "from entrypoint\n"},
{name: "included flatten with deps", taskfile: "Taskfile.yml", task: "with_deps", expectedOutput: "gen from included\nwith_deps from included\n"},
{name: "included flatten nested", taskfile: "Taskfile.yml", task: "from_nested", expectedOutput: "from nested\n"},
{name: "included flatten multiple same task", taskfile: "Taskfile.multiple.yml", task: "gen", expectedErr: true, expectedOutput: "task: Found multiple tasks (gen) included by \"included\"\""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Entrypoint: dir + "/" + test.taskfile,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
err := e.Setup()
if test.expectedErr {
assert.EqualError(t, err, test.expectedOutput)
} else {
require.NoError(t, err)
_ = e.Run(context.Background(), &ast.Call{Task: test.task})
assert.Equal(t, test.expectedOutput, buff.String())
}
})
}
}
func TestIncludesInterpolation(t *testing.T) { func TestIncludesInterpolation(t *testing.T) {
const dir = "testdata/includes_interpolation" const dir = "testdata/includes_interpolation"
tests := []struct { tests := []struct {
@@ -1664,6 +1732,26 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestRunOnceSharedDeps(t *testing.T) {
const dir = "testdata/run_once_shared_deps"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
ForceAll: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "build"}))
rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`)
matches := rx.FindAllStringSubmatch(buff.String(), -1)
assert.Len(t, matches, 1)
assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`)
assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`)
}
func TestDeferredCmds(t *testing.T) { func TestDeferredCmds(t *testing.T) {
const dir = "testdata/deferred" const dir = "testdata/deferred"
var buff bytes.Buffer var buff bytes.Buffer
@@ -1689,6 +1777,34 @@ task-1 ran successfully
assert.Contains(t, buff.String(), expectedOutputOrder) assert.Contains(t, buff.String(), expectedOutputOrder)
} }
func TestExitCodeZero(t *testing.T) {
const dir = "testdata/exit_code"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "exit-zero"}))
assert.Equal(t, "FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String()))
}
func TestExitCodeOne(t *testing.T) {
const dir = "testdata/exit_code"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
require.NoError(t, e.Setup())
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "exit-one"}))
assert.Equal(t, "FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String()))
}
func TestIgnoreNilElements(t *testing.T) { func TestIgnoreNilElements(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -2260,6 +2376,10 @@ func TestForCmds(t *testing.T) {
name: "loop-explicit", name: "loop-explicit",
expectedOutput: "a\nb\nc\n", expectedOutput: "a\nb\nc\n",
}, },
{
name: "loop-matrix",
expectedOutput: "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n",
},
{ {
name: "loop-sources", name: "loop-sources",
expectedOutput: "bar\nfoo\n", expectedOutput: "bar\nfoo\n",
@@ -2317,6 +2437,17 @@ func TestForDeps(t *testing.T) {
name: "loop-explicit", name: "loop-explicit",
expectedOutputContains: []string{"a\n", "b\n", "c\n"}, expectedOutputContains: []string{"a\n", "b\n", "c\n"},
}, },
{
name: "loop-matrix",
expectedOutputContains: []string{
"windows/amd64\n",
"windows/arm64\n",
"linux/amd64\n",
"linux/arm64\n",
"darwin/amd64\n",
"darwin/arm64\n",
},
},
{ {
name: "loop-sources", name: "loop-sources",
expectedOutputContains: []string{"bar\n", "foo\n"}, expectedOutputContains: []string{"bar\n", "foo\n"},
@@ -2426,3 +2557,48 @@ func TestWildcard(t *testing.T) {
}) })
} }
} }
func TestReference(t *testing.T) {
tests := []struct {
name string
call string
expectedOutput string
}{
{
name: "reference in command",
call: "ref-cmd",
expectedOutput: "1\n",
},
{
name: "reference in dependency",
call: "ref-dep",
expectedOutput: "1\n",
},
{
name: "reference using templating resolver",
call: "ref-resolver",
expectedOutput: "1\n",
},
{
name: "reference using templating resolver and dynamic var",
call: "ref-resolver-sh",
expectedOutput: "Alice has 3 children called Bob, Charlie, and Diane\n",
},
}
for _, test := range tests {
t.Run(test.call, func(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: "testdata/var_references",
Stdout: &buff,
Stderr: &buff,
Silent: true,
Force: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.call}))
assert.Equal(t, test.expectedOutput, buff.String())
})
}
}

View File

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

View File

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

View File

@@ -1,19 +1,20 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/omap"
) )
type For struct { type For struct {
From string From string
List []any List []any
Var string Matrix omap.OrderedMap[string, []any]
Split string Var string
As string Split string
As string
} }
func (f *For) UnmarshalYAML(node *yaml.Node) error { func (f *For) UnmarshalYAML(node *yaml.Node) error {
@@ -22,7 +23,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var from string var from string
if err := node.Decode(&from); err != nil { if err := node.Decode(&from); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
f.From = from f.From = from
return nil return nil
@@ -30,28 +31,35 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.SequenceNode: case yaml.SequenceNode:
var list []any var list []any
if err := node.Decode(&list); err != nil { if err := node.Decode(&list); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
f.List = list f.List = list
return nil return nil
case yaml.MappingNode: case yaml.MappingNode:
var forStruct struct { var forStruct struct {
Var string Matrix omap.OrderedMap[string, []any]
Split string Var string
As string Split string
As string
} }
if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" { if err := node.Decode(&forStruct); err != nil {
f.Var = forStruct.Var return errors.NewTaskfileDecodeError(err, node)
f.Split = forStruct.Split
f.As = forStruct.As
return nil
} }
if forStruct.Var == "" && forStruct.Matrix.Len() == 0 {
return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line) return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for")
}
if forStruct.Var != "" && forStruct.Matrix.Len() != 0 {
return errors.NewTaskfileDecodeError(nil, node).WithMessage("cannot use both var and matrix in for")
}
f.Matrix = forStruct.Matrix
f.Var = forStruct.Var
f.Split = forStruct.Split
f.As = forStruct.As
return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into for", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("for")
} }
func (f *For) DeepCopy() *For { func (f *For) DeepCopy() *For {
@@ -59,10 +67,11 @@ func (f *For) DeepCopy() *For {
return nil return nil
} }
return &For{ return &For{
From: f.From, From: f.From,
List: deepcopy.Slice(f.List), List: deepcopy.Slice(f.List),
Var: f.Var, Matrix: f.Matrix.DeepCopy(),
Split: f.Split, Var: f.Var,
As: f.As, Split: f.Split,
As: f.As,
} }
} }

View File

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

View File

@@ -1,10 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
omap "github.com/go-task/task/v3/internal/omap" omap "github.com/go-task/task/v3/internal/omap"
) )
@@ -18,6 +17,7 @@ type Include struct {
Aliases []string Aliases []string
AdvancedImport bool AdvancedImport bool
Vars *Vars Vars *Vars
Flatten bool
} }
// Includes represents information about included tasksfiles // Includes represents information about included tasksfiles
@@ -38,7 +38,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
var v Include var v Include
if err := valueNode.Decode(&v); err != nil { if err := valueNode.Decode(&v); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Namespace = keyNode.Value v.Namespace = keyNode.Value
includes.Set(keyNode.Value, &v) includes.Set(keyNode.Value, &v)
@@ -46,7 +46,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes")
} }
// Len returns the length of the map // Len returns the length of the map
@@ -71,7 +71,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var str string var str string
if err := node.Decode(&str); err != nil { if err := node.Decode(&str); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
include.Taskfile = str include.Taskfile = str
return nil return nil
@@ -82,11 +82,12 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Dir string Dir string
Optional bool Optional bool
Internal bool Internal bool
Flatten bool
Aliases []string Aliases []string
Vars *Vars Vars *Vars
} }
if err := node.Decode(&includedTaskfile); err != nil { if err := node.Decode(&includedTaskfile); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
include.Taskfile = includedTaskfile.Taskfile include.Taskfile = includedTaskfile.Taskfile
include.Dir = includedTaskfile.Dir include.Dir = includedTaskfile.Dir
@@ -95,10 +96,11 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.Aliases = includedTaskfile.Aliases include.Aliases = includedTaskfile.Aliases
include.AdvancedImport = true include.AdvancedImport = true
include.Vars = includedTaskfile.Vars include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("include")
} }
// DeepCopy creates a new instance of IncludedTaskfile and copies // DeepCopy creates a new instance of IncludedTaskfile and copies
@@ -115,5 +117,6 @@ func (include *Include) DeepCopy() *Include {
Internal: include.Internal, Internal: include.Internal,
AdvancedImport: include.AdvancedImport, AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(), Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
package ast package ast
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
) )
// NamespaceSeparator contains the character that separates namespaces // NamespaceSeparator contains the character that separates namespaces
@@ -54,8 +55,7 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
} }
t1.Vars.Merge(t2.Vars, include) t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include) t1.Env.Merge(t2.Env, include)
t1.Tasks.Merge(t2.Tasks, include, t1.Vars) return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
return nil
} }
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
@@ -77,7 +77,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Interval time.Duration Interval time.Duration
} }
if err := node.Decode(&taskfile); err != nil { if err := node.Decode(&taskfile); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
tf.Version = taskfile.Version tf.Version = taskfile.Version
tf.Output = taskfile.Output tf.Output = taskfile.Output
@@ -101,5 +101,5 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile")
} }

View File

@@ -2,10 +2,12 @@ package ast
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/omap"
) )
@@ -45,43 +47,48 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
return matchingTasks return matchingTasks
} }
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) { func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) error {
_ = t2.Range(func(k string, v *Task) error { err := t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can // We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged. // be changed elsewhere once the taskfile is merged.
task := v.DeepCopy() task := v.DeepCopy()
// Set the task to internal if EITHER the included task or the included // Set the task to internal if EITHER the included task or the included
// taskfile are marked as internal // taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal) task.Internal = task.Internal || (include != nil && include.Internal)
taskName := name
// Add namespaces to task dependencies if !include.Flatten {
for _, dep := range task.Deps { // Add namespaces to task dependencies
if dep != nil && dep.Task != "" { for _, dep := range task.Deps {
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace) 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 {
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
for _, alias := range v.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
} }
} }
// 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 {
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
for _, alias := range v.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
}
}
}
taskName = taskNameWithNamespace(name, include.Namespace)
task.Namespace = include.Namespace
task.Task = taskName
} }
if include.AdvancedImport { if include.AdvancedImport {
@@ -93,24 +100,29 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy() task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
} }
if t1.Get(taskName) != nil {
return &errors.TaskNameFlattenConflictError{
TaskName: taskName,
Include: include.Namespace,
}
}
// Add the task to the merged taskfile // Add the task to the merged taskfile
taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace) t1.Set(taskName, task)
task.Task = taskNameWithNamespace
t1.Set(taskNameWithNamespace, task)
return nil return nil
}) })
// If the included Taskfile has a default task and the parent namespace has // If the included Taskfile has a default task, being not flattened and the parent namespace has
// no task with a matching name, we can add an alias so that the user can // no task with a matching name, we can add an alias so that the user can
// run the included Taskfile's default task without specifying its full // run the included Taskfile's default task without specifying its full
// name. If the parent namespace has aliases, we add another alias for each // name. If the parent namespace has aliases, we add another alias for each
// of them. // of them.
if t2.Get("default") != nil && t1.Get(include.Namespace) == nil { if t2.Get("default") != nil && t1.Get(include.Namespace) == nil && !include.Flatten {
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace) defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace) t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace)
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Aliases...) t1.Get(defaultTaskName).Aliases = slices.Concat(t1.Get(defaultTaskName).Aliases, include.Aliases)
} }
return err
} }
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
@@ -118,7 +130,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode: case yaml.MappingNode:
tasks := omap.New[string, *Task]() tasks := omap.New[string, *Task]()
if err := node.Decode(&tasks); err != nil { if err := node.Decode(&tasks); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
// nolint: errcheck // nolint: errcheck
@@ -150,7 +162,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks")
} }
func taskNameWithNamespace(taskName string, namespace string) string { func taskNameWithNamespace(taskName string, namespace string) string {

View File

@@ -1,11 +1,11 @@
package ast package ast
import ( import (
"fmt"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/omap"
) )
@@ -83,8 +83,6 @@ type Var struct {
Live any Live any
Sh string Sh string
Ref string Ref string
Json string
Yaml string
Dir string Dir string
} }
@@ -95,7 +93,7 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.MapVariables.Value == "1" { if experiments.MapVariables.Value == "1" {
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
// If the value is a string and it starts with $, then it's a shell command // If the value is a string and it starts with $, then it's a shell command
if str, ok := value.(string); ok { if str, ok := value.(string); ok {
@@ -103,6 +101,10 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
v.Sh = str v.Sh = str
return nil return nil
} }
if str, ok = strings.CutPrefix(str, "#"); ok {
v.Ref = str
return nil
}
} }
v.Value = value v.Value = value
return nil return nil
@@ -114,30 +116,26 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode: case yaml.MappingNode:
key := node.Content[0].Value key := node.Content[0].Value
switch key { switch key {
case "sh", "ref", "map", "json", "yaml": case "sh", "ref", "map":
var m struct { var m struct {
Sh string Sh string
Ref string Ref string
Map any Map any
Json string
Yaml string
} }
if err := node.Decode(&m); err != nil { if err := node.Decode(&m); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Sh = m.Sh v.Sh = m.Sh
v.Ref = m.Ref v.Ref = m.Ref
v.Value = m.Map v.Value = m.Map
v.Json = m.Json
v.Yaml = m.Yaml
return nil return nil
default: default:
return fmt.Errorf(`yaml: line %d: %q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, node.Line, key) return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
} }
default: default:
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Value = value v.Value = value
return nil return nil
@@ -148,22 +146,27 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
if len(node.Content) > 2 || node.Content[0].Value != "sh" { key := node.Content[0].Value
return fmt.Errorf(`task: line %d: maps cannot be assigned to variables`, node.Line) 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")
} }
var sh struct {
Sh string
}
if err := node.Decode(&sh); err != nil {
return err
}
v.Sh = sh.Sh
return nil
default: default:
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Value = value v.Value = value
return nil return nil

View File

@@ -50,9 +50,23 @@ func (c *Cache) key(node Node) string {
} }
func (c *Cache) cacheFilePath(node Node) string { func (c *Cache) cacheFilePath(node Node) string {
return filepath.Join(c.dir, fmt.Sprintf("%s.yaml", c.key(node))) return c.filePath(node, "yaml")
} }
func (c *Cache) checksumFilePath(node Node) string { func (c *Cache) checksumFilePath(node Node) string {
return filepath.Join(c.dir, fmt.Sprintf("%s.checksum", c.key(node))) return c.filePath(node, "checksum")
}
func (c *Cache) filePath(node Node, suffix string) string {
lastDir, filename := node.FilenameAndLastDir()
prefix := filename
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s-%s", lastDir, filename)
}
return filepath.Join(c.dir, fmt.Sprintf("%s.%s.%s", prefix, c.key(node), suffix))
}
func (c *Cache) Clear() error {
return os.RemoveAll(c.dir)
} }

View File

@@ -20,6 +20,7 @@ type Node interface {
Remote() bool Remote() bool
ResolveEntrypoint(entrypoint string) (string, error) ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error) ResolveDir(dir string) (string, error)
FilenameAndLastDir() (string, string)
} }
func NewRootNode( func NewRootNode(

View File

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

View File

@@ -17,7 +17,9 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP. // An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct { type HTTPNode struct {
*BaseNode *BaseNode
URL *url.URL URL *url.URL
logger *logger.Logger
timeout time.Duration
} }
func NewHTTPNode( func NewHTTPNode(
@@ -36,18 +38,12 @@ func NewHTTPNode(
if url.Scheme == "http" && !insecure { if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint} return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
} }
ctx, cf := context.WithTimeout(context.Background(), timeout)
defer cf()
url, err = RemoteExists(ctx, l, url)
if err != nil {
return nil, err
}
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: url.String(), Timeout: timeout}
}
return &HTTPNode{ return &HTTPNode{
BaseNode: base, BaseNode: base,
URL: url, URL: url,
timeout: timeout,
logger: l,
}, nil }, nil
} }
@@ -60,6 +56,11 @@ func (node *HTTPNode) Remote() bool {
} }
func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) { func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.logger, node.URL, node.timeout)
if err != nil {
return nil, err
}
node.URL = url
req, err := http.NewRequest("GET", node.URL.String(), nil) req, err := http.NewRequest("GET", node.URL.String(), nil)
if err != nil { if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()} return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
@@ -67,6 +68,9 @@ func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
resp, err := http.DefaultClient.Do(req.WithContext(ctx)) resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil { if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.URL.String(), Timeout: node.timeout}
}
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()} return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -110,3 +114,8 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
entrypointDir := filepath.Dir(node.Dir()) entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *HTTPNode) FilenameAndLastDir() (string, string) {
dir, filename := filepath.Split(node.URL.Path)
return filepath.Base(dir), filename
}

View File

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

View File

@@ -109,6 +109,7 @@ func (r *Reader) include(node Node) error {
Dir: templater.Replace(include.Dir, cache), Dir: templater.Replace(include.Dir, cache),
Optional: include.Optional, Optional: include.Optional,
Internal: include.Internal, Internal: include.Internal,
Flatten: include.Flatten,
Aliases: include.Aliases, Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport, AdvancedImport: include.AdvancedImport,
Vars: include.Vars, Vars: include.Vars,
@@ -207,8 +208,9 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
// Read the file // Read the file
b, err = node.Read(ctx) b, err = node.Read(ctx)
var taskfileNetworkTimeoutError *errors.TaskfileNetworkTimeoutError
// If we timed out then we likely have a network issue // If we timed out then we likely have a network issue
if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) { if node.Remote() && errors.As(err, &taskfileNetworkTimeoutError) {
// If a download was requested, then we can't use a cached copy // If a download was requested, then we can't use a cached copy
if r.download { if r.download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout} return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
@@ -265,6 +267,11 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
var tf ast.Taskfile var tf ast.Taskfile
if err := yaml.Unmarshal(b, &tf); err != nil { if err := yaml.Unmarshal(b, &tf); err != nil {
// Decode the taskfile and add the file info the any errors
taskfileInvalidErr := &errors.TaskfileDecodeError{}
if errors.As(err, &taskfileInvalidErr) {
return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2)
}
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err} return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
} }

View File

@@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"time"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
@@ -40,7 +41,7 @@ var (
// at the given URL with any of the default Taskfile files names. If any of // at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are // these match a file, the first matching path will be returned. If no files are
// found, an error will be returned. // found, an error will be returned.
func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL) (*url.URL, error) { func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout time.Duration) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists // Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequest("HEAD", u.String(), nil) req, err := http.NewRequest("HEAD", u.String(), nil)
if err != nil { if err != nil {
@@ -50,6 +51,9 @@ func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL) (*url.URL,
// Request the given URL // Request the given URL
resp, err := http.DefaultClient.Do(req.WithContext(ctx)) resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil { if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: u.String(), Timeout: timeout}
}
return nil, errors.TaskfileFetchFailedError{URI: u.String()} return nil, errors.TaskfileFetchFailedError{URI: u.String()}
} }
defer resp.Body.Close() defer resp.Body.Close()

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

View File

@@ -8,12 +8,14 @@ env:
FOO: foo FOO: foo
BAR: bar BAR: bar
BAZ: "{{.BAZ}}" BAZ: "{{.BAZ}}"
QUX: from_taskfile
tasks: tasks:
default: default:
cmds: cmds:
- task: local - task: local
- task: global - task: global
- task: not-overriden
- task: multiple_type - task: multiple_type
local: local:
@@ -40,3 +42,11 @@ tasks:
BAZ: 1.1 BAZ: 1.1
cmds: cmds:
- echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > multiple_type.txt - echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > multiple_type.txt
not-overriden:
cmds:
- echo "QUX='$QUX'" > not-overriden.txt
overriden:
cmds:
- echo "QUX='$QUX'" > overriden.txt

21
testdata/exit_code/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
version: '3'
silent: true
vars:
PREFIX: EXIT_CODE=
tasks:
exit-zero:
vars:
FOO: bar
cmds:
- defer: echo FOO={{.FOO}} - {{.PREFIX}}{{.EXIT_CODE}}
- exit 0
exit-one:
vars:
FOO: bar
cmds:
- defer: echo FOO={{.FOO}} - {{.PREFIX}}{{.EXIT_CODE}}
- exit 1

View File

@@ -7,6 +7,14 @@ tasks:
- for: ["a", "b", "c"] - for: ["a", "b", "c"]
cmd: echo "{{.ITEM}}" cmd: echo "{{.ITEM}}"
loop-matrix:
cmds:
- for:
matrix:
OS: ["windows", "linux", "darwin"]
ARCH: ["amd64", "arm64"]
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
# Loop over the task's sources # Loop over the task's sources
loop-sources: loop-sources:
sources: sources:

View File

@@ -9,6 +9,16 @@ tasks:
vars: vars:
TEXT: "{{.ITEM}}" TEXT: "{{.ITEM}}"
loop-matrix:
deps:
- for:
matrix:
OS: ["windows", "linux", "darwin"]
ARCH: ["amd64", "arm64"]
task: echo
vars:
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
# Loop over the task's sources # Loop over the task's sources
loop-sources: loop-sources:
sources: sources:

1
testdata/includes_flatten/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.txt

View File

@@ -0,0 +1,12 @@
version: '3'
includes:
included:
taskfile: ./included
flatten: true
tasks:
gen:
cmds:
- echo "gen multiple"

View File

@@ -0,0 +1,3 @@
version: '3'
tasks:
default: echo "default from included flatten"

15
testdata/includes_flatten/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
version: '3'
includes:
included:
taskfile: ./included
dir: ./included
flatten: true
with_default:
taskfile: ./Taskfile.with_default.yml
flatten: true
tasks:
from_entrypoint: echo "from entrypoint"

View File

@@ -0,0 +1,23 @@
version: '3'
includes:
nested:
taskfile: ../nested
flatten: true
tasks:
gen:
cmds:
- echo "gen from included"
with_deps:
deps:
- gen
cmds:
- echo "with_deps from included"
pwd:
desc: Print working directory
cmds:
- pwd

View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
from_nested:
cmds:
- echo "from nested"

View File

@@ -1,8 +1,12 @@
version: '3' version: '3'
vars: vars:
FOO: bar FOO: foo
BAR: bar
tasks: tasks:
foo: foo:
desc: "task has desc with {{.FOO}} var" desc: "task has desc with {{.FOO}}-var"
bar:
desc: "task has desc with {{.BAR}}-var"

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

View File

@@ -6,8 +6,16 @@ includes:
dir: ./included dir: ./included
tasks: tasks:
print-task: echo {{.TASK}} print-task:
aliases: [echo-task]
cmds:
- echo {{.TASK}}
print-root-dir: echo {{.ROOT_DIR}} print-root-dir: echo {{.ROOT_DIR}}
print-taskfile: echo {{.TASKFILE}} print-taskfile: echo {{.TASKFILE}}
print-taskfile-dir: echo {{.TASKFILE_DIR}} print-taskfile-dir: echo {{.TASKFILE_DIR}}
print-task-version: echo {{.TASK_VERSION}} print-task-version: echo {{.TASK_VERSION}}
print-task-alias:
aliases: [echo-task-alias]
cmds:
- echo "{{.ALIAS}}"
print-task-alias-default: echo "{{.ALIAS}}"

View File

@@ -1,8 +1,16 @@
version: '3' version: '3'
tasks: tasks:
print-task: echo {{.TASK}} print-task:
aliases: [echo-task]
cmds:
- echo {{.TASK}}
print-root-dir: echo {{.ROOT_DIR}} print-root-dir: echo {{.ROOT_DIR}}
print-taskfile: echo {{.TASKFILE}} print-taskfile: echo {{.TASKFILE}}
print-taskfile-dir: echo {{.TASKFILE_DIR}} print-taskfile-dir: echo {{.TASKFILE_DIR}}
print-task-version: echo {{.TASK_VERSION}} print-task-version: echo {{.TASK_VERSION}}
print-task-alias:
aliases: [echo-task-alias]
cmds:
- echo "{{.ALIAS}}"
print-task-alias-default: echo "{{.ALIAS}}"

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

@@ -10,7 +10,6 @@ tasks:
- task: ref-dep - task: ref-dep
- task: ref-resolver - task: ref-resolver
- task: json - task: json
- task: yaml
map: map:
vars: vars:
@@ -93,25 +92,13 @@ tasks:
JSON_STRING: JSON_STRING:
sh: cat example.json sh: cat example.json
JSON: JSON:
json: "{{.JSON_STRING}}" ref: "fromJson .JSON_STRING"
cmds: cmds:
- task: print-story - task: print-story
vars: vars:
VAR: VAR:
ref: .JSON ref: .JSON
yaml:
vars:
YAML_STRING:
sh: cat example.yaml
YAML:
yaml: "{{.YAML_STRING}}"
cmds:
- task: print-story
vars:
VAR:
ref: .YAML
print-var: print-var:
cmds: cmds:
- echo "{{.VAR}}" - echo "{{.VAR}}"

View File

@@ -11,6 +11,7 @@ import (
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fingerprint" "github.com/go-task/task/v3/internal/fingerprint"
"github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -72,6 +73,7 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
Location: origTask.Location, Location: origTask.Location,
Requires: origTask.Requires, Requires: origTask.Requires,
Watch: origTask.Watch, Watch: origTask.Watch,
Namespace: origTask.Namespace,
} }
new.Dir, err = execext.Expand(new.Dir) new.Dir, err = execext.Expand(new.Dir)
if err != nil { if err != nil {
@@ -160,6 +162,12 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
} }
continue continue
} }
// Defer commands are replaced in a lazy manner because
// we need to include EXIT_CODE.
if cmd.Defer {
new.Cmds = append(new.Cmds, cmd.DeepCopy())
continue
}
newCmd := cmd.DeepCopy() newCmd := cmd.DeepCopy()
newCmd.Cmd = templater.Replace(cmd.Cmd, cache) newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
newCmd.Task = templater.Replace(cmd.Task, cache) newCmd.Task = templater.Replace(cmd.Task, cache)
@@ -221,8 +229,8 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
} }
if len(origTask.Status) > 0 { if len(origTask.Status) > 0 {
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir, e.Dry) timestampChecker := fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir, e.Dry) checksumChecker := fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} { for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
value, err := checker.Value(&new) value, err := checker.Value(&new)
@@ -264,9 +272,13 @@ func itemsFromFor(
) ([]any, []string, error) { ) ([]any, []string, error) {
var keys []string // The list of keys to loop over (only if looping over a map) var keys []string // The list of keys to loop over (only if looping over a map)
var values []any // The list of values to loop over var values []any // The list of values to loop over
// Get the list from a matrix
if f.Matrix.Len() != 0 {
return asAnySlice(product(f.Matrix)), nil, nil
}
// Get the list from the explicit for list // Get the list from the explicit for list
if f.List != nil && len(f.List) > 0 { if len(f.List) > 0 {
values = f.List return f.List, nil, nil
} }
// Get the list from the task sources // Get the list from the task sources
if f.From == "sources" { if f.From == "sources" {
@@ -315,3 +327,39 @@ func itemsFromFor(
} }
return values, keys, nil return values, keys, nil
} }
// product generates the cartesian product of the input map of slices.
func product(inputMap omap.OrderedMap[string, []any]) []map[string]any {
if inputMap.Len() == 0 {
return nil
}
// Start with an empty product result
result := []map[string]any{{}}
// Iterate over each slice in the slices
_ = inputMap.Range(func(key string, slice []any) error {
var newResult []map[string]any
// For each combination in the current result
for _, combination := range result {
// Append each element from the current slice to the combinations
for _, item := range slice {
newComb := make(map[string]any, len(combination))
// Copy the existing combination
for k, v := range combination {
newComb[k] = v
}
// Add the current item with the corresponding key
newComb[key] = item
newResult = append(newResult, newComb)
}
}
// Update result with the new combinations
result = newResult
return nil
})
return result
}

View File

@@ -50,15 +50,3 @@ tasks:
desc: Upgrade Docusaurus desc: Upgrade Docusaurus
cmds: cmds:
- yarn upgrade @docusaurus/core@latest @docusaurus/preset-classic@latest @docusaurus/module-type-aliases@latest @docusaurus/tsconfig@latest @docusaurus/types@latest - yarn upgrade @docusaurus/core@latest @docusaurus/preset-classic@latest @docusaurus/module-type-aliases@latest @docusaurus/tsconfig@latest @docusaurus/types@latest
crowdin:push:
desc: Upload source files to a Crowdin project
deps: [yarn:install]
cmds:
- npx crowdin push
crowdin:pull:
desc: Download latest translations from Crowdin to the specified place
deps: [yarn:install]
cmds:
- npx crowdin pull --export-only-approved

View File

@@ -103,7 +103,7 @@ tasks:
You can use any of the following list-based functions: `first`, `rest`, `last`, You can use any of the following list-based functions: `first`, `rest`, `last`,
`initial`, `append`, `prepend`, `concat`, `reverse`, `uniq`, `without`, `has`, `initial`, `append`, `prepend`, `concat`, `reverse`, `uniq`, `without`, `has`,
`compact`, `slice` and `chunk`. Check out the [slim-sprg lists `compact`, `slice` and `chunk`. Check out the [slim-sprig lists
documentation][slim-sprig-list] for more information. documentation][slim-sprig-list] for more information.
### Looping over variables using `for` ### Looping over variables using `for`

View File

@@ -1,4 +1,7 @@
export const GITHUB_URL = 'https://github.com/go-task/task'; export const GITHUB_URL = 'https://github.com/go-task/task';
export const TWITTER_URL = 'https://twitter.com/taskfiledev'; export const TWITTER_URL = 'https://twitter.com/taskfiledev';
export const BLUESKY_URL = 'https://bsky.app/profile/taskfile.dev';
export const MASTODON_URL = 'https://fosstodon.org/@task'; export const MASTODON_URL = 'https://fosstodon.org/@task';
export const DISCORD_URL = 'https://discord.gg/6TY36E39UK'; export const DISCORD_URL = 'https://discord.gg/6TY36E39UK';
export const STACK_OVERFLOW = 'https://stackoverflow.com/questions/tagged/taskfile';
export const ANSWER_OVERFLOW = 'https://www.answeroverflow.com/c/974121106208354339';

View File

@@ -1,12 +0,0 @@
project_id: "574591"
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: true
files:
- source: /docs/**/*
translation: /i18n/%locale%/docusaurus-plugin-content-docs/version-latest/**/%original_file_name%
ignore:
- /**/*.json
- source: /blog/**/*
translation: /i18n/%locale%/docusaurus-plugin-content-blog/**/%original_file_name%

View File

@@ -5,6 +5,85 @@ sidebar_position: 14
# Changelog # Changelog
## v3.39.1 - 2024-09-18
- Added Renovate configuration to automatically create PRs to keep dependencies
up to date (#1783 by @vmaerten).
- Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten).
- Fixed a bug where ZSH and PowerShell completions did not work when using the
recommended method. (#1813, #1809 by @vmaerten and @shirayu)
- Fix variables not working properly for a `defer:` statement (#1803, #1814 by
@vmaerten and @andreynering).
## v3.39.0 - 2024-09-07
- Added
[Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence)
(#1038, #1633 by @vmaerten).
- Added a CI lint job to ensure that the docs are updated correctly (#1719 by
@vmaerten).
- Updated minimum required Go version to 1.22 (#1758 by @pd93).
- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes
with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).
- Expose a new `ALIAS` special variable, which will contain the alias used to
call the current task. Falls back to the task name. (#1764 by @DanStory).
- Fixed `TASK_REMOTE_DIR` environment variable not working when the path was
absolute. (#1715 by @vmaerten).
- Added an option to declare an included Taskfile as flattened (#1704 by
@vmaerten).
- Added a new
[`--completion` flag](https://taskfile.dev/installation/#setup-completions) to
output completion scripts for various shells (#293, #1157 by @pd93).
- This is now the preferred way to install completions.
- The completion scripts in the `completion` directory
[are now deprecated](https://taskfile.dev/deprecations/completion-scripts/).
- Added the ability to
[loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix)
(#1766, #1767, #1784 by @pd93).
- Fixed a bug in fish completion where aliases were not displayed (#1781, #1782
by @vmaerten).
- Fixed panic when having a flattened included Taskfile that contains a
`default` task (#1777, #1778 by @vmaerten).
- Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten).
## 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 ## v3.37.2 - 2024-05-12
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93). - Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
@@ -25,7 +104,7 @@ sidebar_position: 14
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563, - Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
#1607 by @pd93). #1607 by @pd93).
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by - Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
@pd03). @pd93).
- Fix error when a file or directory in the project contained a special char - Fix error when a file or directory in the project contained a special char
like `&`, `(` or `)` (#1551, #1584 by @andreynering). like `&`, `(` or `)` (#1551, #1584 by @andreynering).
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt) - Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)

View File

@@ -9,11 +9,6 @@ Some of the work to improve the Task ecosystem is done by the community, be it
installation methods or integrations with code editor. I (the author) am installation methods or integrations with code editor. I (the author) am
thankful for everyone that helps me to improve the overall experience. thankful for everyone that helps me to improve the overall experience.
## Translations
We use [Crowdin](https://crowdin.com/project/taskfile) to translate our
document.
## Integrations ## Integrations
Many of our integrations are contributed and maintained by the community. You Many of our integrations are contributed and maintained by the community. You

View File

@@ -0,0 +1,25 @@
---
slug: /deprecations/completion-scripts/
---
# Completion Scripts
:::warning
This deprecation breaks the following functionality:
- Any direct references to the completion scripts in the Task git repository
:::
Direct use of the completion scripts in the `completion/*` directory of the
[github.com/go-task/task][task] Git repository is deprecated. Any shell
configuration that directly refers to these scripts will potentially break in
the future as the scripts may be moved or deleted entirely. Any configuration
should be updated to use the [new method for generating shell
completions][completions] instead.
{/* prettier-ignore-start */}
[completions]: ../installation.mdx#setup-completions
[task]: https://github.com/go-task/task
{/* prettier-ignore-end */}

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` |

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