Compare commits

...

226 Commits

Author SHA1 Message Date
Andrey Nering
a2390d0dca v3.41.0 2025-01-18 11:15:57 -03:00
Andrey Nering
0f633091eb chore: fix typo on changelog 2025-01-18 10:54:07 -03:00
Andrey Nering
6b16c532c2 chore: add changelog for #1938 2025-01-18 10:27:21 -03:00
Lea Anthony
69f5714e45 fix: disable version check for use as an external library
Closes #1938
2025-01-18 10:26:58 -03:00
Andrey Nering
b3e4cfcf48 refactor: use modern loop syntax
ref #1980
2025-01-18 10:11:00 -03:00
EinoPlasma
65a71e5df3 refactor: signal handling to improve clarity and correctness (#1980) 2025-01-18 13:09:36 +00:00
jonathanagustin
bad2c8fcc1 docs: fix some examples to use spaces instead of tabs (#2002) 2025-01-18 10:04:39 -03:00
dependabot[bot]
97f41b710e chore(deps): bump github.com/go-git/go-git/v5 from 5.13.0 to 5.13.1 (#1992)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-05 16:51:59 +01:00
Pete Davison
240047152d chore: changelog for #1869 2025-01-02 20:11:00 +00:00
rohm1
24a830e384 fix: forward env to RunCommand when evaluating sh vars (#1869)
* forward env to RunCommand when evaluating sh vars. fixes #1742

* feat: added tests

* fix: test

---------

Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2025-01-02 20:07:25 +00:00
Pete Davison
fe9f489702 chore: changelog for #1989 2025-01-02 18:23:06 +00:00
Pete Davison
27de441ed2 docs: updated installation guides (#1989)
* docs: updated installation guides

* fix: installation method titles in sidebar

* feat: add instructions for pip

* fix: anchors in installation doc
2025-01-02 18:20:29 +00:00
renovate[bot]
79f7af2b04 fix(deps): update golang (#1983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 14:46:28 -03:00
Oren
b588d49cfb fix typo in usage.mdx (#1985) 2025-01-02 17:43:40 +00:00
renovate[bot]
45006e2ce0 fix(deps): update website (#1984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-01 11:30:59 +01:00
Pete Davison
e5d8237053 docs: package api doc (#1981) 2024-12-31 16:08:58 +00:00
Valentin Maerten
89740ed72a chore: changelog for #1872 2024-12-31 16:06:44 +01:00
Niklas Rousset
c1e14c461b feat: make CHECKSUM and TIMESTAMP vars available in cmds commands (#1872) 2024-12-31 16:04:42 +01:00
Pete Davison
dc2eceb634 chore: update experiments field in issue template 2024-12-30 22:09:45 +00:00
Andrey Nering
43f3dcea05 chore(requires): skip unneeded variable evaluation for requires (#1976) 2024-12-30 18:27:16 -03:00
Valentin Maerten
f27daea5c9 chore: changelog for #1960 2024-12-30 19:17:55 +01:00
Pete Davison
49e88e92cf chore: changelogs for #1797, #1972 and #1974 2024-12-30 18:13:43 +00:00
Valentin Maerten
da40aabcc7 fix: task level vars are not ignored in requires (#1960)
Co-authored-by: MOKEKO <Anthurium.1605@gmail.com>
2024-12-30 19:11:27 +01:00
Pete Davison
8ce9bdc8c7 refactor: remove pointer from prefixed mutex 2024-12-30 18:03:29 +00:00
Graham Dennis
0409c3c3ba fix: concurrent mutations to prefixWriter (#1974) 2024-12-30 18:02:34 +00:00
Pete Davison
fd3532812e fix: orderedmap race condition (#1972) 2024-12-30 17:58:45 +00:00
Pete Davison
2965841eb7 feat: use external package for ordered maps (#1797) 2024-12-30 17:54:36 +00:00
Andrey Nering
dbe6e41ac8 chore(website): update navbar icons 2024-12-30 14:36:44 -03:00
Valentin Maerten
8f73ced037 fix: missing t.Parallel in some tests 2024-12-30 11:51:13 +01:00
Valentin Maerten
2a4f93eb41 chore: changelog for #1961 2024-12-30 11:51:13 +01:00
Valentin Maerten
9d8c4ba7e6 feat: add TASK_DIR special variable (#1961)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2024-12-30 11:45:25 +01:00
Valentin Maerten
1bda388925 chore: changelog for #1962 2024-12-30 10:20:07 +01:00
Valentin Maerten
d64df3f9d7 fix: evaluate requires before compiled task (#1962) 2024-12-30 10:15:17 +01:00
Valentin Maerten
d1f18d36b8 chore: changelog for #1859 2024-12-30 10:10:11 +01:00
Valentin Maerten
5f1d46c770 feat: can exclude task from being included (#1859) 2024-12-30 10:09:28 +01:00
Jonathan Rayner
9727eef476 docs: update Arch installation instructions (#1971) 2024-12-29 15:13:14 +01:00
Andrey Nering
c5be676555 chore(website): mention devowl.io as a gold sponsor 2024-12-20 10:23:01 -03:00
Pete Davison
f3317266dc feat: issue templates (#1963) 2024-12-16 12:36:45 -06:00
Andrey Nering
36ff00e3f9 chore: change some mentions of project authors 2024-12-11 22:04:41 -03:00
Lumberjackz
041063b732 docs: fix typo (#1895) 2024-12-11 21:50:43 -03:00
CeMoN24
2ab1dcbf1d docs: fix typo (#1896) 2024-12-11 21:50:05 -03:00
Matthias Vogt
24a0f24835 docs: fix typo (#1897) 2024-12-11 21:49:42 -03:00
christiandins
4dffab2e0a feat: add parallel test execution to improve runtime (#1882) 2024-12-11 21:47:10 -03:00
christiandins
b9a5d1c573 lint: add linter noctx (#1898) 2024-12-11 21:42:04 -03:00
renovate[bot]
e1818e9e31 chore(deps): update node.js to v22 (#1899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-11 21:36:05 -03:00
dependabot[bot]
bb2de3fdf9 chore(deps): bump golang.org/x/crypto from 0.25.0 to 0.31.0 (#1949)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 00:24:44 +00:00
Martijn Pieters
82f6029043 docs: document defer task options (#1907) 2024-12-11 21:23:10 -03:00
Valentin Maerten
cfaecf8b4c chore: changelog for #1879 2024-12-07 16:16:47 +01:00
Danilo Bürger
4595c1e32a feat: add silent for defer (#1879)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2024-12-07 16:16:27 +01:00
dependabot[bot]
1a648dea50 chore(deps): bump golang.org/x/term from 0.26.0 to 0.27.0 (#1942)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-07 16:11:53 +01:00
renovate[bot]
a273183745 chore(deps): update website (#1935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-07 16:11:20 +01:00
dependabot[bot]
e2243fc6d9 chore(deps): bump golang.org/x/sync from 0.9.0 to 0.10.0 (#1941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-07 16:08:37 +01:00
Valentin Maerten
c1209d9f13 chore: changelog for #1921 2024-12-07 16:06:16 +01:00
mgbowman
2b54b04cfc fix: dynamic variable output in verbose mode (#1921)
Co-authored-by: Matthew Bowman <mbowman@nvidia.com>
2024-12-07 16:05:53 +01:00
Andrey Nering
32fa3a0156 v3.40.1 2024-12-06 09:59:04 -03:00
dependabot[bot]
973e928c28 chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /website (#1934)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-30 12:01:46 -03:00
Oleksandr Redko
bc844246d4 chore: allow using only github.com/go-task/task/v3/errors package (#1926) 2024-11-30 12:00:58 -03:00
dependabot[bot]
41884f0a69 chore(deps): bump github.com/Masterminds/semver/v3 from 3.3.0 to 3.3.1 (#1927)
Bumps [github.com/Masterminds/semver/v3](https://github.com/Masterminds/semver) from 3.3.0 to 3.3.1.
- [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.3.0...v3.3.1)

---
updated-dependencies:
- dependency-name: github.com/Masterminds/semver/v3
  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-11-30 11:58:17 -03:00
dependabot[bot]
2a96c20739 chore(deps): bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#1928)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  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-11-30 11:58:00 -03:00
dependabot[bot]
c28eb204fb chore(deps): bump golang.org/x/term from 0.25.0 to 0.26.0 (#1913)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 21:54:17 +01:00
dependabot[bot]
b1535aedc1 chore(deps): bump golang.org/x/sync from 0.8.0 to 0.9.0 (#1914)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-15 21:50:03 +01:00
Valentin Maerten
7e3feb2993 chore: changelog for #1883 2024-11-15 21:49:21 +01:00
christiandins
8a79a41717 chore: add linter misspell (#1883) 2024-11-15 21:48:59 +01:00
Valentin Maerten
530818a742 chore: changelog for #1915 2024-11-15 21:42:15 +01:00
Dorian Karter
517bb3fc97 fix(json-schema): add missing platforms property to cmds for (#1915) 2024-11-15 21:41:54 +01:00
Valentin Maerten
6645a1f34c chore: changelog for #1917 2024-11-12 11:20:15 +01:00
Alexey Palazhchenko
2aa2963565 chore(deps): switch to mainted fork of git-urls (#1917) 2024-11-12 09:29:29 +01:00
Oleksandr Redko
390220ec9c fix: typos in docs, changelog, testdata, comments (#1910) 2024-11-07 13:25:59 -03:00
Andrey Nering
c3bd6b9384 chore(taskfile): fix goreleaser:install task to install correct version 2024-11-05 23:21:24 -03:00
Andrey Nering
d8e176311d v3.40.0 2024-11-05 22:34:38 -03:00
dependabot[bot]
1c68f0fee4 chore(deps): bump http-proxy-middleware from 2.0.6 to 2.0.7 in /website (#1886)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 19:58:26 +01:00
Pete Davison
118ef01a69 chore: changelog for #1904 2024-11-04 13:32:47 +00:00
Pete Davison
148b090d8e fix: bug where non-nil, empty dynamic variables are returned as an empty interface (#1904) 2024-11-04 13:30:39 +00:00
Norbert Hauriel
28a96d1427 docs(flags.go): flag description typo (#1905) 2024-11-04 12:58:48 +00:00
George Green
47f5e6ab89 docs: add an example of a default value usage in vars (#1893) 2024-11-01 20:04:46 +01:00
renovate[bot]
fe09c01637 chore(deps): update website (#1891)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 15:31:56 +01:00
dependabot[bot]
7ef3164b16 chore(deps): bump github.com/fatih/color from 1.17.0 to 1.18.0 (#1885)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 15:00:57 +01:00
dependabot[bot]
b48a32b103 chore(deps): bump github.com/go-git/go-billy/v5 from 5.5.0 to 5.6.0 (#1884)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-29 14:52:53 +01:00
Valentin Maerten
2d2c408652 chore: changelog for #1890 2024-10-29 14:50:34 +01:00
Amogh Rameshappa Devapura
c381923d3e feat: add numCPU func (#1890) 2024-10-29 14:50:17 +01:00
Pete Davison
7bfddaa25a chore: changelog for #1866 2024-10-29 13:39:04 +00:00
Matheus Mina
5581954fb1 feat: allow providing single or multi prompts (#1866)
* Add new type to handle single or multi prompts

* update docs

* apply review
2024-10-29 13:37:03 +00:00
renovate[bot]
c4f708b222 fix(deps): update module mvdan.cc/sh/v3 to v3.10.0 (#1874)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 19:52:54 +02:00
Valentin Maerten
27056a9827 chore: changelog for #1827 2024-10-18 18:17:18 +02:00
Valentin Maerten
a35910429c feat: option to ensure variable is within the list of values (#1827) 2024-10-18 18:16:57 +02:00
Valentin Maerten
9a7e79258c chore: changelog for #1771 2024-10-18 18:14:07 +02:00
Paulo Bittencourt
8dd3f4b119 refactor: re-organize node loading code to make it easier to follow (#1771) 2024-10-18 18:13:25 +02:00
Valentin Maerten
9ecc8fc878 chore: changelog for #1810 2024-10-09 09:14:56 +02:00
Valentin Maerten
e078261f12 fix: special variables are defined with dotenv at task level (#1810) 2024-10-09 03:14:23 -04:00
Andrey Nering
bdb3ffddd1 chore: add changelog for #1757 2024-10-05 21:42:35 -03:00
Paulo Bittencourt
a72e70b026 fix: inconsistent current directory resolution depending on include order (#1757) 2024-10-05 21:40:22 -03:00
Paulo Bittencourt
c5eea294aa ci: fix flaky TestForDeps tests (#1839) 2024-10-05 21:25:12 -03:00
renovate[bot]
0fff404eb8 chore(deps): update goreleaser/goreleaser-action action to v6 (#1852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-06 00:21:52 +00:00
renovate[bot]
61172fa8da chore(deps): update dependency @types/react to v18.3.11 (#1851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 21:20:21 -03:00
dependabot[bot]
a6bc3f51cc chore(deps): bump golang.org/x/term from 0.24.0 to 0.25.0 (#1857)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/term/compare/v0.24.0...v0.25.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-10-05 21:19:15 -03:00
renovate[bot]
1af7bf2670 chore(deps): update actions/github-script action to v7 (#1849)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-05 03:27:06 -04:00
dependabot[bot]
d75536bf00 chore(deps): bump express from 4.19.2 to 4.21.0 in /website (#1815)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-29 16:08:03 -04:00
Valentin Maerten
ce3e058f89 chore: changelog for #1842 2024-09-29 22:05:33 +02:00
Paulo Bittencourt
8d0f0b049c fix: Print dotenv file path when there is an error reading file (#1842) 2024-09-29 16:03:48 -04:00
Valentin Maerten
e619bad4a9 chore: changelog for #1652 2024-09-24 19:45:59 +02:00
Valentin Maerten
e6ea0647d7 feat(remote): support include git remote (#1652) 2024-09-24 13:44:54 -04:00
renovate[bot]
d1dc271b9a chore(deps): update website (#1834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-24 13:26:36 -04:00
Valentin Maerten
f5082f3692 chore: changelog for #1833 2024-09-24 19:23:18 +02:00
Valentin Maerten
30c59bf387 fix(remote): wait for prompt in the reader (#1833) 2024-09-24 13:21:09 -04:00
renovate[bot]
38d0fc2c55 chore(deps): update tj-actions/changed-files action to v45 (#1835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 15:31:51 -04:00
George Rawlinson
460e587c66 fix: checksum override when passed via ldflags (#1830) 2024-09-23 13:45:41 -04:00
Valentin Maerten
ad5a3166ac chore: changelog for #1716 2024-09-21 17:24:33 +02:00
Valentin Maerten
ddccd1bb61 feat: add TASK_OFFLINE env and expose it as a special variable (#1716)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2024-09-21 11:17:15 -04:00
Pete Davison
96a690ac2f chore: changelog for #1822 2024-09-20 17:08:54 +00:00
Piotr Stawarski
cb07189bab Fix: Cannot use splitArgs and splitLines in for-loops (#1823)
* Update variables.go

Probably solves https://github.com/go-task/task/issues/1822

* add type casting

* reorder to look better

* add suport for []int functions (until, untilStep)
2024-09-20 18:05:19 +01:00
Carlos Alexandro Becker
7e6577eb5f fix: snapshot builds and wrong winget version (#1824) 2024-09-20 10:13:42 -03:00
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
204 changed files with 11560 additions and 6519 deletions

View File

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

1
.gitattributes vendored
View File

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

View File

@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrey@nering.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at task@taskfile.dev. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

2
.github/FUNDING.yml vendored
View File

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

View File

@@ -1,20 +0,0 @@
---
name: Bug Report
about: Use this to report bugs and issues
---
<!--
Thanks for your bug report!
Before submitting this issue, please make sure the same problem was not
already reported by someone else.
Please describe the bug you're facing. Consider pasting example Taskfiles
showing how to reproduce the problem.
-->
- Task version:
- Operating system:
- Experiments enabled:

69
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: '🐞 Bug Report'
description: Report a bug in Task.
labels: ['state: needs-triage']
body:
- type: markdown
attributes:
value: |
Thanks for your bug report!
Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same bug was not already reported by someone else.
- type: textarea
id: description
attributes:
label: Description
description: Describe the bug you're seeing.
placeholder: |
- What did you do?
- What did you expect to happen?
- What happened instead?
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version(s) of Task is the issue occurring on?
validations:
required: true
- type: input
id: os
attributes:
label: Operating system
description: What operating system(s) is the issue occurring on?
validations:
required: true
- type: dropdown
id: experiments
attributes:
label: Experiments Enabled
description: Do you have any experiments enabled? You can check by running `task --experiments`.
multiple: true
options:
- Env Precedence
- Gentle Force
- Map Variables (1)
- Map Variables (2)
- Remote Taskfiles
validations:
required: false
- type: textarea
id: logs
attributes:
label: Example Taskfile
description: |
If you have a Taskfile that reproduces the issue, please paste it here.
This will be automatically formatted into code, so no need for backticks.
render: YAML
placeholder: |
version: '3'
tasks:
default:
cmds:
- 'echo "This Taskfile is buggy :("'

View File

@@ -1,11 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Extension for Visual Studio Code - name: '🔌 Task for Visual Studio Code'
url: https://github.com/go-task/vscode-task url: https://github.com/go-task/vscode-task
about: Issues related to the Visual Studio Code extension should be opened here. about: 'Issues related to the Visual Studio Code extension should be opened here.'
- name: Help forum on Discord - name: '💬 Help forum on Discord'
url: https://discord.gg/6TY36E39UK url: https://discord.com/channels/974121106208354339/1025054680289660989
about: 'The Discord #help channel is the best way to get help from the community.' about: 'The #help channel on our Discord is the best way to get help from the community.'
- name: Questions, Ideas and General Discussions - name: '❓ Questions, Ideas and General Discussions'
url: https://github.com/go-task/task/discussions url: https://github.com/go-task/task/discussions
about: Ask questions and discuss general ideas with the community. about: 'Ask questions and discuss general ideas with the community.'

View File

@@ -1,15 +0,0 @@
---
name: Feature Request
about: Use this to make feature requests
---
<!--
Describe in detail what feature do you want to see in Task.
Give examples if possible.
Please, search if this wasn't proposed before, and if this is more like an idea
than a strong feature request, consider opening a
[discussion](https://github.com/go-task/task/discussions) instead.
-->

View File

@@ -0,0 +1,23 @@
name: '✨ Feature Request'
description: Suggest a new feature or enhancement for Task.
labels: ['state: needs-triage']
body:
- type: markdown
attributes:
value: |
Thanks for your feature request!
Before submitting, please check the list of [existing issues](https://github.com/go-task/task/issues) and make sure the same change was not already requested by someone else.
If your request is more of an idea than a feature request, consider opening a [discussion](https://github.com/go-task/task/discussions) instead.
- type: textarea
id: description
attributes:
label: Description
description: Describe the feature/enhancement you want to see in Task.
placeholder: |
- Give a general overview of the feature/enhancement.
- Explain problem is the change trying to solve.
- Give examples of how you would use the feature.
validations:
required: true

View File

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

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

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

View File

@@ -8,7 +8,7 @@ jobs:
issue-awaiting-response: issue-awaiting-response:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -8,7 +8,7 @@ jobs:
issue-closed: issue-closed:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -9,7 +9,7 @@ jobs:
if: github.event.label.name == format('experiment{0} proposed', ':') if: github.event.label.name == format('experiment{0} proposed', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -23,7 +23,7 @@ jobs:
if: github.event.label.name == format('experiment{0} draft', ':') if: github.event.label.name == format('experiment{0} draft', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -37,7 +37,7 @@ jobs:
if: github.event.label.name == format('experiment{0} candidate', ':') if: github.event.label.name == format('experiment{0} candidate', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -51,7 +51,7 @@ jobs:
if: github.event.label.name == format('experiment{0} stable', ':') if: github.event.label.name == format('experiment{0} stable', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -65,7 +65,7 @@ jobs:
if: github.event.label.name == format('experiment{0} released', ':') if: github.event.label.name == format('experiment{0} released', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -85,7 +85,7 @@ jobs:
if: github.event.label.name == format('experiment{0} abandoned', ':') if: github.event.label.name == format('experiment{0} abandoned', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |
@@ -105,7 +105,7 @@ jobs:
if: github.event.label.name == format('experiment{0} superseded', ':') if: github.event.label.name == format('experiment{0} superseded', ':')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -8,7 +8,7 @@ jobs:
issue-needs-triage: issue-needs-triage:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v6 - uses: actions/github-script@v7
with: with:
github-token: ${{secrets.GH_PAT}} github-token: ${{secrets.GH_PAT}}
script: | script: |

View File

@@ -13,19 +13,19 @@ jobs:
name: Lint name: Lint
strategy: strategy:
matrix: matrix:
go-version: [1.21.x, 1.22.x] go-version: [1.22.x, 1.23.x]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: ${{matrix.go-version}} go-version: ${{matrix.go-version}}
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.55.2 version: v1.60.1
lint-jsonschema: lint-jsonschema:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -34,10 +34,25 @@ jobs:
with: with:
python-version: 3.12 python-version: 3.12
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: install check-jsonschema - name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3' run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema) - name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/static/schema.json run: check-jsonschema --check-metaschema website/static/schema.json
check_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v45
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,15 +10,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: 1.21.x go-version: 1.22.x
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v6
with: with:
version: latest version: latest
args: release --clean args: release --clean

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

@@ -5,11 +5,28 @@
linters: linters:
enable: enable:
- depguard
- goimports - goimports
- gofmt - gofmt
- gofumpt - gofumpt
- misspell
- noctx
- paralleltest
- tenv
- thelper
- tparallel
linters-settings: linters-settings:
depguard:
rules:
main:
files:
- "$all"
- "!$test"
- "!**/errors/*.go"
deny:
- pkg: "errors"
desc: "Use github.com/go-task/task/v3/errors instead"
goimports: goimports:
local-prefixes: github.com/go-task local-prefixes: github.com/go-task
gofmt: gofmt:

View File

@@ -1,3 +1,6 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
builds: builds:
- binary: task - binary: task
main: ./cmd/task main: ./cmd/task
@@ -11,11 +14,16 @@ builds:
- amd64 - amd64
- arm - arm
- arm64 - arm64
- riscv64
goarm: goarm:
- '6' - '6'
ignore: ignore:
- goos: darwin - goos: darwin
goarch: '386' goarch: '386'
- goos: darwin
goarch: riscv64
- goos: windows
goarch: riscv64
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}' mod_timestamp: '{{ .CommitTimestamp }}'
@@ -41,7 +49,7 @@ release:
draft: true draft: true
snapshot: snapshot:
name_template: "{{.Tag}}" version_template: "{{.Version}}"
checksum: checksum:
name_template: "task_checksums.txt" name_template: "task_checksums.txt"
@@ -49,7 +57,7 @@ checksum:
nfpms: nfpms:
- vendor: Task - vendor: Task
homepage: https://taskfile.dev homepage: https://taskfile.dev
maintainer: Andrey Nering <andrey@nering.com.br> maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go description: Simple task runner written in Go
license: MIT license: MIT
conflicts: conflicts:

2
.nvmrc
View File

@@ -1 +1 @@
18.12.1 22.12.0

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,152 @@
# Changelog # Changelog
## v3.41.0 - 2025-01-18
- Fixed an issue where dynamic variables were not properly logged in verbose
mode (#1920, #1921 by @mgbowman).
- Support `silent` for defer statements (#1877, #1879 by @danilobuerger).
- Added an option to exclude some tasks from being included (#1859 by
@vmaerten).
- Fixed an issue where a required variable was incorrectly handled in a template
function (#1950, #1962 by @vmaerten).
- Expose a new `TASK_DIR` special variable, which will contain the absolute path
of task directory. (#1959, #1961 by @vmaerten).
- Fixed fatal bugs that caused concurrent map writes (#1605, #1972, #1974 by
@pd93, @GrahamDennis and @trim21).
- Refactored internal ordered map implementation to use
[github.com/elliotchance/orderedmap](https://github.com/elliotchance/orderedmap)
(#1797 by @pd93).
- Fixed a bug where variables defined at the task level were being ignored in
the `requires` section. (#1960, #1955, #1768 by @vmaerten and @mokeko)
- The `CHECKSUM` and `TIMESTAMP` variables are now accessible within `cmds`
(#1872 by @niklasr22).
- Updated [installation docs](https://taskfile.dev/installation) and added pip
installation method (#935, #1989 by @pd93).
- Fixed a bug where dynamic variables could not access environment variables
(#630, #1869 by @rohm1 and @pd93).
- Disable version check for use as an external library (#1938 by @leaanthony).
## v3.40.1 - 2024-12-06
- Fixed a security issue in `git-urls` by switching to the maintained fork
`chainguard-dev/git-urls` (#1917 by @AlekSi).
- Added missing `platforms` property to `cmds` that use `for` (#1915 by
@dkarter).
- Added misspell linter to check for misspelled English words (#1883 by
@christiandins).
## v3.40.0 - 2024-11-05
- Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in
for loops (#1822, #1823 by @stawii).
- Added a new `TASK_OFFLINE` environment variable to configure the `--offline`
flag and expose it as a special variable in the templating system (#1470,
#1716 by @vmaerten and @pd93).
- Fixed a bug where multiple remote includes caused all prompts to display
without waiting for user input (#1832, #1833 by @vmaerten and @pd93).
- When using the
"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)".
experiment, you can now include Taskfiles from Git repositories (#1652 by
@vmaerten).
- Improved the error message when a dotenv file cannot be parsed (#1842 by
@pbitty).
- Fix issue with directory when using the remote experiment (#1757 by @pbitty).
- Fixed an issue where a special variable was used in combination with a dotenv
file (#1232, #1810 by @vmaerten).
- Refactor the way Task reads Taskfiles to improve readability (#1771 by
@pbitty).
- Added a new option to ensure variable is within the list of values (#1827 by
@vmaerten).
- Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina).
- Added new template function: `numCPU`, which returns the number of logical
CPUs usable (#1890, #1887 by @Amoghrd).
- Fixed a bug where non-nil, empty dynamic variables are returned as an empty
interface (#1903, #1904 by @pd93).
## 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 ## v3.37.2 - 2024-05-12
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93). - Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
@@ -20,7 +167,7 @@
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563, - Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
#1607 by @pd93). #1607 by @pd93).
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by - Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
@pd03). @pd93).
- Fix error when a file or directory in the project contained a special char - Fix error when a file or directory in the project contained a special char
like `&`, `(` or `)` (#1551, #1584 by @andreynering). like `&`, `(` or `)` (#1551, #1584 by @andreynering).
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt) - Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)
@@ -180,8 +327,8 @@
- Added the - Added the
[Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles) [Remote Taskfiles experiment](https://taskfile.dev/experiments/remote-taskfiles)
as a draft (#1152, #1317 by @pd93). as a draft (#1152, #1317 by @pd93).
- Improve performance of content checksuming on `sources:` by replacing md5 with - Improve performance of content checksumming on `sources:` by replacing md5
[XXH3](https://xxhash.com/) which is much faster. This is a soft breaking with [XXH3](https://xxhash.com/) which is much faster. This is a soft breaking
change because checksums will be invalidated when upgrading to this release change because checksums will be invalidated when upgrading to this release
(#1325 by @ReillyBrogan). (#1325 by @ReillyBrogan).
@@ -240,7 +387,7 @@
- Deprecated `version: 2` schema. This will be removed in the next major release - Deprecated `version: 2` schema. This will be removed in the next major release
(#1197, #1198, #1199 by @pd93). (#1197, #1198, #1199 by @pd93).
- Added a new `prompt:` prop to set a warning prompt to be shown before running - Added a new `prompt:` prop to set a warning prompt to be shown before running
a potential dangurous task (#100, #1163 by @MaxCheetham, a potential dangerous task (#100, #1163 by @MaxCheetham,
[Documentation](https://taskfile.dev/usage/#warning-prompts)). [Documentation](https://taskfile.dev/usage/#warning-prompts)).
- Added support for single command task syntax. With this change, it's now - Added support for single command task syntax. With this change, it's now
possible to declare just `cmd:` in a task, avoiding the more complex possible to declare just `cmd:` in a task, avoiding the more complex
@@ -255,7 +402,7 @@
percentage (#1173 by @misitebao). percentage (#1173 by @misitebao).
- Starting on this release, official binaries for FreeBSD will be available to - Starting on this release, official binaries for FreeBSD will be available to
download (#1068 by @andreynering). download (#1068 by @andreynering).
- Fix some errors being unintendedly supressed (#1134 by @clintmod). - Fix some errors being unintendedly suppressed (#1134 by @clintmod).
- Fix a nil pointer error when `version` is omitted from a Taskfile (#1148, - Fix a nil pointer error when `version` is omitted from a Taskfile (#1148,
#1149 by @pd93). #1149 by @pd93).
- Fix duplicate error message when a task does not exists (#1141, #1144 by - Fix duplicate error message when a task does not exists (#1141, #1144 by
@@ -328,8 +475,8 @@ it a go and let us know what you think via a
- Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007 - Fixed a bug where tasks were sometimes incorrectly marked as internal (#1007
by @pd93). by @pd93).
- Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93) - Update to Go 1.20 (bump minimum version to 1.19) (#1010 by @pd93)
- Added environment variable `FORCE_COLOR` support to force color output. - Added environment variable `FORCE_COLOR` support to force color output. Useful
Usefull for environments without TTY (#1003 by @automation-stack) for environments without TTY (#1003 by @automation-stack)
## v3.20.0 - 2023-01-14 ## v3.20.0 - 2023-01-14
@@ -684,7 +831,7 @@ it a go and let us know what you think via a
- Fix error code for the `--help` flag (#300, #330). - Fix error code for the `--help` flag (#300, #330).
- Print version to stdout instead of stderr (#299, #329). - Print version to stdout instead of stderr (#299, #329).
- Supress `context` errors when using the `--watch` flag (#313, #317). - Suppress `context` errors when using the `--watch` flag (#313, #317).
- Support templating on description (#276, #283). - Support templating on description (#276, #283).
## v2.8.0 - 2019-12-07 ## v2.8.0 - 2019-12-07
@@ -693,7 +840,7 @@ it a go and let us know what you think via a
parallel (#266). parallel (#266).
- Fixed bug where calling the `task` CLI only informing global vars would not - Fixed bug where calling the `task` CLI only informing global vars would not
execute the `default` task. execute the `default` task.
- Add hability to silent all tasks by adding `silent: true` a the root of the - Add ability to silent all tasks by adding `silent: true` a the root of the
Taskfile. Taskfile.
## v2.7.1 - 2019-11-10 ## v2.7.1 - 2019-11-10
@@ -835,7 +982,7 @@ document, since it describes in depth what changed for this version.
## v1.4.3 - 2017-09-07 ## v1.4.3 - 2017-09-07
- Allow assigning variables to tasks at run time via CLI (#33) - Allow assigning variables to tasks at run time via CLI (#33)
- Added suport for multiline variables from sh (#64) - Added support for multiline variables from sh (#64)
- Fixes env: remove square braces and evaluate shell (#62) - Fixes env: remove square braces and evaluate shell (#62)
- Watch: change watch library and few fixes and improvements - Watch: change watch library and few fixes and improvements
- When use watching, cancel and restart long running process on file change (#59 - When use watching, cancel and restart long running process on file change (#59
@@ -895,7 +1042,7 @@ document, since it describes in depth what changed for this version.
- More tests and Travis integration - More tests and Travis integration
- Watch a task (experimental) - Watch a task (experimental)
- Possibility to call another task - Possibility to call another task
- Fix "=" not being reconized in variables/environment variables - Fix "=" not being recognized in variables/environment variables
- Tasks can now have a description, and help will print them (#10) - Tasks can now have a description, and help will print them (#10)
- Task dependencies now run concurrently - Task dependencies now run concurrently
- Support for a default task (#16) - Support for a default task (#16)

View File

@@ -10,6 +10,18 @@
</p> </p>
<p> <p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a> <a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p> </p>
<h1>Gold Sponsors</h1>
<table>
<tr>
<td align="center" valign="middle">
<a target="_blank" href="https://devowl.io">
<img src="/website/static/img/devowl.io.svg" height="100px" title="devowl.io" />
</a>
</td>
</tr>
</table>
</div> </div>

View File

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

View File

@@ -9,7 +9,7 @@ import (
// Parse parses command line argument: tasks and global variables // Parse parses command line argument: tasks and global variables
func Parse(args ...string) ([]*ast.Call, *ast.Vars) { func Parse(args ...string) ([]*ast.Call, *ast.Vars) {
calls := []*ast.Call{} calls := []*ast.Call{}
globals := &ast.Vars{} globals := ast.NewVars()
for _, arg := range args { for _, arg := range args {
if !strings.Contains(arg, "=") { if !strings.Contains(arg, "=") {

View File

@@ -7,11 +7,12 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/args" "github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func TestArgs(t *testing.T) { func TestArgs(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
Args []string Args []string
ExpectedCalls []*ast.Call ExpectedCalls []*ast.Call
@@ -32,30 +33,40 @@ func TestArgs(t *testing.T) {
{Task: "task-b"}, {Task: "task-b"},
{Task: "task-c"}, {Task: "task-c"},
}, },
ExpectedGlobals: &ast.Vars{ ExpectedGlobals: ast.NewVars(
OrderedMap: omap.FromMapWithOrder( &ast.VarElement{
map[string]ast.Var{ Key: "FOO",
"FOO": {Value: "bar"}, Value: ast.Var{
"BAR": {Value: "baz"}, Value: "bar",
"BAZ": {Value: "foo"},
}, },
[]string{"FOO", "BAR", "BAZ"}, },
), &ast.VarElement{
}, Key: "BAR",
Value: ast.Var{
Value: "baz",
},
},
&ast.VarElement{
Key: "BAZ",
Value: ast.Var{
Value: "foo",
},
},
),
}, },
{ {
Args: []string{"task-a", "CONTENT=with some spaces"}, Args: []string{"task-a", "CONTENT=with some spaces"},
ExpectedCalls: []*ast.Call{ ExpectedCalls: []*ast.Call{
{Task: "task-a"}, {Task: "task-a"},
}, },
ExpectedGlobals: &ast.Vars{ ExpectedGlobals: ast.NewVars(
OrderedMap: omap.FromMapWithOrder( &ast.VarElement{
map[string]ast.Var{ Key: "CONTENT",
"CONTENT": {Value: "with some spaces"}, Value: ast.Var{
Value: "with some spaces",
}, },
[]string{"CONTENT"}, },
), ),
},
}, },
{ {
Args: []string{"FOO=bar", "task-a", "task-b"}, Args: []string{"FOO=bar", "task-a", "task-b"},
@@ -63,14 +74,14 @@ func TestArgs(t *testing.T) {
{Task: "task-a"}, {Task: "task-a"},
{Task: "task-b"}, {Task: "task-b"},
}, },
ExpectedGlobals: &ast.Vars{ ExpectedGlobals: ast.NewVars(
OrderedMap: omap.FromMapWithOrder( &ast.VarElement{
map[string]ast.Var{ Key: "FOO",
"FOO": {Value: "bar"}, Value: ast.Var{
Value: "bar",
}, },
[]string{"FOO"}, },
), ),
},
}, },
{ {
Args: nil, Args: nil,
@@ -83,25 +94,32 @@ func TestArgs(t *testing.T) {
{ {
Args: []string{"FOO=bar", "BAR=baz"}, Args: []string{"FOO=bar", "BAR=baz"},
ExpectedCalls: []*ast.Call{}, ExpectedCalls: []*ast.Call{},
ExpectedGlobals: &ast.Vars{ ExpectedGlobals: ast.NewVars(
OrderedMap: omap.FromMapWithOrder( &ast.VarElement{
map[string]ast.Var{ Key: "FOO",
"FOO": {Value: "bar"}, Value: ast.Var{
"BAR": {Value: "baz"}, Value: "bar",
}, },
[]string{"FOO", "BAR"}, },
), &ast.VarElement{
}, Key: "BAR",
Value: ast.Var{
Value: "baz",
},
},
),
}, },
} }
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) { t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
t.Parallel()
calls, globals := args.Parse(test.Args...) calls, globals := args.Parse(test.Args...)
assert.Equal(t, test.ExpectedCalls, calls) assert.Equal(t, test.ExpectedCalls, calls)
if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 { if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 {
assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys()) assert.Equal(t, test.ExpectedGlobals, globals)
assert.Equal(t, test.ExpectedGlobals.Values(), globals.Values()) assert.Equal(t, test.ExpectedGlobals, globals)
} }
}) })
} }

View File

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

View File

@@ -159,7 +159,7 @@ func worker(
return workerDone return workerDone
} }
// Do some work and then return, so that the caller can decide wether to continue or not. // Do some work and then return, so that the caller can decide whether to continue or not.
// Return true when all work is done. // Return true when all work is done.
func doSomeWork(deadline time.Time) bool { func doSomeWork(deadline time.Time) bool {
if time.Now().After(deadline) { if time.Now().After(deadline) {

View File

@@ -17,6 +17,7 @@ import (
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version" ver "github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -58,7 +59,7 @@ func run() error {
entrypoint := flags.Entrypoint entrypoint := flags.Entrypoint
if flags.Version { if flags.Version {
fmt.Printf("Task version: %s\n", ver.GetVersion()) fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
return nil return nil
} }
@@ -82,6 +83,15 @@ func run() error {
return nil return nil
} }
if flags.Completion != "" {
script, err := task.Completion(flags.Completion)
if err != nil {
return err
}
fmt.Println(script)
return nil
}
if flags.Global { if flags.Global {
home, err := os.UserHomeDir() home, err := os.UserHomeDir()
if err != nil { if err != nil {
@@ -122,19 +132,19 @@ func run() error {
Stdout: os.Stdout, Stdout: os.Stdout,
Stderr: os.Stderr, Stderr: os.Stderr,
OutputStyle: flags.Output, OutputStyle: flags.Output,
TaskSorter: taskSorter, TaskSorter: taskSorter,
EnableVersionCheck: true,
} }
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus) listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
if err := listOptions.Validate(); err != nil { if err := listOptions.Validate(); err != nil {
return err return err
} }
if err := e.Setup(); err != nil { err := e.Setup()
if err != nil {
return err return err
} }
if experiments.AnyVariables.Enabled { if experiments.AnyVariables.Enabled {
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n") logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
} }
@@ -145,6 +155,14 @@ func run() error {
return nil return nil
} }
if flags.ClearCache {
cache, err := taskfile.NewCache(e.TempDir.Remote)
if err != nil {
return err
}
return cache.Clear()
}
if (listOptions.ShouldListTasks()) && flags.Silent { if (listOptions.ShouldListTasks()) && flags.Silent {
return e.ListTaskNames(flags.ListAll) return e.ListTaskNames(flags.ListAll)
} }
@@ -179,6 +197,9 @@ func run() error {
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs}) globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll}) globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
e.Taskfile.Vars.Merge(globals, nil) e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch { if !flags.Watch {

34
completion.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -12,14 +12,14 @@ const (
const ( const (
CodeTaskfileNotFound int = iota + 100 CodeTaskfileNotFound int = iota + 100
CodeTaskfileAlreadyExists CodeTaskfileAlreadyExists
CodeTaskfileInvalid CodeTaskfileDecode
CodeTaskfileFetchFailed CodeTaskfileFetchFailed
CodeTaskfileNotTrusted CodeTaskfileNotTrusted
CodeTaskfileNotSecure CodeTaskfileNotSecure
CodeTaskfileCacheNotFound CodeTaskfileCacheNotFound
CodeTaskfileVersionCheckError CodeTaskfileVersionCheckError
CodeTaskfileNetworkTimeout CodeTaskfileNetworkTimeout
_ // CodeTaskfileDuplicateInclude CodeTaskfileInvalid
CodeTaskfileCycle CodeTaskfileCycle
) )
@@ -32,6 +32,7 @@ const (
CodeTaskCalledTooManyTimes CodeTaskCalledTooManyTimes
CodeTaskCancelled CodeTaskCancelled
CodeTaskMissingRequiredVars CodeTaskMissingRequiredVars
CodeTaskNotAllowedVars
) )
// TaskError extends the standard error interface with a Code method. This code will // TaskError extends the standard error interface with a Code method. This code will
@@ -58,3 +59,8 @@ func Is(err, target error) bool {
func As(err error, target any) bool { func As(err error, target any) bool {
return errors.As(err, target) return errors.As(err, target)
} }
// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package.
func Unwrap(err error) error {
return errors.Unwrap(err)
}

View File

@@ -80,6 +80,19 @@ func (err *TaskNameConflictError) Code() int {
return CodeTaskNameConflict return CodeTaskNameConflict
} }
type TaskNameFlattenConflictError struct {
TaskName string
Include string
}
func (err *TaskNameFlattenConflictError) Error() string {
return fmt.Sprintf(`task: Found multiple tasks (%s) included by "%s""`, err.TaskName, err.Include)
}
func (err *TaskNameFlattenConflictError) Code() int {
return CodeTaskNameConflict
}
// TaskCalledTooManyTimesError is returned when the maximum task call limit is // TaskCalledTooManyTimesError is returned when the maximum task call limit is
// exceeded. This is to prevent infinite loops and cyclic dependencies. // exceeded. This is to prevent infinite loops and cyclic dependencies.
type TaskCalledTooManyTimesError struct { type TaskCalledTooManyTimesError struct {
@@ -145,3 +158,29 @@ func (err *TaskMissingRequiredVars) Error() string {
func (err *TaskMissingRequiredVars) Code() int { func (err *TaskMissingRequiredVars) Code() int {
return CodeTaskMissingRequiredVars return CodeTaskMissingRequiredVars
} }
type NotAllowedVar struct {
Value string
Enum []string
Name string
}
type TaskNotAllowedVars struct {
TaskName string
NotAllowedVars []NotAllowedVar
}
func (err *TaskNotAllowedVars) Error() string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
for _, s := range err.NotAllowedVars {
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
}
return builder.String()
}
func (err *TaskNotAllowedVars) Code() int {
return CodeTaskNotAllowedVars
}

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>

50
go.mod
View File

@@ -1,35 +1,61 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.21.0 go 1.22.0
require ( require (
github.com/Masterminds/semver/v3 v3.2.1 github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.14.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0 github.com/dominikbraun/graph v0.23.0
github.com/fatih/color v1.16.0 github.com/elliotchance/orderedmap/v2 v2.7.0
github.com/fatih/color v1.18.0
github.com/go-git/go-billy/v5 v5.6.1
github.com/go-git/go-git/v5 v5.13.1
github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 github.com/go-task/template v0.1.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-zglob v0.0.4 github.com/mattn/go-zglob v0.0.6
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.0 github.com/otiai10/copy v1.14.0
github.com/radovskyb/watcher v1.0.7 github.com/radovskyb/watcher v1.0.7
github.com/sajari/fuzzy v1.0.0 github.com/sajari/fuzzy v1.0.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.10.0
github.com/zeebo/xxh3 v1.0.2 github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.7.0 golang.org/x/sync v0.10.0
golang.org/x/term v0.19.0 golang.org/x/term v0.27.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.8.0 mvdan.cc/sh/v3 v3.10.0
) )
require ( require (
github.com/klauspost/cpuid/v2 v2.0.9 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/sys v0.19.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
) )

158
go.sum
View File

@@ -1,27 +1,82 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/elliotchance/orderedmap/v2 v2.7.0 h1:WHuf0DRo63uLnldCPp9ojm3gskYwEdIIfAUVG5KhoOc=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/elliotchance/orderedmap/v2 v2.7.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 h1:JBbiZ2CXIZ9Upe3O2yI5+3ksWoa7hNVNi4BINs8TIrs= github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -29,44 +84,91 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8= mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY= mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=

View File

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

35
help.go
View File

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

View File

@@ -3,14 +3,13 @@ package compiler
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"gopkg.in/yaml.v3" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -47,14 +46,12 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, err
func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) { func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) {
result := GetEnviron() result := GetEnviron()
if t != nil { specialVars, err := c.getSpecialVars(t, call)
specialVars, err := c.getSpecialVars(t) if err != nil {
if err != nil { return nil, err
return nil, err }
} for k, v := range specialVars {
for k, v := range specialVars { result.Set(k, ast.Var{Value: v})
result.Set(k, ast.Var{Value: v})
}
} }
getRangeFunc := func(dir string) func(k string, v ast.Var) error { getRangeFunc := func(dir string) func(k string, v ast.Var) error {
@@ -77,25 +74,13 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
if err := cache.Err(); err != nil { if err := cache.Err(); err != nil {
return err return err
} }
// Evaluate JSON // If the variable is already set, we can set it and return
if newVar.Json != "" { if newVar.Value != nil {
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}) result.Set(k, ast.Var{Value: newVar.Value})
return nil return nil
} }
// If the variable is dynamic, we need to resolve it first // If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir) static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
if err != nil { if err != nil {
return err return err
} }
@@ -147,14 +132,19 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
return result, nil return result, nil
} }
func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) { func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string, error) {
c.muDynamicCache.Lock() c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock() defer c.muDynamicCache.Unlock()
// If the variable is not dynamic or it is empty, return an empty string
if v.Sh == nil || *v.Sh == "" {
return "", nil
}
if c.dynamicCache == nil { if c.dynamicCache == nil {
c.dynamicCache = make(map[string]string, 30) c.dynamicCache = make(map[string]string, 30)
} }
if result, ok := c.dynamicCache[v.Sh]; ok { if result, ok := c.dynamicCache[*v.Sh]; ok {
return result, nil return result, nil
} }
@@ -165,10 +155,11 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
var stdout bytes.Buffer var stdout bytes.Buffer
opts := &execext.RunCommandOptions{ opts := &execext.RunCommandOptions{
Command: v.Sh, Command: *v.Sh,
Dir: dir, Dir: dir,
Stdout: &stdout, Stdout: &stdout,
Stderr: c.Logger.Stderr, Stderr: c.Logger.Stderr,
Env: e,
} }
if err := execext.RunCommand(context.Background(), opts); err != nil { if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err) return "", fmt.Errorf(`task: Command "%s" failed: %s`, opts.Command, err)
@@ -179,13 +170,13 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
result := strings.TrimSuffix(stdout.String(), "\r\n") result := strings.TrimSuffix(stdout.String(), "\r\n")
result = strings.TrimSuffix(result, "\n") result = strings.TrimSuffix(result, "\n")
c.dynamicCache[v.Sh] = result c.dynamicCache[*v.Sh] = result
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", v.Sh, result) c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", *v.Sh, result)
return result, nil return result, nil
} }
// ResetCache clear the dymanic variables cache // ResetCache clear the dynamic variables cache
func (c *Compiler) ResetCache() { func (c *Compiler) ResetCache() {
c.muDynamicCache.Lock() c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock() defer c.muDynamicCache.Unlock()
@@ -193,14 +184,23 @@ func (c *Compiler) ResetCache() {
c.dynamicCache = nil c.dynamicCache = nil
} }
func (c *Compiler) getSpecialVars(t *ast.Task) (map[string]string, error) { func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
return map[string]string{ allVars := map[string]string{
"TASK": t.Task, "TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint), "ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": c.Dir, "ROOT_DIR": c.Dir,
"TASKFILE": t.Location.Taskfile,
"TASKFILE_DIR": filepath.Dir(t.Location.Taskfile),
"USER_WORKING_DIR": c.UserWorkingDir, "USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(), "TASK_VERSION": version.GetVersion(),
}, nil }
if t != nil {
allVars["TASK"] = t.Task
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
}
if call != nil {
allVars["ALIAS"] = call.Task
}
return allVars, nil
} }

View File

@@ -10,7 +10,7 @@ import (
// GetEnviron the all return all environment variables encapsulated on a // GetEnviron the all return all environment variables encapsulated on a
// ast.Vars // ast.Vars
func GetEnviron() *ast.Vars { func GetEnviron() *ast.Vars {
m := &ast.Vars{} m := ast.NewVars()
for _, e := range os.Environ() { for _, e := range os.Environ() {
keyVal := strings.SplitN(e, "=", 2) keyVal := strings.SplitN(e, "=", 2)
key, val := keyVal[0], keyVal[1] key, val := keyVal[0], keyVal[1]

View File

@@ -2,6 +2,8 @@ package deepcopy
import ( import (
"reflect" "reflect"
"github.com/elliotchance/orderedmap/v2"
) )
type Copier[T any] interface { type Copier[T any] interface {
@@ -38,6 +40,21 @@ func Map[K comparable, V any](orig map[K]V) map[K]V {
return c return c
} }
func OrderedMap[K comparable, V any](orig *orderedmap.OrderedMap[K, V]) *orderedmap.OrderedMap[K, V] {
if orig.Len() == 0 {
return orderedmap.NewOrderedMap[K, V]()
}
c := orderedmap.NewOrderedMap[K, V]()
for pair := orig.Front(); pair != nil; pair = pair.Next() {
if copyable, ok := any(pair.Value).(Copier[V]); ok {
c.Set(pair.Key, copyable.DeepCopy())
} else {
c.Set(pair.Key, pair.Value)
}
}
return c
}
// TraverseStringsFunc runs the given function on every string in the given // TraverseStringsFunc runs the given function on every string in the given
// value by traversing it recursively. If the given value is a string, the // value by traversing it recursively. If the given value is a string, the
// function will run on a copy of the string and return it. If the value is a // function will run on a copy of the string and return it. If the value is a
@@ -85,7 +102,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
case reflect.Struct: case reflect.Struct:
// Loop over each field and call traverseFunc recursively // Loop over each field and call traverseFunc recursively
for i := 0; i < v.NumField(); i += 1 { for i := range v.NumField() {
if err := traverseFunc(copy.Field(i), v.Field(i)); err != nil { if err := traverseFunc(copy.Field(i), v.Field(i)); err != nil {
return err return err
} }
@@ -95,7 +112,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
// Create an empty copy from the original value's type // Create an empty copy from the original value's type
copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap())) copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap()))
// Loop over each element and call traverseFunc recursively // Loop over each element and call traverseFunc recursively
for i := 0; i < v.Len(); i += 1 { for i := range v.Len() {
if err := traverseFunc(copy.Index(i), v.Index(i)); err != nil { if err := traverseFunc(copy.Index(i), v.Index(i)); err != nil {
return err return err
} }

15
internal/env/env.go vendored
View File

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

View File

@@ -2,7 +2,6 @@ package execext
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -14,6 +13,8 @@ import (
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
"github.com/go-task/task/v3/errors"
) )
// RunCommandOptions is the options for the RunCommand func // RunCommandOptions is the options for the RunCommand func
@@ -90,14 +91,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
return r.Run(ctx, p) return r.Run(ctx, p)
} }
// IsExitError returns true the given error is an exis status error
func IsExitError(err error) bool {
if _, ok := interp.IsExitStatus(err); ok {
return true
}
return false
}
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field // Expand is a helper to mvdan.cc/shell.Fields that returns the first field
// if available. // if available.
func Expand(s string) (string, error) { func Expand(s string) (string, error) {

View File

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

View File

@@ -7,6 +7,8 @@ import (
) )
func TestNormalizeFilename(t *testing.T) { func TestNormalizeFilename(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
In, Out string In, Out string
}{ }{

View File

@@ -26,6 +26,8 @@ import (
// | false | true | false | // | false | true | false |
// | false | false | false | // | false | false | false |
func TestIsTaskUpToDate(t *testing.T) { func TestIsTaskUpToDate(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
name string name string
task *ast.Task task *ast.Task
@@ -150,6 +152,8 @@ func TestIsTaskUpToDate(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mockStatusChecker := mocks.NewStatusCheckable(t) mockStatusChecker := mocks.NewStatusCheckable(t)
if tt.setupMockStatusChecker != nil { if tt.setupMockStatusChecker != nil {
tt.setupMockStatusChecker(mockStatusChecker) tt.setupMockStatusChecker(mockStatusChecker)

View File

@@ -1,13 +1,15 @@
package flags package flags
import ( import (
"errors" "cmp"
"log" "log"
"os" "os"
"strconv"
"time" "time"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
@@ -38,6 +40,7 @@ var (
Version bool Version bool
Help bool Help bool
Init bool Init bool
Completion string
List bool List bool
ListAll bool ListAll bool
ListJson bool ListJson bool
@@ -65,6 +68,7 @@ var (
Experiments bool Experiments bool
Download bool Download bool
Offline bool Offline bool
ClearCache bool
Timeout time.Duration Timeout time.Duration
) )
@@ -75,10 +79,14 @@ func init() {
log.Print(usage) log.Print(usage)
pflag.PrintDefaults() pflag.PrintDefaults()
} }
offline, err := strconv.ParseBool(cmp.Or(os.Getenv("TASK_OFFLINE"), "false"))
if err != nil {
offline = false
}
pflag.BoolVar(&Version, "version", false, "Show Task version.") pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.")
pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.") pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.")
pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.") pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.")
pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.") pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.")
@@ -101,7 +109,7 @@ func init() {
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.") pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.") pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -117,8 +125,9 @@ func init() {
// Remote Taskfiles experiment will adds the "download" and "offline" flags // Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled { if experiments.RemoteTaskfiles.Enabled {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", false, "Forces Task to only use local or cached Taskfiles.") pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.") pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
} }
pflag.Parse() pflag.Parse()
@@ -129,6 +138,10 @@ func Validate() error {
return errors.New("task: You can't set both --download and --offline flags") return errors.New("task: You can't set both --download and --offline flags")
} }
if Download && ClearCache {
return errors.New("task: You can't set both --download and --clear-cache flags")
}
if Global && Dir != "" { if Global && Dir != "" {
log.Fatal("task: You can't set both --global and --dir") log.Fatal("task: You can't set both --global and --dir")
return nil return nil

View File

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

View File

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

View File

@@ -1,164 +0,0 @@
package omap
import (
"cmp"
"fmt"
"slices"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/exp"
)
// An OrderedMap is a wrapper around a regular map that maintains an ordered
// list of the map's keys. This allows you to run deterministic and ordered
// operations on the map such as printing/serializing/iterating.
type OrderedMap[K cmp.Ordered, V any] struct {
s []K
m map[K]V
}
// New will create a new OrderedMap of the given type and return it.
func New[K cmp.Ordered, V any]() OrderedMap[K, V] {
return OrderedMap[K, V]{
s: make([]K, 0),
m: make(map[K]V),
}
}
// FromMap will create a new OrderedMap from the given map. Since Golang maps
// are unordered, the order of the created OrderedMap will be random.
func FromMap[K cmp.Ordered, V any](m map[K]V) OrderedMap[K, V] {
om := New[K, V]()
om.m = m
om.s = exp.Keys(m)
return om
}
func FromMapWithOrder[K cmp.Ordered, V any](m map[K]V, order []K) OrderedMap[K, V] {
om := New[K, V]()
if len(m) != len(order) {
panic("length of map and order must be equal")
}
om.m = m
om.s = order
for key := range om.m {
if !slices.Contains(om.s, key) {
panic("order keys must match map keys")
}
}
return om
}
// Len will return the number of items in the map.
func (om *OrderedMap[K, V]) Len() int {
return len(om.s)
}
// Set will set the value for a given key.
func (om *OrderedMap[K, V]) Set(key K, value V) {
if om.m == nil {
om.m = make(map[K]V)
}
if _, ok := om.m[key]; !ok {
om.s = append(om.s, key)
}
om.m[key] = value
}
// Get will return the value for a given key.
// If the key does not exist, it will return the zero value of the value type.
func (om *OrderedMap[K, V]) Get(key K) V {
value, ok := om.m[key]
if !ok {
var zero V
return zero
}
return value
}
// Exists will return whether or not the given key exists.
func (om *OrderedMap[K, V]) Exists(key K) bool {
_, ok := om.m[key]
return ok
}
// Sort will sort the map.
func (om *OrderedMap[K, V]) Sort() {
slices.Sort(om.s)
}
// SortFunc will sort the map using the given function.
func (om *OrderedMap[K, V]) SortFunc(less func(i, j K) int) {
slices.SortFunc(om.s, less)
}
// Keys will return a slice of the map's keys in order.
func (om *OrderedMap[K, V]) Keys() []K {
return om.s
}
// Values will return a slice of the map's values in order.
func (om *OrderedMap[K, V]) Values() []V {
var values []V
for _, key := range om.s {
values = append(values, om.m[key])
}
return values
}
// Range will iterate over the map and call the given function for each key/value.
func (om *OrderedMap[K, V]) Range(fn func(key K, value V) error) error {
for _, key := range om.s {
if err := fn(key, om.m[key]); err != nil {
return err
}
}
return nil
}
// Merge merges the given Vars into the caller one
func (om *OrderedMap[K, V]) Merge(other OrderedMap[K, V]) {
// nolint: errcheck
other.Range(func(key K, value V) error {
om.Set(key, value)
return nil
})
}
func (om *OrderedMap[K, V]) DeepCopy() OrderedMap[K, V] {
return OrderedMap[K, V]{
s: deepcopy.Slice(om.s),
m: deepcopy.Map(om.m),
}
}
func (om *OrderedMap[K, V]) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
// Even numbers contain the keys
// Odd numbers contain the values
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
// Decode the key
keyNode := node.Content[i]
var k K
if err := keyNode.Decode(&k); err != nil {
return err
}
// Decode the value
valueNode := node.Content[i+1]
var v V
if err := valueNode.Decode(&v); err != nil {
return err
}
// Set the key and value
om.Set(k, v)
}
return nil
}
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag())
}

View File

@@ -1,121 +0,0 @@
package omap
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestFromMap(t *testing.T) {
m := map[int]string{3: "three", 1: "one", 2: "two"}
om := FromMap(m)
assert.Len(t, om.m, 3)
assert.Len(t, om.s, 3)
assert.ElementsMatch(t, []int{1, 2, 3}, om.s)
for key, value := range m {
assert.Equal(t, om.Get(key), value)
}
}
func TestSetGetExists(t *testing.T) {
om := New[int, string]()
assert.False(t, om.Exists(1))
assert.Equal(t, "", om.Get(1))
om.Set(1, "one")
assert.True(t, om.Exists(1))
assert.Equal(t, "one", om.Get(1))
}
func TestSort(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
om.Sort()
assert.Equal(t, []int{1, 2, 3}, om.s)
}
func TestSortFunc(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
om.SortFunc(func(a, b int) int {
return b - a
})
assert.Equal(t, []int{3, 2, 1}, om.s)
}
func TestKeysValues(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
assert.Equal(t, []int{3, 1, 2}, om.Keys())
assert.Equal(t, []string{"three", "one", "two"}, om.Values())
}
func Range(t *testing.T) {
om := New[int, string]()
om.Set(3, "three")
om.Set(1, "one")
om.Set(2, "two")
expectedKeys := []int{3, 1, 2}
expectedValues := []string{"three", "one", "two"}
keys := make([]int, 0, len(expectedKeys))
values := make([]string, 0, len(expectedValues))
err := om.Range(func(key int, value string) error {
keys = append(keys, key)
values = append(values, value)
return nil
})
assert.NoError(t, err)
assert.ElementsMatch(t, expectedKeys, keys)
assert.ElementsMatch(t, expectedValues, values)
}
func TestOrderedMapMerge(t *testing.T) {
om1 := New[string, int]()
om1.Set("a", 1)
om1.Set("b", 2)
om2 := New[string, int]()
om2.Set("b", 3)
om2.Set("c", 4)
om1.Merge(om2)
expectedKeys := []string{"a", "b", "c"}
expectedValues := []int{1, 3, 4}
assert.Equal(t, len(expectedKeys), len(om1.s))
assert.Equal(t, len(expectedKeys), len(om1.m))
for i, key := range expectedKeys {
assert.True(t, om1.Exists(key))
assert.Equal(t, expectedValues[i], om1.Get(key))
}
}
func TestUnmarshalYAML(t *testing.T) {
yamlString := `
3: three
1: one
2: two
`
var om OrderedMap[int, string]
err := yaml.Unmarshal([]byte(yamlString), &om)
require.NoError(t, err)
expectedKeys := []int{3, 1, 2}
expectedValues := []string{"three", "one", "two"}
assert.Equal(t, expectedKeys, om.Keys())
assert.Equal(t, expectedValues, om.Values())
}

View File

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

View File

@@ -7,16 +7,19 @@ import (
"io" "io"
"testing" "testing"
"github.com/fatih/color"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func TestInterleaved(t *testing.T) { func TestInterleaved(t *testing.T) {
t.Parallel()
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Interleaved{} var o output.Output = output.Interleaved{}
w, _, _ := o.WrapWriter(&b, io.Discard, "", nil) w, _, _ := o.WrapWriter(&b, io.Discard, "", nil)
@@ -28,6 +31,8 @@ func TestInterleaved(t *testing.T) {
} }
func TestGroup(t *testing.T) { func TestGroup(t *testing.T) {
t.Parallel()
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Group{} var o output.Output = output.Group{}
stdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, "", nil) stdOut, stdErr, cleanup := o.WrapWriter(&b, io.Discard, "", nil)
@@ -46,12 +51,15 @@ func TestGroup(t *testing.T) {
} }
func TestGroupWithBeginEnd(t *testing.T) { func TestGroupWithBeginEnd(t *testing.T) {
t.Parallel()
tmpl := templater.Cache{ tmpl := templater.Cache{
Vars: &ast.Vars{ Vars: ast.NewVars(
OrderedMap: omap.FromMap(map[string]ast.Var{ &ast.VarElement{
"VAR1": {Value: "example-value"}, Key: "VAR1",
}), Value: ast.Var{Value: "example-value"},
}, },
),
} }
var o output.Output = output.Group{ var o output.Output = output.Group{
@@ -59,6 +67,8 @@ func TestGroupWithBeginEnd(t *testing.T) {
End: "::endgroup::", End: "::endgroup::",
} }
t.Run("simple", func(t *testing.T) { t.Run("simple", func(t *testing.T) {
t.Parallel()
var b bytes.Buffer var b bytes.Buffer
w, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl) w, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl)
@@ -70,6 +80,8 @@ func TestGroupWithBeginEnd(t *testing.T) {
assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String()) assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
}) })
t.Run("no output", func(t *testing.T) { t.Run("no output", func(t *testing.T) {
t.Parallel()
var b bytes.Buffer var b bytes.Buffer
_, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl) _, _, cleanup := o.WrapWriter(&b, io.Discard, "", &tmpl)
require.NoError(t, cleanup(nil)) require.NoError(t, cleanup(nil))
@@ -78,6 +90,8 @@ func TestGroupWithBeginEnd(t *testing.T) {
} }
func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) { func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) {
t.Parallel()
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Group{ var o output.Output = output.Group{
ErrorOnly: true, ErrorOnly: true,
@@ -92,6 +106,8 @@ func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) {
} }
func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) { func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
t.Parallel()
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Group{ var o output.Output = output.Group{
ErrorOnly: true, ErrorOnly: true,
@@ -105,12 +121,16 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
assert.Equal(t, "std-out\nstd-err\n", b.String()) assert.Equal(t, "std-out\nstd-err\n", b.String())
} }
func TestPrefixed(t *testing.T) { func TestPrefixed(t *testing.T) { //nolint:paralleltest // cannot run in parallel
var b bytes.Buffer var b bytes.Buffer
var o output.Output = output.Prefixed{} l := &logger.Logger{
Color: false,
}
var o output.Output = output.NewPrefixed(l)
w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil) w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil)
t.Run("simple use cases", func(t *testing.T) { t.Run("simple use cases", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel
b.Reset() b.Reset()
fmt.Fprintln(w, "foo\nbar") fmt.Fprintln(w, "foo\nbar")
@@ -120,7 +140,7 @@ func TestPrefixed(t *testing.T) {
require.NoError(t, cleanup(nil)) require.NoError(t, cleanup(nil))
}) })
t.Run("multiple writes for a single line", func(t *testing.T) { t.Run("multiple writes for a single line", func(t *testing.T) { //nolint:paralleltest // cannot run in parallel
b.Reset() b.Reset()
for _, char := range []string{"T", "e", "s", "t", "!"} { for _, char := range []string{"T", "e", "s", "t", "!"} {
@@ -132,3 +152,41 @@ func TestPrefixed(t *testing.T) {
assert.Equal(t, "[prefix] Test!\n", b.String()) assert.Equal(t, "[prefix] Test!\n", b.String())
}) })
} }
func TestPrefixedWithColor(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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

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

View File

@@ -9,6 +9,8 @@ import (
) )
func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) { func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
t.Parallel()
task1 := &ast.Task{Task: "task1"} task1 := &ast.Task{Task: "task1"}
task2 := &ast.Task{Task: "task2"} task2 := &ast.Task{Task: "task2"}
task3 := &ast.Task{Task: "ns1:task3"} task3 := &ast.Task{Task: "ns1:task3"}
@@ -40,6 +42,8 @@ func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &AlphaNumericWithRootTasksFirst{} s := &AlphaNumericWithRootTasksFirst{}
s.Sort(tt.tasks) s.Sort(tt.tasks)
assert.Equal(t, tt.want, tt.tasks) assert.Equal(t, tt.want, tt.tasks)
@@ -48,6 +52,8 @@ func TestAlphaNumericWithRootTasksFirst_Sort(t *testing.T) {
} }
func TestAlphaNumeric_Sort(t *testing.T) { func TestAlphaNumeric_Sort(t *testing.T) {
t.Parallel()
task1 := &ast.Task{Task: "task1"} task1 := &ast.Task{Task: "task1"}
task2 := &ast.Task{Task: "task2"} task2 := &ast.Task{Task: "task2"}
task3 := &ast.Task{Task: "ns1:task3"} task3 := &ast.Task{Task: "ns1:task3"}
@@ -69,6 +75,8 @@ func TestAlphaNumeric_Sort(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &AlphaNumeric{} s := &AlphaNumeric{}
s.Sort(tt.tasks) s.Sort(tt.tasks)
assert.Equal(t, tt.tasks, tt.want) assert.Equal(t, tt.tasks, tt.want)

View File

@@ -10,7 +10,9 @@ import (
func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) { func PrintTasks(l *logger.Logger, t *ast.Taskfile, c []*ast.Call) {
for i, call := range c { for i, call := range c {
PrintSpaceBetweenSummaries(l, i) PrintSpaceBetweenSummaries(l, i)
PrintTask(l, t.Tasks.Get(call.Task)) if task, ok := t.Tasks.Get(call.Task); ok {
PrintTask(l, task)
}
} }
} }

View File

@@ -13,6 +13,8 @@ import (
) )
func TestPrintsDependenciesIfPresent(t *testing.T) { func TestPrintsDependenciesIfPresent(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
task := &ast.Task{ task := &ast.Task{
Deps: []*ast.Dep{ Deps: []*ast.Dep{
@@ -38,6 +40,8 @@ func createDummyLogger() (*bytes.Buffer, logger.Logger) {
} }
func TestDoesNotPrintDependenciesIfMissing(t *testing.T) { func TestDoesNotPrintDependenciesIfMissing(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
task := &ast.Task{ task := &ast.Task{
Deps: []*ast.Dep{}, Deps: []*ast.Dep{},
@@ -49,6 +53,8 @@ func TestDoesNotPrintDependenciesIfMissing(t *testing.T) {
} }
func TestPrintTaskName(t *testing.T) { func TestPrintTaskName(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
task := &ast.Task{ task := &ast.Task{
Task: "my-task-name", Task: "my-task-name",
@@ -60,6 +66,8 @@ func TestPrintTaskName(t *testing.T) {
} }
func TestPrintTaskCommandsIfPresent(t *testing.T) { func TestPrintTaskCommandsIfPresent(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
task := &ast.Task{ task := &ast.Task{
Cmds: []*ast.Cmd{ Cmds: []*ast.Cmd{
@@ -78,6 +86,8 @@ func TestPrintTaskCommandsIfPresent(t *testing.T) {
} }
func TestDoesNotPrintCommandIfMissing(t *testing.T) { func TestDoesNotPrintCommandIfMissing(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
task := &ast.Task{ task := &ast.Task{
Cmds: []*ast.Cmd{}, Cmds: []*ast.Cmd{},
@@ -89,6 +99,8 @@ func TestDoesNotPrintCommandIfMissing(t *testing.T) {
} }
func TestLayout(t *testing.T) { func TestLayout(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
task := &ast.Task{ task := &ast.Task{
Task: "sample-task", Task: "sample-task",
@@ -123,6 +135,8 @@ commands:
} }
func TestPrintDescriptionAsFallback(t *testing.T) { func TestPrintDescriptionAsFallback(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
taskWithoutSummary := &ast.Task{ taskWithoutSummary := &ast.Task{
Desc: "description", Desc: "description",
@@ -150,13 +164,15 @@ func TestPrintDescriptionAsFallback(t *testing.T) {
} }
func TestPrintAllWithSpaces(t *testing.T) { func TestPrintAllWithSpaces(t *testing.T) {
t.Parallel()
buffer, l := createDummyLogger() buffer, l := createDummyLogger()
t1 := &ast.Task{Task: "t1"} t1 := &ast.Task{Task: "t1"}
t2 := &ast.Task{Task: "t2"} t2 := &ast.Task{Task: "t2"}
t3 := &ast.Task{Task: "t3"} t3 := &ast.Task{Task: "t3"}
tasks := ast.Tasks{} tasks := ast.NewTasks()
tasks.Set("t1", t1) tasks.Set("t1", t1)
tasks.Set("t2", t2) tasks.Set("t2", t2)
tasks.Set("t3", t3) tasks.Set("t3", t3)

View File

@@ -17,8 +17,9 @@ var templateFuncs template.FuncMap
func init() { func init() {
taskFuncs := template.FuncMap{ taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS }, "OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH }, "ARCH": func() string { return runtime.GOARCH },
"numCPU": func() int { return runtime.NumCPU() },
"catLines": func(s string) string { "catLines": func(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ") s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ") return strings.ReplaceAll(s, "\n", " ")

View File

@@ -2,6 +2,7 @@ package templater
import ( import (
"bytes" "bytes"
"fmt"
"maps" "maps"
"strings" "strings"
@@ -40,7 +41,15 @@ func ResolveRef(ref string, cache *Cache) any {
cache.cacheMap = cache.Vars.ToCacheMap() cache.cacheMap = cache.Vars.ToCacheMap()
} }
val, err := template.ResolveRef(ref, cache.cacheMap) if ref == "." {
return cache.cacheMap
}
t, err := template.New("resolver").Funcs(templateFuncs).Parse(fmt.Sprintf("{{%s}}", ref))
if err != nil {
cache.err = err
return nil
}
val, err := t.Resolve(cache.cacheMap)
if err != nil { if err != nil {
cache.err = err cache.err = err
return nil return nil
@@ -119,8 +128,6 @@ func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var
Live: v.Live, Live: v.Live,
Ref: v.Ref, Ref: v.Ref,
Dir: v.Dir, Dir: v.Dir,
Json: ReplaceWithExtra(v.Json, cache, extra),
Yaml: ReplaceWithExtra(v.Yaml, cache, extra),
} }
} }
@@ -133,11 +140,11 @@ func ReplaceVarsWithExtra(vars *ast.Vars, cache *Cache, extra map[string]any) *a
return nil return nil
} }
var newVars ast.Vars newVars := ast.NewVars()
_ = vars.Range(func(k string, v ast.Var) error { _ = vars.Range(func(k string, v ast.Var) error {
newVars.Set(k, ReplaceVarWithExtra(v, cache, extra)) newVars.Set(k, ReplaceVarWithExtra(v, cache, extra))
return nil return nil
}) })
return &newVars return newVars
} }

View File

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

2
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@go-task/cli", "name": "@go-task/cli",
"version": "3.37.2", "version": "3.41.0",
"description": "A task runner / simpler Make alternative written in Go", "description": "A task runner / simpler Make alternative written in Go",
"scripts": { "scripts": {
"postinstall": "go-npm install", "postinstall": "go-npm install",
@@ -22,7 +22,7 @@
"build-tool", "build-tool",
"task-runner" "task-runner"
], ],
"author": "Andrey Nering", "author": "The Task authors",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/go-task/task/issues" "url": "https://github.com/go-task/task/issues"

View File

@@ -2,8 +2,8 @@ package task
import ( import (
"context" "context"
"errors"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"

View File

@@ -1,26 +1,32 @@
package task package task
import ( import (
"context" "slices"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call *ast.Call) error { func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 { if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil return nil
} }
vars, err := e.Compiler.GetVariables(t, call)
if err != nil {
return err
}
var missingVars []string var missingVars []string
var notAllowedValuesVars []errors.NotAllowedVar
for _, requiredVar := range t.Requires.Vars { for _, requiredVar := range t.Requires.Vars {
if !vars.Exists(requiredVar) { value, ok := t.Vars.Get(requiredVar.Name)
missingVars = append(missingVars, requiredVar) if !ok {
missingVars = append(missingVars, requiredVar.Name)
} else {
value, isString := value.Value.(string)
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Name: requiredVar.Name,
})
}
} }
} }
@@ -31,5 +37,12 @@ func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call
} }
} }
if len(notAllowedValuesVars) > 0 {
return &errors.TaskNotAllowedVars{
TaskName: t.Name(),
NotAllowedVars: notAllowedValuesVars,
}
}
return nil return nil
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"sync" "sync"
@@ -69,7 +70,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
e.Download, e.Download,
e.Offline, e.Offline,
e.Timeout, e.Timeout,
e.TempDir, e.TempDir.Remote,
e.Logger, e.Logger,
) )
graph, err := reader.Read() graph, err := reader.Read()
@@ -95,7 +96,7 @@ func (e *Executor) setupFuzzyModel() {
words = append(words, taskName) words = append(words, taskName)
for _, task := range e.Taskfile.Tasks.Values() { for _, task := range e.Taskfile.Tasks.Values() {
words = append(words, task.Aliases...) words = slices.Concat(words, task.Aliases)
} }
} }
@@ -104,12 +105,15 @@ func (e *Executor) setupFuzzyModel() {
} }
func (e *Executor) setupTempDir() error { func (e *Executor) setupTempDir() error {
if e.TempDir != "" { if e.TempDir != (TempDir{}) {
return nil return nil
} }
if os.Getenv("TASK_TEMP_DIR") == "" { if os.Getenv("TASK_TEMP_DIR") == "" {
e.TempDir = filepathext.SmartJoin(e.Dir, ".task") e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") { } else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR")) tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
if err != nil { if err != nil {
@@ -117,9 +121,28 @@ func (e *Executor) setupTempDir() error {
} }
projectDir, _ := filepath.Abs(e.Dir) projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir) projectName := filepath.Base(projectDir)
e.TempDir = filepathext.SmartJoin(tempDir, projectName) e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else { } else {
e.TempDir = filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")) e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
}
}
if os.Getenv("TASK_REMOTE_DIR") != "" {
if filepath.IsAbs(os.Getenv("TASK_REMOTE_DIR")) || strings.HasPrefix(os.Getenv("TASK_REMOTE_DIR"), "~") {
remoteTempDir, err := execext.Expand(os.Getenv("TASK_REMOTE_DIR"))
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
} }
return nil return nil
@@ -155,7 +178,7 @@ func (e *Executor) setupOutput() error {
} }
var err error var err error
e.Output, err = output.BuildFor(&e.OutputStyle) e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
return err return err
} }
@@ -190,7 +213,7 @@ func (e *Executor) readDotEnvFiles() error {
} }
err = env.Range(func(key string, value ast.Var) error { err = env.Range(func(key string, value ast.Var) error {
if ok := e.Taskfile.Env.Exists(key); !ok { if _, ok := e.Taskfile.Env.Get(key); !ok {
e.Taskfile.Env.Set(key, value) e.Taskfile.Env.Set(key, value)
} }
return nil return nil
@@ -223,6 +246,9 @@ func (e *Executor) setupConcurrencyState() {
} }
func (e *Executor) doVersionChecks() error { func (e *Executor) doVersionChecks() error {
if !e.EnableVersionCheck {
return nil
}
// Copy the version to avoid modifying the original // Copy the version to avoid modifying the original
schemaVersion := &semver.Version{} schemaVersion := &semver.Version{}
*schemaVersion = *e.Taskfile.Version *schemaVersion = *e.Taskfile.Version

View File

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

View File

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

View File

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

100
task.go
View File

@@ -11,6 +11,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/compiler" "github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
@@ -34,13 +36,18 @@ const (
MaximumTaskCall = 1000 MaximumTaskCall = 1000
) )
type TempDir struct {
Remote string
Fingerprint string
}
// Executor executes a Taskfile // Executor executes a Taskfile
type Executor struct { type Executor struct {
Taskfile *ast.Taskfile Taskfile *ast.Taskfile
Dir string Dir string
Entrypoint string Entrypoint string
TempDir string TempDir TempDir
Force bool Force bool
ForceAll bool ForceAll bool
Insecure bool Insecure bool
@@ -63,12 +70,13 @@ type Executor struct {
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
Logger *logger.Logger Logger *logger.Logger
Compiler *compiler.Compiler Compiler *compiler.Compiler
Output output.Output Output output.Output
OutputStyle ast.Output OutputStyle ast.Output
TaskSorter sort.TaskSorter TaskSorter sort.TaskSorter
UserWorkingDir string UserWorkingDir string
EnableVersionCheck bool
fuzzyModel *fuzzy.Model fuzzyModel *fuzzy.Model
@@ -169,6 +177,10 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
return nil return nil
} }
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
t, err = e.CompiledTask(call) t, err = e.CompiledTask(call)
if err != nil { if err != nil {
return err return err
@@ -183,16 +195,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
release := e.acquireConcurrencyLimit() release := e.acquireConcurrencyLimit()
defer release() defer release()
if t.Prompt != "" {
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
return e.startExecution(ctx, t, func(ctx context.Context) error { return e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil { if err := e.runDeps(ctx, t); err != nil {
@@ -205,10 +207,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
return err return err
} }
if err := e.areTaskRequiredVarsSet(ctx, t, call); err != nil {
return err
}
preCondMet, err := e.areTaskPreconditionsMet(ctx, t) preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
if err != nil { if err != nil {
return err return err
@@ -222,7 +220,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir), fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry), fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger), fingerprint.WithLogger(e.Logger),
) )
@@ -238,13 +236,27 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
} }
} }
for _, p := range t.Prompt {
if p != "" && !e.Dry {
if err := e.Logger.Prompt(logger.Yellow, p, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
} else if errors.Is(err, logger.ErrPromptCancelled) {
return &errors.TaskCancelledByUserError{TaskName: call.Task}
} else if err != nil {
return err
}
}
}
if err := e.mkdir(t); err != nil { if err := e.mkdir(t); err != nil {
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err) e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
} }
var deferredExitCode uint8
for i := range t.Cmds { for i := range t.Cmds {
if t.Cmds[i].Defer { if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i) defer e.runDeferred(t, call, i, &deferredExitCode)
continue continue
} }
@@ -253,9 +265,13 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
} }
if execext.IsExitError(err) && t.IgnoreError { exitCode, isExitError := interp.IsExitStatus(err)
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) if isExitError {
continue if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue
}
deferredExitCode = exitCode
} }
if call.Indirect { if call.Indirect {
@@ -307,10 +323,26 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) { func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i]
vars, _ := e.Compiler.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 { if err := e.runCommand(ctx, t, call, i); err != nil {
e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error()) e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
} }
@@ -352,7 +384,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
if err != nil { if err != nil {
return fmt.Errorf("task: failed to get variables: %w", err) return fmt.Errorf("task: failed to get variables: %w", err)
} }
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater) stdOut, stdErr, closer := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
err = execext.RunCommand(ctx, &execext.RunCommandOptions{ err = execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd, Command: cmd.Cmd,
@@ -364,10 +396,10 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
Stdout: stdOut, Stdout: stdOut,
Stderr: stdErr, Stderr: stdErr,
}) })
if closeErr := close(err); closeErr != nil { if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr) e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
} }
if execext.IsExitError(err) && cmd.IgnoreError { if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err) e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
return nil return nil
} }
@@ -420,7 +452,7 @@ func (e *Executor) GetTask(call *ast.Call) (*ast.Task, error) {
case 0: // Carry on case 0: // Carry on
case 1: case 1:
if call.Vars == nil { if call.Vars == nil {
call.Vars = &ast.Vars{} call.Vars = ast.NewVars()
} }
call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards}) call.Vars.Set("MATCH", ast.Var{Value: matchingTasks[0].Wildcards})
return matchingTasks[0].Task, nil return matchingTasks[0].Task, nil
@@ -489,14 +521,12 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks // Compile the list of tasks
for i := range tasks { for i := range tasks {
idx := i
task := tasks[idx]
g.Go(func() error { g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: task.Task}) compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
if err != nil { if err != nil {
return err return err
} }
tasks[idx] = compiledTask tasks[i] = compiledTask
return nil return nil
}) })
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
package ast package ast
import ( import (
"fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
) )
@@ -46,7 +45,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var cmd string var cmd string
if err := node.Decode(&cmd); err != nil { if err := node.Decode(&cmd); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
c.Cmd = cmd c.Cmd = cmd
return nil return nil
@@ -76,11 +75,13 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
// A deferred command // A deferred command
var deferredCmd struct { var deferredCmd struct {
Defer string Defer string
Silent bool
} }
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" { if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
c.Defer = true c.Defer = true
c.Cmd = deferredCmd.Defer c.Cmd = deferredCmd.Defer
c.Silent = deferredCmd.Silent
return nil return nil
} }
@@ -92,6 +93,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
c.Defer = true c.Defer = true
c.Task = deferredCall.Defer.Task c.Task = deferredCall.Defer.Task
c.Vars = deferredCall.Defer.Vars c.Vars = deferredCall.Defer.Vars
c.Silent = deferredCall.Defer.Silent
return nil return nil
} }
@@ -110,8 +112,8 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
return nil return nil
} }
return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line) return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command")
} }

View File

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

View File

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

View File

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

View File

@@ -1,68 +1,134 @@
package ast package ast
import ( import (
"fmt" "sync"
"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
omap "github.com/go-task/task/v3/internal/omap" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
) )
// Include represents information about included taskfiles type (
type Include struct { // Include represents information about included taskfiles
Namespace string Include struct {
Taskfile string Namespace string
Dir string Taskfile string
Optional bool Dir string
Internal bool Optional bool
Aliases []string Internal bool
AdvancedImport bool Aliases []string
Vars *Vars Excludes []string
AdvancedImport bool
Vars *Vars
Flatten bool
}
// Includes is an ordered map of namespaces to includes.
Includes struct {
om *orderedmap.OrderedMap[string, *Include]
mutex sync.RWMutex
}
// An IncludeElement is a key-value pair that is used for initializing an
// Includes structure.
IncludeElement orderedmap.Element[string, *Include]
)
// NewIncludes creates a new instance of Includes and initializes it with the
// provided set of elements, if any. The elements are added in the order they
// are passed.
func NewIncludes(els ...*IncludeElement) *Includes {
includes := &Includes{
om: orderedmap.NewOrderedMap[string, *Include](),
}
for _, el := range els {
includes.Set(el.Key, el.Value)
}
return includes
} }
// Includes represents information about included tasksfiles // Len returns the number of includes in the Includes map.
type Includes struct { func (includes *Includes) Len() int {
omap.OrderedMap[string, *Include] if includes == nil || includes.om == nil {
return 0
}
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Len()
}
// Get returns the value the the include with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned include is a zero value and the bool is false.
func (includes *Includes) Get(key string) (*Include, bool) {
if includes == nil || includes.om == nil {
return &Include{}, false
}
defer includes.mutex.RUnlock()
includes.mutex.RLock()
return includes.om.Get(key)
}
// Set sets the value of the include with the provided key to the provided
// value. If the include already exists, its value is updated. If the include
// does not exist, it is created.
func (includes *Includes) Set(key string, value *Include) bool {
if includes == nil {
includes = NewIncludes()
}
if includes.om == nil {
includes.om = orderedmap.NewOrderedMap[string, *Include]()
}
defer includes.mutex.Unlock()
includes.mutex.Lock()
return includes.om.Set(key, value)
}
// Range calls the provided function for each include in the map. The function
// receives the include's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (includes *Includes) Range(f func(k string, v *Include) error) error {
if includes == nil || includes.om == nil {
return nil
}
for pair := includes.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil {
return err
}
}
return nil
} }
// UnmarshalYAML implements the yaml.Unmarshaler interface. // UnmarshalYAML implements the yaml.Unmarshaler interface.
func (includes *Includes) UnmarshalYAML(node *yaml.Node) error { func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
if includes == nil || includes.om == nil {
*includes = *NewIncludes()
}
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
// NOTE(@andreynering): on this style of custom unmarshalling, // NOTE: orderedmap does not have an unmarshaler, so we have to decode
// even number contains the keys, while odd numbers contains // the map manually. We increment over 2 values at a time and assign
// the values. // them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 { for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i] keyNode := node.Content[i]
valueNode := node.Content[i+1] valueNode := node.Content[i+1]
// Decode the value node into an Include struct
var v Include var v Include
if err := valueNode.Decode(&v); err != nil { if err := valueNode.Decode(&v); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
// Set the include namespace
v.Namespace = keyNode.Value v.Namespace = keyNode.Value
// Add the include to the ordered map
includes.Set(keyNode.Value, &v) includes.Set(keyNode.Value, &v)
} }
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes")
}
// Len returns the length of the map
func (includes *Includes) Len() int {
if includes == nil {
return 0
}
return includes.OrderedMap.Len()
}
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors
func (includes *Includes) Range(f func(k string, v *Include) error) error {
if includes == nil {
return nil
}
return includes.OrderedMap.Range(f)
} }
func (include *Include) UnmarshalYAML(node *yaml.Node) error { func (include *Include) UnmarshalYAML(node *yaml.Node) error {
@@ -71,7 +137,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
case yaml.ScalarNode: case yaml.ScalarNode:
var str string var str string
if err := node.Decode(&str); err != nil { if err := node.Decode(&str); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
include.Taskfile = str include.Taskfile = str
return nil return nil
@@ -82,23 +148,27 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Dir string Dir string
Optional bool Optional bool
Internal bool Internal bool
Flatten bool
Aliases []string Aliases []string
Excludes []string
Vars *Vars Vars *Vars
} }
if err := node.Decode(&includedTaskfile); err != nil { if err := node.Decode(&includedTaskfile); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
include.Taskfile = includedTaskfile.Taskfile include.Taskfile = includedTaskfile.Taskfile
include.Dir = includedTaskfile.Dir include.Dir = includedTaskfile.Dir
include.Optional = includedTaskfile.Optional include.Optional = includedTaskfile.Optional
include.Internal = includedTaskfile.Internal include.Internal = includedTaskfile.Internal
include.Aliases = includedTaskfile.Aliases include.Aliases = includedTaskfile.Aliases
include.Excludes = includedTaskfile.Excludes
include.AdvancedImport = true include.AdvancedImport = true
include.Vars = includedTaskfile.Vars include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("include")
} }
// DeepCopy creates a new instance of IncludedTaskfile and copies // DeepCopy creates a new instance of IncludedTaskfile and copies
@@ -113,7 +183,9 @@ func (include *Include) DeepCopy() *Include {
Dir: include.Dir, Dir: include.Dir,
Optional: include.Optional, Optional: include.Optional,
Internal: include.Internal, Internal: include.Internal,
Excludes: deepcopy.Slice(include.Excludes),
AdvancedImport: include.AdvancedImport, AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(), Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,
} }
} }

95
taskfile/ast/matrix.go Normal file
View File

@@ -0,0 +1,95 @@
package ast
import (
"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
type Matrix struct {
om *orderedmap.OrderedMap[string, []any]
}
type MatrixElement orderedmap.Element[string, []any]
func NewMatrix(els ...*MatrixElement) *Matrix {
matrix := &Matrix{
om: orderedmap.NewOrderedMap[string, []any](),
}
for _, el := range els {
matrix.Set(el.Key, el.Value)
}
return matrix
}
func (matrix *Matrix) Len() int {
if matrix == nil || matrix.om == nil {
return 0
}
return matrix.om.Len()
}
func (matrix *Matrix) Get(key string) ([]any, bool) {
if matrix == nil || matrix.om == nil {
return nil, false
}
return matrix.om.Get(key)
}
func (matrix *Matrix) Set(key string, value []any) bool {
if matrix == nil {
matrix = NewMatrix()
}
if matrix.om == nil {
matrix.om = orderedmap.NewOrderedMap[string, []any]()
}
return matrix.om.Set(key, value)
}
func (matrix *Matrix) Range(f func(k string, v []any) error) error {
if matrix == nil || matrix.om == nil {
return nil
}
for pair := matrix.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil {
return err
}
}
return nil
}
func (matrix *Matrix) DeepCopy() *Matrix {
if matrix == nil {
return nil
}
return &Matrix{
om: deepcopy.OrderedMap(matrix.om),
}
}
func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
// the map manually. We increment over 2 values at a time and assign
// them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
// Decode the value node into a Matrix struct
var v []any
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the task to the ordered map
matrix.Set(keyNode.Value, v)
}
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("matrix")
}

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ import (
) )
func TestPlatformParsing(t *testing.T) { func TestPlatformParsing(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
Input string Input string
ExpectedOS string ExpectedOS string
@@ -26,14 +28,16 @@ func TestPlatformParsing(t *testing.T) {
{Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"}, {Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"},
{Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"}, {Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"},
{Input: "invalid", Error: `task: Invalid platform "invalid"`}, {Input: "invalid", Error: `invalid platform "invalid"`},
{Input: "invalid/invalid", Error: `task: Invalid platform "invalid/invalid"`}, {Input: "invalid/invalid", Error: `invalid platform "invalid/invalid"`},
{Input: "windows/invalid", Error: `task: Invalid platform "windows/invalid"`}, {Input: "windows/invalid", Error: `invalid platform "windows/invalid"`},
{Input: "invalid/amd64", Error: `task: Invalid platform "invalid/amd64"`}, {Input: "invalid/amd64", Error: `invalid platform "invalid/amd64"`},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.Input, func(t *testing.T) { t.Run(test.Input, func(t *testing.T) {
t.Parallel()
var p Platform var p Platform
err := p.parsePlatform(test.Input) err := p.parsePlatform(test.Input)

View File

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

View File

@@ -11,6 +11,8 @@ import (
) )
func TestPreconditionParse(t *testing.T) { func TestPreconditionParse(t *testing.T) {
t.Parallel()
tests := []struct { tests := []struct {
content string content string
v any v any

29
taskfile/ast/prompt.go Normal file
View File

@@ -0,0 +1,29 @@
package ast
import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
)
type Prompt []string
func (p *Prompt) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var str string
if err := node.Decode(&str); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
*p = []string{str}
return nil
case yaml.SequenceNode:
var list []string
if err := node.Decode(&list); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
*p = list
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("prompt")
}

View File

@@ -1,10 +1,15 @@
package ast package ast
import "github.com/go-task/task/v3/internal/deepcopy" import (
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
)
// Requires represents a set of required variables necessary for a task to run // Requires represents a set of required variables necessary for a task to run
type Requires struct { type Requires struct {
Vars []string Vars []*VarsWithValidation
} }
func (r *Requires) DeepCopy() *Requires { func (r *Requires) DeepCopy() *Requires {
@@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
Vars: deepcopy.Slice(r.Vars), Vars: deepcopy.Slice(r.Vars),
} }
} }
type VarsWithValidation struct {
Name string
Enum []string
}
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
if v == nil {
return nil
}
return &VarsWithValidation{
Name: v.Name,
Enum: v.Enum,
}
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
var cmd string
if err := node.Decode(&cmd); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Name = cmd
v.Enum = nil
return nil
case yaml.MappingNode:
var vv struct {
Name string
Enum []string
}
if err := node.Decode(&vv); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Name = vv.Name
v.Enum = vv.Enum
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires")
}

View File

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

View File

@@ -1,12 +1,13 @@
package ast package ast
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
) )
// NamespaceSeparator contains the character that separates namespaces // NamespaceSeparator contains the character that separates namespaces
@@ -28,7 +29,7 @@ type Taskfile struct {
Shopt []string Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks Tasks Tasks *Tasks
Silent bool Silent bool
Dotenv []string Dotenv []string
Run string Run string
@@ -46,16 +47,21 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t2.Output.IsSet() { if t2.Output.IsSet() {
t1.Output = t2.Output t1.Output = t2.Output
} }
if t1.Includes == nil {
t1.Includes = NewIncludes()
}
if t1.Vars == nil { if t1.Vars == nil {
t1.Vars = &Vars{} t1.Vars = NewVars()
} }
if t1.Env == nil { if t1.Env == nil {
t1.Env = &Vars{} t1.Env = NewVars()
}
if t1.Tasks == nil {
t1.Tasks = NewTasks()
} }
t1.Vars.Merge(t2.Vars, include) t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include) t1.Env.Merge(t2.Env, include)
t1.Tasks.Merge(t2.Tasks, include, t1.Vars) return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
return nil
} }
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
@@ -70,14 +76,14 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
Shopt []string Shopt []string
Vars *Vars Vars *Vars
Env *Vars Env *Vars
Tasks Tasks Tasks *Tasks
Silent bool Silent bool
Dotenv []string Dotenv []string
Run string Run string
Interval time.Duration Interval time.Duration
} }
if err := node.Decode(&taskfile); err != nil { if err := node.Decode(&taskfile); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
tf.Version = taskfile.Version tf.Version = taskfile.Version
tf.Output = taskfile.Output tf.Output = taskfile.Output
@@ -92,14 +98,20 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run tf.Run = taskfile.Run
tf.Interval = taskfile.Interval tf.Interval = taskfile.Interval
if tf.Includes == nil {
tf.Includes = NewIncludes()
}
if tf.Vars == nil { if tf.Vars == nil {
tf.Vars = &Vars{} tf.Vars = NewVars()
} }
if tf.Env == nil { if tf.Env == nil {
tf.Env = &Vars{} tf.Env = NewVars()
}
if tf.Tasks == nil {
tf.Tasks = NewTasks()
} }
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile")
} }

View File

@@ -7,11 +7,12 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/omap"
"github.com/go-task/task/v3/taskfile/ast" "github.com/go-task/task/v3/taskfile/ast"
) )
func TestCmdParse(t *testing.T) { func TestCmdParse(t *testing.T) {
t.Parallel()
const ( const (
yamlCmd = `echo "a string command"` yamlCmd = `echo "a string command"`
yamlDep = `"task-name"` yamlDep = `"task-name"`
@@ -38,15 +39,21 @@ vars:
yamlTaskCall, yamlTaskCall,
&ast.Cmd{}, &ast.Cmd{},
&ast.Cmd{ &ast.Cmd{
Task: "another-task", Vars: &ast.Vars{ Task: "another-task",
OrderedMap: omap.FromMapWithOrder( Vars: ast.NewVars(
map[string]ast.Var{ &ast.VarElement{
"PARAM1": {Value: "VALUE1"}, Key: "PARAM1",
"PARAM2": {Value: "VALUE2"}, Value: ast.Var{
Value: "VALUE1",
}, },
[]string{"PARAM1", "PARAM2"}, },
), &ast.VarElement{
}, Key: "PARAM2",
Value: ast.Var{
Value: "VALUE2",
},
},
),
}, },
}, },
{ {
@@ -58,14 +65,15 @@ vars:
yamlDeferredCall, yamlDeferredCall,
&ast.Cmd{}, &ast.Cmd{},
&ast.Cmd{ &ast.Cmd{
Task: "some_task", Vars: &ast.Vars{ Task: "some_task",
OrderedMap: omap.FromMapWithOrder( Vars: ast.NewVars(
map[string]ast.Var{ &ast.VarElement{
"PARAM1": {Value: "var"}, Key: "PARAM1",
Value: ast.Var{
Value: "var",
}, },
[]string{"PARAM1"}, },
), ),
},
Defer: true, Defer: true,
}, },
}, },
@@ -78,15 +86,21 @@ vars:
yamlTaskCall, yamlTaskCall,
&ast.Dep{}, &ast.Dep{},
&ast.Dep{ &ast.Dep{
Task: "another-task", Vars: &ast.Vars{ Task: "another-task",
OrderedMap: omap.FromMapWithOrder( Vars: ast.NewVars(
map[string]ast.Var{ &ast.VarElement{
"PARAM1": {Value: "VALUE1"}, Key: "PARAM1",
"PARAM2": {Value: "VALUE2"}, Value: ast.Var{
Value: "VALUE1",
}, },
[]string{"PARAM1", "PARAM2"}, },
), &ast.VarElement{
}, Key: "PARAM2",
Value: ast.Var{
Value: "VALUE2",
},
},
),
}, },
}, },
} }

View File

@@ -2,32 +2,137 @@ package ast
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"sync"
"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/omap"
) )
// Tasks represents a group of tasks type (
type Tasks struct { // Tasks is an ordered map of task names to Tasks.
omap.OrderedMap[string, *Task] Tasks struct {
om *orderedmap.OrderedMap[string, *Task]
mutex sync.RWMutex
}
// A TaskElement is a key-value pair that is used for initializing a Tasks
// structure.
TaskElement orderedmap.Element[string, *Task]
// MatchingTask represents a task that matches a given call. It includes the
// task itself and a list of wildcards that were matched.
MatchingTask struct {
Task *Task
Wildcards []string
}
)
// NewTasks creates a new instance of Tasks and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewTasks(els ...*TaskElement) *Tasks {
tasks := &Tasks{
om: orderedmap.NewOrderedMap[string, *Task](),
}
for _, el := range els {
tasks.Set(el.Key, el.Value)
}
return tasks
} }
type MatchingTask struct { // Len returns the number of variables in the Tasks map.
Task *Task func (tasks *Tasks) Len() int {
Wildcards []string if tasks == nil || tasks.om == nil {
return 0
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Len()
} }
// Get returns the value the the task with the provided key and a boolean that
// indicates if the value was found or not. If the value is not found, the
// returned task is a zero value and the bool is false.
func (tasks *Tasks) Get(key string) (*Task, bool) {
if tasks == nil || tasks.om == nil {
return &Task{}, false
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
return tasks.om.Get(key)
}
// Set sets the value of the task with the provided key to the provided value.
// If the task already exists, its value is updated. If the task does not exist,
// it is created.
func (tasks *Tasks) Set(key string, value *Task) bool {
if tasks == nil {
tasks = NewTasks()
}
if tasks.om == nil {
tasks.om = orderedmap.NewOrderedMap[string, *Task]()
}
defer tasks.mutex.Unlock()
tasks.mutex.Lock()
return tasks.om.Set(key, value)
}
// Range calls the provided function for each task in the map. The function
// receives the task's key and value as arguments. If the function returns an
// error, the iteration stops and the error is returned.
func (tasks *Tasks) Range(f func(k string, v *Task) error) error {
if tasks == nil || tasks.om == nil {
return nil
}
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil {
return err
}
}
return nil
}
// Keys returns a slice of all the keys in the Tasks map.
func (tasks *Tasks) Keys() []string {
if tasks == nil {
return nil
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var keys []string
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
keys = append(keys, pair.Key)
}
return keys
}
// Values returns a slice of all the values in the Tasks map.
func (tasks *Tasks) Values() []*Task {
if tasks == nil {
return nil
}
defer tasks.mutex.RUnlock()
tasks.mutex.RLock()
var values []*Task
for pair := tasks.om.Front(); pair != nil; pair = pair.Next() {
values = append(values, pair.Value)
}
return values
}
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask { func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
if call == nil { if call == nil {
return nil return nil
} }
var task *Task
var matchingTasks []*MatchingTask var matchingTasks []*MatchingTask
// If there is a direct match, return it // If there is a direct match, return it
if task = t.OrderedMap.Get(call.Task); task != nil { if task, ok := t.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil}) matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks return matchingTasks
} }
@@ -45,112 +150,131 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
return matchingTasks return matchingTasks
} }
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) { func (t1 *Tasks) Merge(t2 *Tasks, include *Include, includedTaskfileVars *Vars) error {
_ = t2.Range(func(k string, v *Task) error { defer t2.mutex.RUnlock()
t2.mutex.RLock()
err := t2.Range(func(name string, v *Task) error {
// We do a deep copy of the task struct here to ensure that no data can // We do a deep copy of the task struct here to ensure that no data can
// be changed elsewhere once the taskfile is merged. // be changed elsewhere once the taskfile is merged.
task := v.DeepCopy() task := v.DeepCopy()
// Set the task to internal if EITHER the included task or the included // Set the task to internal if EITHER the included task or the included
// taskfile are marked as internal // taskfile are marked as internal
task.Internal = task.Internal || (include != nil && include.Internal) task.Internal = task.Internal || (include != nil && include.Internal)
taskName := name
// Add namespaces to task dependencies // if the task is in the exclude list, don't add it to the merged taskfile and early return
for _, dep := range task.Deps { if slices.Contains(include.Excludes, name) {
if dep != nil && dep.Task != "" { return nil
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
}
} }
// Add namespaces to task commands if !include.Flatten {
for _, cmd := range task.Cmds { // Add namespaces to task dependencies
if cmd != nil && cmd.Task != "" { for _, dep := range task.Deps {
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace) if dep != nil && dep.Task != "" {
} dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
}
// Add namespaces to task aliases
for i, alias := range task.Aliases {
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
}
// Add namespace aliases
if include != nil {
for _, namespaceAlias := range include.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
for _, alias := range v.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
} }
} }
// Add namespaces to task commands
for _, cmd := range task.Cmds {
if cmd != nil && cmd.Task != "" {
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
}
}
// Add namespaces to task aliases
for i, alias := range task.Aliases {
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
}
// Add namespace aliases
if include != nil {
for _, namespaceAlias := range include.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
for _, alias := range v.Aliases {
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
}
}
}
taskName = taskNameWithNamespace(name, include.Namespace)
task.Namespace = include.Namespace
task.Task = taskName
} }
if include.AdvancedImport { if include.AdvancedImport {
task.Dir = filepathext.SmartJoin(include.Dir, task.Dir) task.Dir = filepathext.SmartJoin(include.Dir, task.Dir)
if task.IncludeVars == nil { if task.IncludeVars == nil {
task.IncludeVars = &Vars{} task.IncludeVars = NewVars()
} }
task.IncludeVars.Merge(include.Vars, nil) task.IncludeVars.Merge(include.Vars, nil)
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy() task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
} }
if _, ok := t1.Get(taskName); ok {
return &errors.TaskNameFlattenConflictError{
TaskName: taskName,
Include: include.Namespace,
}
}
// Add the task to the merged taskfile // Add the task to the merged taskfile
taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace) t1.Set(taskName, task)
task.Task = taskNameWithNamespace
t1.Set(taskNameWithNamespace, task)
return nil return nil
}) })
// If the included Taskfile has a default task and the parent namespace has // If the included Taskfile has a default task, is not flattened and the
// no task with a matching name, we can add an alias so that the user can // parent namespace has no task with a matching name, we can add an alias so
// run the included Taskfile's default task without specifying its full // that the user can run the included Taskfile's default task without
// name. If the parent namespace has aliases, we add another alias for each // specifying its full name. If the parent namespace has aliases, we add
// of them. // another alias for each of them.
if t2.Get("default") != nil && t1.Get(include.Namespace) == nil { _, t2DefaultExists := t2.Get("default")
_, t1NamespaceExists := t1.Get(include.Namespace)
if t2DefaultExists && !t1NamespaceExists && !include.Flatten {
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace) defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace) t1DefaultTask, ok := t1.Get(defaultTaskName)
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Aliases...) if ok {
t1DefaultTask.Aliases = append(t1DefaultTask.Aliases, include.Namespace)
t1DefaultTask.Aliases = slices.Concat(t1DefaultTask.Aliases, include.Aliases)
}
} }
return err
} }
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error { func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
if t == nil || t.om == nil {
*t = *NewTasks()
}
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
tasks := omap.New[string, *Task]() // NOTE: orderedmap does not have an unmarshaler, so we have to decode
if err := node.Decode(&tasks); err != nil { // the map manually. We increment over 2 values at a time and assign
return err // them as a key-value pair.
} for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
// nolint: errcheck // Decode the value node into a Task struct
tasks.Range(func(name string, task *Task) error { var v Task
// Set the task's name if err := valueNode.Decode(&v); err != nil {
if task == nil { return errors.NewTaskfileDecodeError(err, node)
task = &Task{
Task: name,
}
} }
task.Task = name
// Set the task's location // Set the task name and location
for _, keys := range node.Content { v.Task = keyNode.Value
if keys.Value == name { v.Location = &Location{
task.Location = &Location{ Line: keyNode.Line,
Line: keys.Line, Column: keyNode.Column,
Column: keys.Column,
}
}
} }
tasks.Set(name, task)
return nil
})
*t = Tasks{ // Add the task to the ordered map
OrderedMap: tasks, t.Set(keyNode.Value, &v)
} }
return nil return nil
} }
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag()) return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks")
} }
func taskNameWithNamespace(taskName string, namespace string) string { func taskNameWithNamespace(taskName string, namespace string) string {

View File

@@ -1,90 +1,176 @@
package ast package ast
import ( import (
"fmt"
"strings" "strings"
"sync"
"github.com/elliotchance/orderedmap/v2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/omap"
) )
// Vars is a string[string] variables map. type (
type Vars struct { // Vars is an ordered map of variable names to values.
omap.OrderedMap[string, Var] Vars struct {
om *orderedmap.OrderedMap[string, Var]
mutex sync.RWMutex
}
// A VarElement is a key-value pair that is used for initializing a Vars
// structure.
VarElement orderedmap.Element[string, Var]
)
// NewVars creates a new instance of Vars and initializes it with the provided
// set of elements, if any. The elements are added in the order they are passed.
func NewVars(els ...*VarElement) *Vars {
vars := &Vars{
om: orderedmap.NewOrderedMap[string, Var](),
}
for _, el := range els {
vars.Set(el.Key, el.Value)
}
return vars
} }
// ToCacheMap converts Vars to a map containing only the static // Len returns the number of variables in the Vars map.
func (vars *Vars) Len() int {
if vars == nil || vars.om == nil {
return 0
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Len()
}
// Get returns the value the the variable with the provided key and a boolean
// that indicates if the value was found or not. If the value is not found, the
// returned variable is a zero value and the bool is false.
func (vars *Vars) Get(key string) (Var, bool) {
if vars == nil || vars.om == nil {
return Var{}, false
}
defer vars.mutex.RUnlock()
vars.mutex.RLock()
return vars.om.Get(key)
}
// Set sets the value of the variable with the provided key to the provided
// value. If the variable already exists, its value is updated. If the variable
// does not exist, it is created.
func (vars *Vars) Set(key string, value Var) bool {
if vars == nil {
vars = NewVars()
}
if vars.om == nil {
vars.om = orderedmap.NewOrderedMap[string, Var]()
}
defer vars.mutex.Unlock()
vars.mutex.Lock()
return vars.om.Set(key, value)
}
// Range calls the provided function for each variable in the map. The function
// receives the variable's key and value as arguments. If the function returns
// an error, the iteration stops and the error is returned.
func (vars *Vars) Range(f func(k string, v Var) error) error {
if vars == nil || vars.om == nil {
return nil
}
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if err := f(pair.Key, pair.Value); err != nil {
return err
}
}
return nil
}
// ToCacheMap converts Vars to an unordered map containing only the static
// variables // variables
func (vs *Vars) ToCacheMap() (m map[string]any) { func (vars *Vars) ToCacheMap() (m map[string]any) {
m = make(map[string]any, vs.Len()) defer vars.mutex.RUnlock()
_ = vs.Range(func(k string, v Var) error { vars.mutex.RLock()
if v.Sh != "" { m = make(map[string]any, vars.Len())
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
if pair.Value.Sh != nil && *pair.Value.Sh != "" {
// Dynamic variable is not yet resolved; trigger // Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates. // <no value> to be used in templates.
return nil return nil
} }
if pair.Value.Live != nil {
if v.Live != nil { m[pair.Key] = pair.Value.Live
m[k] = v.Live
} else { } else {
m[k] = v.Value m[pair.Key] = pair.Value.Value
} }
return nil }
})
return return
} }
// Wrapper around OrderedMap.Set to ensure we don't get nil pointer errors // Merge loops over other and merges it values with the variables in vars. If
func (vs *Vars) Range(f func(k string, v Var) error) error { // the include parameter is not nil and its it is an advanced import, the
if vs == nil { // directory is set set to the value of the include parameter.
return nil func (vars *Vars) Merge(other *Vars, include *Include) {
} if vars == nil || vars.om == nil || other == nil {
return vs.OrderedMap.Range(f)
}
// Wrapper around OrderedMap.Merge to ensure we don't get nil pointer errors
func (vs *Vars) Merge(other *Vars, include *Include) {
if vs == nil || other == nil {
return return
} }
_ = other.Range(func(key string, value Var) error { defer other.mutex.RUnlock()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
if include != nil && include.AdvancedImport { if include != nil && include.AdvancedImport {
value.Dir = include.Dir pair.Value.Dir = include.Dir
} }
vs.Set(key, value) vars.om.Set(pair.Key, pair.Value)
return nil
})
}
// Wrapper around OrderedMap.Len to ensure we don't get nil pointer errors
func (vs *Vars) Len() int {
if vs == nil {
return 0
} }
return vs.OrderedMap.Len()
} }
// DeepCopy creates a new instance of Vars and copies
// data by value from the source struct.
func (vs *Vars) DeepCopy() *Vars { func (vs *Vars) DeepCopy() *Vars {
if vs == nil { if vs == nil {
return nil return nil
} }
defer vs.mutex.RUnlock()
vs.mutex.RLock()
return &Vars{ return &Vars{
OrderedMap: vs.OrderedMap.DeepCopy(), om: deepcopy.OrderedMap(vs.om),
} }
} }
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
if vs == nil || vs.om == nil {
*vs = *NewVars()
}
vs.om = orderedmap.NewOrderedMap[string, Var]()
switch node.Kind {
case yaml.MappingNode:
// NOTE: orderedmap does not have an unmarshaler, so we have to decode
// the map manually. We increment over 2 values at a time and assign
// them as a key-value pair.
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
// Decode the value node into a Task struct
var v Var
if err := valueNode.Decode(&v); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// Add the task to the ordered map
vs.Set(keyNode.Value, v)
}
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("vars")
}
// Var represents either a static or dynamic variable. // Var represents either a static or dynamic variable.
type Var struct { type Var struct {
Value any Value any
Live any Live any
Sh string Sh *string
Ref string Ref string
Json string
Yaml string
Dir string Dir string
} }
@@ -95,12 +181,16 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.MapVariables.Value == "1" { if experiments.MapVariables.Value == "1" {
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
// If the value is a string and it starts with $, then it's a shell command // If the value is a string and it starts with $, then it's a shell command
if str, ok := value.(string); ok { if str, ok := value.(string); ok {
if str, ok = strings.CutPrefix(str, "$"); ok { if str, ok = strings.CutPrefix(str, "$"); ok {
v.Sh = str v.Sh = &str
return nil
}
if str, ok = strings.CutPrefix(str, "#"); ok {
v.Ref = str
return nil return nil
} }
} }
@@ -114,30 +204,26 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode: case yaml.MappingNode:
key := node.Content[0].Value key := node.Content[0].Value
switch key { switch key {
case "sh", "ref", "map", "json", "yaml": case "sh", "ref", "map":
var m struct { var m struct {
Sh string Sh *string
Ref string Ref string
Map any Map any
Json string
Yaml string
} }
if err := node.Decode(&m); err != nil { if err := node.Decode(&m); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Sh = m.Sh v.Sh = m.Sh
v.Ref = m.Ref v.Ref = m.Ref
v.Value = m.Map v.Value = m.Map
v.Json = m.Json
v.Yaml = m.Yaml
return nil return nil
default: default:
return fmt.Errorf(`yaml: line %d: %q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, node.Line, key) return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
} }
default: default:
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Value = value v.Value = value
return nil return nil
@@ -148,22 +234,27 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
if len(node.Content) > 2 || node.Content[0].Value != "sh" { key := node.Content[0].Value
return fmt.Errorf(`task: line %d: maps cannot be assigned to variables`, node.Line) switch key {
case "sh", "ref":
var m struct {
Sh *string
Ref string
}
if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
return nil
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables")
} }
var sh struct {
Sh string
}
if err := node.Decode(&sh); err != nil {
return err
}
v.Sh = sh.Sh
return nil
default: default:
var value any var value any
if err := node.Decode(&value); err != nil { if err := node.Decode(&value); err != nil {
return err return errors.NewTaskfileDecodeError(err, node)
} }
v.Value = value v.Value = value
return nil return nil

View File

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

View File

@@ -1,6 +1,7 @@
package taskfile package taskfile
import ( import (
"fmt"
"os" "os"
"github.com/joho/godotenv" "github.com/joho/godotenv"
@@ -21,7 +22,7 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro
return nil, err return nil, err
} }
env := &ast.Vars{} env := ast.NewVars()
cache := &templater.Cache{Vars: vars} cache := &templater.Cache{Vars: vars}
for _, dotEnvPath := range tf.Dotenv { for _, dotEnvPath := range tf.Dotenv {
@@ -37,10 +38,10 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro
envs, err := godotenv.Read(dotEnvPath) envs, err := godotenv.Read(dotEnvPath)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("error reading env file %s: %w", dotEnvPath, err)
} }
for key, value := range envs { for key, value := range envs {
if ok := env.Exists(key); !ok { if _, ok := env.Get(key); !ok {
env.Set(key, ast.Var{Value: value}) env.Set(key, ast.Var{Value: value})
} }
} }

View File

@@ -7,6 +7,8 @@ import (
"strings" "strings"
"time" "time"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments" "github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -20,6 +22,7 @@ type Node interface {
Remote() bool Remote() bool
ResolveEntrypoint(entrypoint string) (string, error) ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error) ResolveDir(dir string) (string, error)
FilenameAndLastDir() (string, string)
} }
func NewRootNode( func NewRootNode(
@@ -47,24 +50,39 @@ func NewNode(
) (Node, error) { ) (Node, error) {
var node Node var node Node
var err error var err error
switch getScheme(entrypoint) { scheme, err := getScheme(entrypoint)
if err != nil {
return nil, err
}
switch scheme {
case "git":
node, err = NewGitNode(entrypoint, dir, insecure, opts...)
case "http", "https": case "http", "https":
node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...) node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...)
default: default:
// If no other scheme matches, we assume it's a file
node, err = NewFileNode(l, entrypoint, dir, opts...) node, err = NewFileNode(l, entrypoint, dir, opts...)
} }
if node.Remote() && !experiments.RemoteTaskfiles.Enabled { if node.Remote() && !experiments.RemoteTaskfiles.Enabled {
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles") return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
} }
return node, err return node, err
} }
func getScheme(uri string) string { func getScheme(uri string) (string, error) {
if i := strings.Index(uri, "://"); i != -1 { u, err := giturls.Parse(uri)
return uri[:i] if u == nil {
return "", err
} }
return "" if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
return "git", nil
}
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i], nil
}
return "", nil
} }
func getDefaultDir(entrypoint, dir string) string { func getDefaultDir(entrypoint, dir string) string {

View File

@@ -81,6 +81,9 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
if strings.Contains(entrypoint, "://") { if strings.Contains(entrypoint, "://") {
return entrypoint, nil return entrypoint, nil
} }
if strings.HasPrefix(entrypoint, "git") {
return entrypoint, nil
}
path, err := execext.Expand(entrypoint) path, err := execext.Expand(entrypoint)
if err != nil { if err != nil {
@@ -112,3 +115,7 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
entrypointDir := filepath.Dir(node.Entrypoint) entrypointDir := filepath.Dir(node.Entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil return filepathext.SmartJoin(entrypointDir, path), nil
} }
func (node *FileNode) FilenameAndLastDir() (string, string) {
return "", filepath.Base(node.Entrypoint)
}

126
taskfile/node_git.go Normal file
View File

@@ -0,0 +1,126 @@
package taskfile
import (
"context"
"fmt"
"io"
"net/url"
"path/filepath"
"strings"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
)
// An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct {
*BaseNode
URL *url.URL
rawUrl string
ref string
path string
}
func NewGitNode(
entrypoint string,
dir string,
insecure bool,
opts ...NodeOption,
) (*GitNode, error) {
base := NewBaseNode(dir, opts...)
u, err := giturls.Parse(entrypoint)
if err != nil {
return nil, err
}
basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
ref := u.Query().Get("ref")
rawUrl := u.String()
u.RawQuery = ""
u.Path = basePath
if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
}
return &GitNode{
BaseNode: base,
URL: u,
rawUrl: rawUrl,
ref: ref,
path: path,
}, nil
}
func (node *GitNode) Location() string {
return node.rawUrl
}
func (node *GitNode) Remote() bool {
return true
}
func (node *GitNode) Read(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
})
if err != nil {
return nil, err
}
file, err := fs.Open(node.path)
if err != nil {
return nil, err
}
// Read the entire response body
b, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return b, nil
}
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
return resolvedEntrypoint, nil
}
func (node *GitNode) ResolveDir(dir string) (string, error) {
path, err := execext.Expand(dir)
if err != nil {
return "", err
}
if filepathext.IsAbs(path) {
return path, nil
}
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
// This means that files are included relative to one another
entrypointDir := filepath.Dir(node.Dir())
return filepathext.SmartJoin(entrypointDir, path), nil
}
func (node *GitNode) FilenameAndLastDir() (string, string) {
return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path))
}

85
taskfile/node_git_test.go Normal file
View File

@@ -0,0 +1,85 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGitNode_ssh(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}
func TestGitNode_sshWithDir(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}
func TestGitNode_https(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}
func TestGitNode_httpsWithDir(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}
func TestGitNode_FilenameAndDir(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir := node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, ".", dir)
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
}

View File

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

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