Compare commits

...

122 Commits

Author SHA1 Message Date
Pete Davison
58ab26c4ab v3.39.2 2024-09-19 12:25:29 +00:00
Valentin Maerten
65d332dfd0 chore: changelog for #1818 2024-09-19 14:24:35 +02:00
Valentin Maerten
5eaf0b2dcd fix: interpolate dynamic vars in defer (#1818) 2024-09-19 08:22:39 -04:00
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
Pete Davison
57c094f415 v3.37.2 2024-05-12 19:36:09 +00:00
Pete Davison
2f4876b71c chore: changelog for #1649 2024-05-12 19:33:39 +00:00
Pete Davison
725f929778 fix: included variable merging (#1649) 2024-05-12 20:32:09 +01:00
Pete Davison
8266b28b48 chore: changelog for #1648 2024-05-12 19:27:39 +00:00
Pete Davison
f5c7472f64 fix: nil schema panic (#1648) 2024-05-12 20:25:54 +01:00
Pete Davison
ced3e7a579 fix: var_subkey schema 2024-05-10 16:41:02 +00:00
Orel Lazri
36dd71b122 fix(docs): add references to experiments links (#1644) 2024-05-09 21:30:20 +00:00
143 changed files with 6049 additions and 2854 deletions

View File

@@ -8,6 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true
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_size = 2

1
.gitattributes vendored
View File

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

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
strategy:
matrix:
go-version: [1.21.x, 1.22.x]
go-version: [1.22.x, 1.23.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v5
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.55.2
version: v1.60.1
lint-jsonschema:
runs-on: ubuntu-latest
@@ -34,10 +34,25 @@ jobs:
with:
python-version: 3.12
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema)
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
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.21.x
go-version: 1.22.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2

View File

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

View File

@@ -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
.idea/
.vscode/*
!.vscode/*-sample.json
.vscode/settings.json
.fleet/
# exuberant ctags

View File

@@ -1,3 +1,6 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
builds:
- binary: task
main: ./cmd/task
@@ -11,11 +14,16 @@ builds:
- amd64
- arm
- arm64
- riscv64
goarm:
- '6'
ignore:
- goos: darwin
goarch: '386'
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64
env:
- CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}'
@@ -41,7 +49,7 @@ release:
draft: true
snapshot:
name_template: "{{.Tag}}"
version_template: "{{.Tag}}"
checksum:
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,95 @@
# Changelog
## v3.39.2 - 2024-09-19
- Fix dynamic variables not working properly for a defer: statement (#1803,
#1818 by @vmaerten).
## 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
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
- Fixed a bug where includes Taskfile variable were not being merged correctly
(#1643, #1649 by @pd93).
## v3.37.1 - 2024-05-09
- Fix bug where non-string values (numbers, bools) added to `env:` weren't been
@@ -14,7 +104,7 @@
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
#1607 by @pd93).
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
@pd03).
@pd93).
- Fix error when a file or directory in the project contained a special char
like `&`, `(` or `)` (#1551, #1584 by @andreynering).
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import (
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -58,7 +59,7 @@ func run() error {
entrypoint := flags.Entrypoint
if flags.Version {
fmt.Printf("Task version: %s\n", ver.GetVersion())
fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
return nil
}
@@ -82,6 +83,15 @@ func run() error {
return nil
}
if flags.Completion != "" {
script, err := task.Completion(flags.Completion)
if err != nil {
return err
}
fmt.Println(script)
return nil
}
if flags.Global {
home, err := os.UserHomeDir()
if err != nil {
@@ -125,16 +135,15 @@ func run() error {
OutputStyle: flags.Output,
TaskSorter: taskSorter,
}
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil {
return err
}
if err := e.Setup(); err != nil {
err := e.Setup()
if err != nil {
return err
}
if experiments.AnyVariables.Enabled {
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
}
@@ -145,6 +154,14 @@ func run() error {
return nil
}
if flags.ClearCache {
cache, err := taskfile.NewCache(e.TempDir.Remote)
if err != nil {
return err
}
return cache.Clear()
}
if (listOptions.ShouldListTasks()) && flags.Silent {
return e.ListTaskNames(flags.ListAll)
}
@@ -179,6 +196,8 @@ func run() error {
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
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)
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
# 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
echo $output
end

View File

@@ -1,6 +1,5 @@
#compdef task
local context state state_descr line
compdef _task task
typeset -A opt_args
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
@@ -39,26 +38,33 @@ function __task_list() {
_describe 'Task to run' scripts
}
_arguments \
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
'(-c --color)'{-c,--color}'[colored output]' \
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
'(-s --silent)'{-s,--silent}'[disable echoing]' \
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
+ '(operation)' \
{-l,--list}'[list describable tasks]' \
{-a,--list-all}'[list all tasks]' \
{-i,--init}'[create new Taskfile.yml]' \
'(-*)'{-h,--help}'[show help]' \
'(-*)--version[show version and exit]' \
'*: :__task_list'
_task() {
_arguments \
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
'(-c --color)'{-c,--color}'[colored output]' \
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
'(-s --silent)'{-s,--silent}'[disable echoing]' \
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
+ '(operation)' \
{-l,--list}'[list describable tasks]' \
{-a,--list-all}'[list all tasks]' \
{-i,--init}'[create new Taskfile.yml]' \
'(-*)'{-h,--help}'[show help]' \
'(-*)--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 (
CodeTaskfileNotFound int = iota + 100
CodeTaskfileAlreadyExists
CodeTaskfileInvalid
CodeTaskfileDecode
CodeTaskfileFetchFailed
CodeTaskfileNotTrusted
CodeTaskfileNotSecure
CodeTaskfileCacheNotFound
CodeTaskfileVersionCheckError
CodeTaskfileNetworkTimeout
_ // CodeTaskfileDuplicateInclude
CodeTaskfileInvalid
CodeTaskfileCycle
)
@@ -58,3 +58,8 @@ func Is(err, target error) bool {
func As(err error, target any) bool {
return errors.As(err, target)
}
// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package.
func Unwrap(err error) error {
return errors.Unwrap(err)
}

View File

@@ -80,6 +80,19 @@ func (err *TaskNameConflictError) Code() int {
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
// exceeded. This is to prevent infinite loops and cyclic dependencies.
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
go 1.21.0
go 1.22.0
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/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/template v0.0.0-20240422130016-8f6b279b1e90
github.com/go-task/template v0.1.0
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/otiai10/copy v1.14.0
github.com/radovskyb/watcher v1.0.7
@@ -18,18 +20,20 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.7.0
golang.org/x/term v0.19.0
golang.org/x/sync v0.8.0
golang.org/x/term v0.24.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.8.0
mvdan.cc/sh/v3 v3.9.0
)
require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.25.0 // 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/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.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/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
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/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 h1:JBbiZ2CXIZ9Upe3O2yI5+3ksWoa7hNVNi4BINs8TIrs=
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 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
@@ -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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
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/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/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
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/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
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 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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY=
mvdan.cc/sh/v3 v3.9.0 h1:it14fyjCdQUk4jf/aYxLO3FG8jFarR9GzMCtnlvvD7c=
mvdan.cc/sh/v3 v3.9.0/go.mod h1:cdBk8bgoiBI7lSZqK5JhUuq7OB64VQ7fgm85xelw3Nk=

View File

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

35
help.go
View File

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

View File

@@ -3,14 +3,12 @@ package compiler
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
@@ -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) {
result := GetEnviron()
if t != nil {
specialVars, err := c.getSpecialVars(t)
specialVars, err := c.getSpecialVars(t, call)
if err != nil {
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 {
return err
}
// Evaluate JSON
if newVar.Json != "" {
if err := json.Unmarshal([]byte(newVar.Json), &newVar.Value); err != nil {
return err
}
}
// Evaluate YAML
if newVar.Yaml != "" {
if err := yaml.Unmarshal([]byte(newVar.Yaml), &newVar.Value); err != nil {
return err
}
}
// If the variable is not dynamic, we can set it and return
if newVar.Value != nil || newVar.Sh == "" {
result.Set(k, ast.Var{Value: newVar.Value})
@@ -193,9 +179,11 @@ func (c *Compiler) ResetCache() {
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{
"TASK": t.Task,
"ALIAS": call.Task,
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": c.Dir,
"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:
// 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 {
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
copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap()))
// 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 {
return err
}

9
internal/env/env.go vendored
View File

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

View File

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

View File

@@ -38,6 +38,7 @@ var (
Version bool
Help bool
Init bool
Completion string
List bool
ListAll bool
ListJson bool
@@ -65,6 +66,7 @@ var (
Experiments bool
Download bool
Offline bool
ClearCache bool
Timeout time.Duration
)
@@ -79,6 +81,7 @@ func init() {
pflag.BoolVar(&Version, "version", false, "Show Task version.")
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.StringVar(&Completion, "completion", "", "Generates shell completion script.")
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(&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(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
}
pflag.Parse()
@@ -129,6 +133,10 @@ func Validate() error {
return errors.New("task: You can't set both --download and --offline flags")
}
if Download && ClearCache {
return errors.New("task: You can't set both --download and --clear-cache flags")
}
if Global && Dir != "" {
log.Fatal("task: You can't set both --global and --dir")
return nil

View File

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

View File

@@ -52,6 +52,30 @@ func Red() PrintFunc {
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc()
}
func BrightBlue() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc()
}
func BrightGreen() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc()
}
func BrightCyan() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc()
}
func BrightYellow() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc()
}
func BrightMagenta() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc()
}
func BrightRed() PrintFunc {
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc()
}
func envColor(env string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false
@@ -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.
attributeStrs := strings.Split(override, ",")
if len(attributeStrs) == 3 {
attributeStrs = append([]string{"38", "2"}, attributeStrs...)
attributeStrs = slices.Concat([]string{"38", "2"}, attributeStrs)
} else {
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")
}
l.Outf(color, "%s [%s/%s]\n", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
l.Outf(color, "%s [%s/%s]: ", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
reader := bufio.NewReader(l.Stdin)
input, err := reader.ReadString('\n')

View File

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

View File

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

View File

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

View File

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

View File

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

2
package-lock.json generated
View File

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

View File

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

View File

@@ -1,13 +1,11 @@
package task
import (
"context"
"github.com/go-task/task/v3/errors"
"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 {
return nil
}

View File

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

View File

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

View File

@@ -20,9 +20,7 @@ import (
"time"
)
var (
SLEEPIT, _ = filepath.Abs("./bin/sleepit")
)
var SLEEPIT, _ = filepath.Abs("./bin/sleepit")
func TestSignalSentToProcessGroup(t *testing.T) {
task, err := getTaskPath()
@@ -147,7 +145,7 @@ func TestSignalSentToProcessGroup(t *testing.T) {
// 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
// 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 {
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
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger),
)
@@ -46,7 +46,7 @@ func (e *Executor) statusOnError(t *ast.Task) error {
if method == "" {
method = e.Taskfile.Method
}
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir, e.Dry)
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry)
if err != nil {
return err
}

73
task.go
View File

@@ -11,6 +11,8 @@ import (
"sync/atomic"
"time"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/env"
@@ -34,13 +36,18 @@ const (
MaximumTaskCall = 1000
)
type TempDir struct {
Remote string
Fingerprint string
}
// Executor executes a Taskfile
type Executor struct {
Taskfile *ast.Taskfile
Dir string
Entrypoint string
TempDir string
TempDir TempDir
Force bool
ForceAll bool
Insecure bool
@@ -183,16 +190,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
release := e.acquireConcurrencyLimit()
defer release()
if t.Prompt != "" {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
return e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil {
@@ -205,7 +202,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
return err
}
if err := e.areTaskRequiredVarsSet(ctx, t, call); err != nil {
if err := e.areTaskRequiredVarsSet(t, call); err != nil {
return err
}
@@ -222,7 +219,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger),
)
@@ -238,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 {
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
}
var deferredExitCode uint8
for i := range t.Cmds {
if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i)
defer e.runDeferred(t, call, i, &deferredExitCode)
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)
}
if execext.IsExitError(err) && t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue
exitCode, isExitError := interp.IsExitStatus(err)
if isExitError {
if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue
}
deferredExitCode = exitCode
}
if call.Indirect {
@@ -307,10 +320,26 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
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())
defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i]
vars, _ := e.Compiler.GetVariables(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 {
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 {
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)
return nil
}
@@ -489,14 +518,12 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks
for i := range tasks {
idx := i
task := tasks[idx]
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 {
return err
}
tasks[idx] = compiledTask
tasks[i] = compiledTask
return nil
})
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/go-task/task/v3"
"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/taskfile/ast"
)
@@ -60,17 +61,19 @@ func (fct fileContentTest) Run(t *testing.T) {
for f := range fct.Files {
_ = os.Remove(filepathext.SmartJoin(fct.Dir, f))
}
e := &task.Executor{
Dir: fct.Dir,
TempDir: filepathext.SmartJoin(fct.Dir, ".task"),
Dir: fct.Dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(fct.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"),
},
Entrypoint: fct.Entrypoint,
Stdout: io.Discard,
Stderr: io.Discard,
}
require.NoError(t, e.Setup(), "e.Setup()")
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: fct.Target}), "e.Run(target)")
for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) {
path := filepathext.SmartJoin(e.Dir, name)
@@ -95,7 +98,17 @@ func TestEmptyTask(t *testing.T) {
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
}
func TestEmptyTaskfile(t *testing.T) {
e := &task.Executor{
Dir: "testdata/empty_taskfile",
Stdout: io.Discard,
Stderr: io.Discard,
}
require.Error(t, e.Setup(), "e.Setup()")
}
func TestEnv(t *testing.T) {
t.Setenv("QUX", "from_os")
tt := fileContentTest{
Dir: "testdata/env",
Target: "default",
@@ -104,9 +117,21 @@ func TestEnv(t *testing.T) {
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
"multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n",
"not-overriden.txt": "QUX='from_os'\n",
},
}
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) {
@@ -263,11 +288,14 @@ func TestStatus(t *testing.T) {
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"),
Stdout: &buff,
Stderr: &buff,
Silent: true,
Dir: dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
require.NoError(t, e.Setup())
// gen-foo creates foo.txt, and will always fail it's status check.
@@ -459,7 +487,10 @@ func TestStatusChecksum(t *testing.T) {
}
var buff bytes.Buffer
tempdir := filepathext.SmartJoin(dir, ".task")
tempdir := task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}
e := task.Executor{
Dir: dir,
TempDir: tempdir,
@@ -476,7 +507,7 @@ func TestStatusChecksum(t *testing.T) {
// Capture the modification time, so we can ensure the checksum file
// is not regenerated when the hash hasn't changed.
s, err := os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task))
s, err := os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
require.NoError(t, err)
time := s.ModTime()
@@ -484,7 +515,7 @@ func TestStatusChecksum(t *testing.T) {
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.task}))
assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String())
s, err = os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task))
s, err = os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
require.NoError(t, err)
assert.Equal(t, time, s.ModTime())
})
@@ -794,7 +825,8 @@ func TestListDescInterpolation(t *testing.T) {
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) {
@@ -805,8 +837,11 @@ func TestStatusVariables(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"),
Dir: dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: &buff,
Stderr: &buff,
Silent: false,
@@ -954,11 +989,14 @@ func TestDryChecksum(t *testing.T) {
_ = os.Remove(checksumFile)
e := task.Executor{
Dir: dir,
TempDir: filepathext.SmartJoin(dir, ".task"),
Stdout: io.Discard,
Stderr: io.Discard,
Dry: true,
Dir: dir,
TempDir: task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
},
Stdout: io.Discard,
Stderr: io.Discard,
Dry: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
@@ -1033,7 +1071,7 @@ func TestIncludesIncorrect(t *testing.T) {
err := e.Setup()
require.Error(t, err)
assert.Contains(t, err.Error(), "task: Failed to parse testdata/includes_incorrect/incomplete.yml:")
assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error())
}
func TestIncludesEmptyMain(t *testing.T) {
@@ -1192,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) {
const dir = "testdata/includes_interpolation"
tests := []struct {
@@ -1228,6 +1305,34 @@ func TestIncludesInterpolation(t *testing.T) {
}
}
func TestIncludedTaskfileVarMerging(t *testing.T) {
const dir = "testdata/included_taskfile_var_merging"
tests := []struct {
name string
task string
expectedOutput string
}{
{"foo", "foo:pwd", "included_taskfile_var_merging/foo\n"},
{"bar", "bar:pwd", "included_taskfile_var_merging/bar\n"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &ast.Call{Task: test.task})
require.NoError(t, err)
assert.Contains(t, buff.String(), test.expectedOutput)
})
}
}
func TestInternalTask(t *testing.T) {
const dir = "testdata/internal_task"
tests := []struct {
@@ -1627,6 +1732,26 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
tt.Run(t)
}
func TestRunOnceSharedDeps(t *testing.T) {
const dir = "testdata/run_once_shared_deps"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
ForceAll: true,
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "build"}))
rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`)
matches := rx.FindAllStringSubmatch(buff.String(), -1)
assert.Len(t, matches, 1)
assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`)
assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`)
}
func TestDeferredCmds(t *testing.T) {
const dir = "testdata/deferred"
var buff bytes.Buffer
@@ -1652,6 +1777,34 @@ task-1 ran successfully
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 - DYNAMIC_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 - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String()))
}
func TestIgnoreNilElements(t *testing.T) {
tests := []struct {
name string
@@ -2223,6 +2376,10 @@ func TestForCmds(t *testing.T) {
name: "loop-explicit",
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",
expectedOutput: "bar\nfoo\n",
@@ -2280,6 +2437,17 @@ func TestForDeps(t *testing.T) {
name: "loop-explicit",
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",
expectedOutputContains: []string{"bar\n", "foo\n"},
@@ -2389,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
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
@@ -46,7 +45,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
c.Cmd = cmd
return nil
@@ -110,8 +109,8 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil
}
return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line)
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag())
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command")
}

View File

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

View File

@@ -1,19 +1,20 @@
package ast
import (
"fmt"
"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/omap"
)
type For struct {
From string
List []any
Var string
Split string
As string
From string
List []any
Matrix omap.OrderedMap[string, []any]
Var string
Split string
As string
}
func (f *For) UnmarshalYAML(node *yaml.Node) error {
@@ -22,7 +23,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var from string
if err := node.Decode(&from); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
f.From = from
return nil
@@ -30,28 +31,35 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
case yaml.SequenceNode:
var list []any
if err := node.Decode(&list); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
f.List = list
return nil
case yaml.MappingNode:
var forStruct struct {
Var string
Split string
As string
Matrix omap.OrderedMap[string, []any]
Var string
Split string
As string
}
if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" {
f.Var = forStruct.Var
f.Split = forStruct.Split
f.As = forStruct.As
return nil
if err := node.Decode(&forStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line)
if forStruct.Var == "" && forStruct.Matrix.Len() == 0 {
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 {
@@ -59,10 +67,11 @@ func (f *For) DeepCopy() *For {
return nil
}
return &For{
From: f.From,
List: deepcopy.Slice(f.List),
Var: f.Var,
Split: f.Split,
As: f.As,
From: f.From,
List: deepcopy.Slice(f.List),
Matrix: f.Matrix.DeepCopy(),
Var: f.Var,
Split: f.Split,
As: f.As,
}
}

View File

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

View File

@@ -1,10 +1,9 @@
package ast
import (
"fmt"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
omap "github.com/go-task/task/v3/internal/omap"
)
@@ -18,6 +17,7 @@ type Include struct {
Aliases []string
AdvancedImport bool
Vars *Vars
Flatten bool
}
// Includes represents information about included tasksfiles
@@ -38,7 +38,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
var v Include
if err := valueNode.Decode(&v); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
v.Namespace = keyNode.Value
includes.Set(keyNode.Value, &v)
@@ -46,7 +46,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
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
@@ -71,7 +71,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
include.Taskfile = str
return nil
@@ -82,11 +82,12 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Dir string
Optional bool
Internal bool
Flatten bool
Aliases []string
Vars *Vars
}
if err := node.Decode(&includedTaskfile); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
include.Taskfile = includedTaskfile.Taskfile
include.Dir = includedTaskfile.Dir
@@ -95,10 +96,11 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.Aliases = includedTaskfile.Aliases
include.AdvancedImport = true
include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten
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
@@ -115,5 +117,6 @@ func (include *Include) DeepCopy() *Include {
Internal: include.Internal,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,12 @@ package ast
import (
"fmt"
"slices"
"strings"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/omap"
)
@@ -45,43 +47,48 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
return matchingTasks
}
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
_ = t2.Range(func(k string, v *Task) error {
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) 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
// be changed elsewhere once the taskfile is merged.
task := v.DeepCopy()
// Set the task to internal if EITHER the included task or the included
// taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal)
// Add namespaces to task dependencies
for _, dep := range task.Deps {
if dep != nil && dep.Task != "" {
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
}
}
// Add namespaces to task commands
for _, cmd := range task.Cmds {
if cmd != nil && cmd.Task != "" {
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
}
}
// Add namespaces to task aliases
for i, alias := range task.Aliases {
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
}
// Add namespace aliases
if include != nil {
for _, namespaceAlias := range include.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
for _, alias := range v.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
taskName := name
if !include.Flatten {
// Add namespaces to task dependencies
for _, dep := range task.Deps {
if dep != nil && dep.Task != "" {
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
}
}
// Add namespaces to task commands
for _, cmd := range task.Cmds {
if cmd != nil && cmd.Task != "" {
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
}
}
// Add namespaces to task aliases
for i, alias := range task.Aliases {
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
}
// Add namespace aliases
if include != nil {
for _, namespaceAlias := range include.Aliases {
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 {
@@ -90,27 +97,32 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
task.IncludeVars = &Vars{}
}
task.IncludeVars.Merge(include.Vars, nil)
task.IncludedTaskfileVars = includedTaskfileVars
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
}
if t1.Get(taskName) != nil {
return &errors.TaskNameFlattenConflictError{
TaskName: taskName,
Include: include.Namespace,
}
}
// Add the task to the merged taskfile
taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace)
task.Task = taskNameWithNamespace
t1.Set(taskNameWithNamespace, task)
t1.Set(taskName, task)
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
// run the included Taskfile's default task without specifying its full
// name. If the parent namespace has aliases, we add another alias for each
// 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)
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 {
@@ -118,7 +130,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode:
tasks := omap.New[string, *Task]()
if err := node.Decode(&tasks); err != nil {
return err
return errors.NewTaskfileDecodeError(err, node)
}
// nolint: errcheck
@@ -150,7 +162,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
return nil
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag())
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks")
}
func taskNameWithNamespace(taskName string, namespace string) string {

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,9 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*BaseNode
URL *url.URL
URL *url.URL
logger *logger.Logger
timeout time.Duration
}
func NewHTTPNode(
@@ -36,18 +38,12 @@ func NewHTTPNode(
if url.Scheme == "http" && !insecure {
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{
BaseNode: base,
URL: url,
timeout: timeout,
logger: l,
}, nil
}
@@ -60,6 +56,11 @@ func (node *HTTPNode) Remote() bool {
}
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)
if err != nil {
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))
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()}
}
defer resp.Body.Close()
@@ -110,3 +114,8 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil
}
func (node *HTTPNode) FilenameAndLastDir() (string, string) {
dir, filename := filepath.Split(node.URL.Path)
return filepath.Base(dir), filename
}

View File

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

View File

@@ -109,6 +109,7 @@ func (r *Reader) include(node Node) error {
Dir: templater.Replace(include.Dir, cache),
Optional: include.Optional,
Internal: include.Internal,
Flatten: include.Flatten,
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Vars: include.Vars,
@@ -207,8 +208,9 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
// Read the file
b, err = node.Read(ctx)
var taskfileNetworkTimeoutError *errors.TaskfileNetworkTimeoutError
// 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 r.download {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
@@ -263,23 +265,33 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
}
}
var t ast.Taskfile
if err := yaml.Unmarshal(b, &t); err != nil {
var tf ast.Taskfile
if err := yaml.Unmarshal(b, &tf); err != nil {
// Decode the taskfile and add the file info the any errors
taskfileInvalidErr := &errors.TaskfileDecodeError{}
if errors.As(err, &taskfileInvalidErr) {
return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2)
}
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
}
// Check that the Taskfile is set and has a schema version
if tf.Version == nil {
return nil, &errors.TaskfileVersionCheckError{URI: node.Location()}
}
// Set the taskfile/task's locations
t.Location = node.Location()
for _, task := range t.Tasks.Values() {
tf.Location = node.Location()
for _, task := range tf.Tasks.Values() {
// If the task is not defined, create a new one
if task == nil {
task = &ast.Task{}
}
// Set the location of the taskfile for each task
if task.Location.Taskfile == "" {
task.Location.Taskfile = t.Location
task.Location.Taskfile = tf.Location
}
}
return &t, nil
return &tf, nil
}

View File

@@ -8,6 +8,7 @@ import (
"path/filepath"
"slices"
"strings"
"time"
"github.com/go-task/task/v3/errors"
"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
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, 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
req, err := http.NewRequest("HEAD", u.String(), 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
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
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()}
}
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
BAR: bar
BAZ: "{{.BAZ}}"
QUX: from_taskfile
tasks:
default:
cmds:
- task: local
- task: global
- task: not-overriden
- task: multiple_type
local:
@@ -40,3 +42,11 @@ tasks:
BAZ: 1.1
cmds:
- 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

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

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

View File

@@ -7,6 +7,14 @@ tasks:
- for: ["a", "b", "c"]
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-sources:
sources:

View File

@@ -9,6 +9,16 @@ tasks:
vars:
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-sources:
sources:

View File

@@ -0,0 +1,12 @@
version: "3"
includes:
foo:
taskfile: ./foo/Taskfile.yaml
bar:
taskfile: ./bar/Taskfile.yaml
tasks:
stub:
cmds:
- echo 0

View File

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

View File

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

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'
vars:
FOO: bar
FOO: foo
BAR: bar
tasks:
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
tasks:
print-task: echo {{.TASK}}
print-task:
aliases: [echo-task]
cmds:
- echo {{.TASK}}
print-root-dir: echo {{.ROOT_DIR}}
print-taskfile: echo {{.TASKFILE}}
print-taskfile-dir: echo {{.TASKFILE_DIR}}
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'
tasks:
print-task: echo {{.TASK}}
print-task:
aliases: [echo-task]
cmds:
- echo {{.TASK}}
print-root-dir: echo {{.ROOT_DIR}}
print-taskfile: echo {{.TASKFILE}}
print-taskfile-dir: echo {{.TASKFILE_DIR}}
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-resolver
- task: json
- task: yaml
map:
vars:
@@ -93,25 +92,13 @@ tasks:
JSON_STRING:
sh: cat example.json
JSON:
json: "{{.JSON_STRING}}"
ref: "fromJson .JSON_STRING"
cmds:
- task: print-story
vars:
VAR:
ref: .JSON
yaml:
vars:
YAML_STRING:
sh: cat example.yaml
YAML:
yaml: "{{.YAML_STRING}}"
cmds:
- task: print-story
vars:
VAR:
ref: .YAML
print-var:
cmds:
- echo "{{.VAR}}"

View File

@@ -11,6 +11,7 @@ import (
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"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/taskfile/ast"
)
@@ -72,6 +73,7 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
Location: origTask.Location,
Requires: origTask.Requires,
Watch: origTask.Watch,
Namespace: origTask.Namespace,
}
new.Dir, err = execext.Expand(new.Dir)
if err != nil {
@@ -160,6 +162,12 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
}
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 = templater.Replace(cmd.Cmd, 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 {
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir, e.Dry)
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir, e.Dry)
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
value, err := checker.Value(&new)
@@ -264,9 +272,13 @@ func itemsFromFor(
) ([]any, []string, error) {
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
// 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
if f.List != nil && len(f.List) > 0 {
values = f.List
if len(f.List) > 0 {
return f.List, nil, nil
}
// Get the list from the task sources
if f.From == "sources" {
@@ -315,3 +327,39 @@ func itemsFromFor(
}
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
cmds:
- 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`,
`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.
### Looping over variables using `for`

View File

@@ -1,4 +1,7 @@
export const GITHUB_URL = 'https://github.com/go-task/task';
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 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%

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