Compare commits

...

198 Commits

Author SHA1 Message Date
Andrey Nering
72e25a25fd v3.45.5 2025-11-11 17:13:39 -03:00
renovate[bot]
a496ee5fcb chore(deps): update all non-major dependencies (#2501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-11 20:12:27 +00:00
Andrey Nering
ef4292c42f chore(changelog): add entry for #2506 2025-11-11 17:09:17 -03:00
Andrey Nering
dc315efc7f chore(deps): update mvdan.cc/sh/moreinterp with core utils fixes
* Fixes https://github.com/go-task/task/issues/2426
* Ref https://github.com/u-root/u-root/pull/3464
* Ref https://github.com/mvdan/sh/pull/1199
2025-11-11 17:09:17 -03:00
Andrey Nering
a3a3e7fb0b chore(changelog): add entry for #2434 2025-11-11 16:51:53 -03:00
Andrey Nering
ee99849b1d refactor: migrate to the official yaml package (#2434)
The old package is long archived, but the YAML org forked it and will
officially maintain it from now on.

* Old: https://github.com/go-yaml/yaml
* New: https://github.com/yaml/go-yaml
2025-11-11 19:49:37 +00:00
Andrey Nering
bf9dc3f662 chore(changelog): add entry for #2286 2025-11-11 16:43:15 -03:00
Graham Dennis
94f82cbc5a fix: make task failure errors include stack of running tasks (#2286)
Previously if a task was run as a dependency of another task,
the error message simply reported something like:

    exit status 1

It is desirable instead to name the root task and all child tasks in the tree
to the failing task.

After this PR, the error message will read:

    task: Failed to run task "root": task: Failed to run task "failing-task": exit status 1
2025-11-11 16:40:40 -03:00
Valentin Maerten
b14318ed3f chore: changelog for #2494 2025-11-11 20:40:32 +01:00
Valentin Maerten
17757c0c15 fix: better error when a Taskfile does not exist in include (#2494) 2025-11-11 20:37:24 +01:00
Andrey Nering
19f72b7eb0 chore(changelog): add entry for #2418 2025-11-11 15:51:33 -03:00
Timothy Rule
0052ad2309 fix: do not re-evaluate variables for defer: (#2418) 2025-11-11 15:50:01 -03:00
Andrey Nering
af1e755196 chore(changelog): add entry for #2350 2025-11-11 14:42:18 -03:00
Andrey Nering
43074c20f2 refactor: improve code of group output 2025-11-11 14:37:16 -03:00
Timothy Rule
39c86992bd fix: address concurrent group output causing flaky tests (#2350) 2025-11-11 14:36:32 -03:00
Kaj Kowalski
c71241bcbd docs: fix YAML syntax errors in schema and guide documentation (#2500) 2025-11-10 11:00:39 +01:00
renovate[bot]
7c2bb78540 chore(deps): update all non-major dependencies (#2492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-07 21:21:21 -03:00
Andrey Nering
32e675895a chore(website): update umami domain 2025-11-07 17:48:28 -03:00
Tatsuya Kyushima
786813d95d docs: add fzf-make to "community integrations" (#2393) 2025-11-04 14:32:53 -03:00
Valentin Maerten
f7287c503a docs: dictionary operations example was wrong (#2490) 2025-11-02 19:17:13 +01:00
Valentin Maerten
413574e3ee chore: changelog for #1322, #2053 2025-11-02 17:25:35 +01:00
Valentin Maerten
4b39becf65 chore: changelog for #2460, #2461 2025-11-02 17:25:35 +01:00
Valentin Maerten
15b7e3c69a refactor: VeryFastCompile for Task list (#2053) 2025-11-02 17:25:07 +01:00
Libor Mořkovský
7c93ea8b44 docs: add reference to slim-sprig in the templating page (#2472) 2025-11-02 17:21:17 +01:00
Valentin Maerten
6a7cfa58f9 fix: return taskrc config even if there is an error (#2461) 2025-11-02 17:15:58 +01:00
renovate[bot]
74b93f6eef chore(deps): update all non-major dependencies (#2463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 17:03:54 +01:00
Andrey Nering
88101613c8 docs: add magic.dev as a gold sponsor 2025-10-17 09:56:59 -03:00
Valentin Maerten
599591ad3c chore: changelog for #2437, #2438 2025-10-12 13:30:28 +02:00
Skip Baney
348158a5f6 fix: properly resolve remote entrypoints (#2438) 2025-10-12 13:29:57 +02:00
pancho horrillo
c3e410e95a docs: update Arch and Nix community links (#2454) 2025-10-10 22:33:52 +02:00
Aku Kotkavuo
42bcd5406a docs: link to the known bug with --watch (#2449) 2025-10-10 22:26:40 +02:00
renovate[bot]
ba23aca631 chore(deps): update module github.com/puzpuzpuz/xsync/v3 to v4 (#2168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 22:22:16 +02:00
renovate[bot]
5ef245a4bd chore(deps): update all non-major dependencies (#2448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 22:12:37 +02:00
renovate[bot]
036a60f517 chore(deps): update actions/github-script action to v8 (#2421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 21:55:30 +02:00
renovate[bot]
9c969541a5 chore(deps): update actions/setup-python action to v6 (#2457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 21:35:21 +02:00
renovate[bot]
a52b483dd0 chore(deps): update actions/setup-go action to v6 (#2456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 21:32:17 +02:00
Pete Davison
4e84c6bb76 fix: links to static files 2025-09-23 22:34:43 +00:00
merusso
0f9baf62a1 docs: Update Remote Taskfiles default values (#2430)
* docs: Update Remote Taskfiles default values

This change updates the documentation for [Remote Taskfiles > Configuration](https://taskfile.dev/docs/experiments/remote-taskfiles#configuration) for `timeout` and `cache-expiry` to match the defaults in code.

* docs: Update `cache-expiry` default to 0s

* docs: Update executor.go comment

Fix doc comment to indicate that cache expiry default duration is 0.
2025-09-23 13:10:20 +01:00
renovate[bot]
979ad523ef chore(deps): update mvdan.cc/sh/moreinterp digest to 1714925 (#2435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:30:52 -03:00
renovate[bot]
975c07688e chore(deps): update all non-major dependencies (#2436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 09:30:29 -03:00
Andrey Nering
67a02255b5 docs(changelog): add entry for #2431 2025-09-21 16:12:06 -03:00
Andrey Nering
028ae1a660 fix: fix message shown when a taskfile was not found (#2431) 2025-09-21 16:10:06 -03:00
Andrey Nering
68b1d2783d lint: fix lint by passing context 2025-09-21 16:09:51 -03:00
Andrey Nering
12793c350d chore: delete unused file cmd/tmp/main.go 2025-09-21 16:09:51 -03:00
Andrey Nering
8716ab81be docs: remove ga 2025-09-17 18:39:09 -03:00
Andrey Nering
c2a4e4470b docs: update sprig links to our domain 2025-09-17 18:32:49 -03:00
Valentin Maerten
f5a8ec8a0c fix: changelog in website 2025-09-17 17:12:57 +02:00
Valentin Maerten
048d92709a v3.45.4 2025-09-17 17:05:20 +02:00
Valentin Maerten
8dc9637e7a chore: changelog for #2413, #2424, #2425 2025-09-16 19:36:19 +02:00
Valentin Maerten
700bf00107 fix: search for all taskrc work as expected (#2424) 2025-09-16 19:35:31 +02:00
Valentin Maerten
4836d42828 fix: cache expiry in Taskrc was not working (#2423) 2025-09-16 19:35:12 +02:00
Valentin Maerten
5762d5ef8e fix: autocomplete from subfolder works as expected in zsh shell (#2425) 2025-09-16 19:34:57 +02:00
Andrey Nering
9f2fe0da61 docs: github action is not community maintained anymore 2025-09-16 14:01:33 -03:00
renovate[bot]
d1a5771839 chore(deps): update all non-major dependencies (#2420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 14:19:35 +00:00
renovate[bot]
7663abdcde chore(deps): update mvdan.cc/sh/moreinterp digest to b717ad5 (#2409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 11:14:12 -03:00
Valentin Maerten
1e42e1f817 fix: changelog for website 2025-09-16 08:54:06 +02:00
Valentin Maerten
5f7ae5d32e fix: changelog for website 2025-09-16 08:39:03 +02:00
Pete Davison
17db402e4b v3.45.3 2025-09-15 12:59:29 +00:00
Valentin Maerten
f2242958a6 fix: pnpm install in the website's folder 2025-09-15 14:55:55 +02:00
Pete Davison
ea4b695b5a chore: move changelog items back to unreleased 2025-09-15 12:45:35 +00:00
Pete Davison
209c88c341 v3.45.2 2025-09-15 12:40:54 +00:00
Pete Davison
bd94f9f607 fix: set pnpm version 2025-09-15 12:40:12 +00:00
Pete Davison
9f6b78ec84 chore: move changelog items back to unreleased 2025-09-15 12:38:52 +00:00
Pete Davison
fbde227167 v3.45.1 2025-09-15 12:34:34 +00:00
Pete Davison
fc06e92a87 chore: move changelog items back to unreleased 2025-09-15 12:34:11 +00:00
Pete Davison
a0cab3f5ec fix: use go-task/setup-task instead of arduino/setup-task in CI 2025-09-15 12:30:35 +00:00
Pete Davison
bb4c254211 v3.45.0 2025-09-15 12:17:50 +00:00
Pete Davison
57bf348829 fix: release script 2025-09-15 12:17:28 +00:00
Pete Davison
092b9b6391 chore: update blog post date 2025-09-15 12:16:51 +00:00
Andrey Nering
cd8c831204 chore(website): add umami 2025-09-14 10:18:32 -03:00
Andrey Nering
0d03f4f266 docs(changelog): add entry for #2416 and #2417 2025-09-12 15:46:42 -03:00
Timothy Rule
b8bf298c84 fix: panic for empty hash var ({}) (#2417) 2025-09-12 15:29:40 -03:00
Pete Davison
9a91c4cb21 chore: changelog for the new github action 2025-09-12 14:22:55 +00:00
Pete Davison
2921450bf7 docs: add mise and github actions installation methods (#2414)
* docs: add mise and github actions installation methods

* chore: rename go-task/action to go-task/setup-task
2025-09-11 18:48:23 +01:00
Valentin Maerten
dffa355cad chore: changelog for #1808 2025-09-11 19:47:06 +02:00
Valentin Maerten
48039be12c feat: improve fingerprint, run and output with wildcard (#1808) 2025-09-11 19:33:53 +02:00
Andrey Nering
43cb64e6cc fix: address panic if a config file is not available 2025-09-11 10:02:51 -03:00
Pete Davison
25a7b5936f chore: changelog for #2415 2025-09-11 09:30:02 +00:00
Pete Davison
4ae3071845 feat: nested json (#2415)
* feat: nested json

* feat: remove up_to_date from json output when --no-status flag is set

* feat: restrict use of --nested with --json and --list/--list-all
2025-09-11 10:26:59 +01:00
Valentin Maerten
242523c797 chore: changelog for #2380, #1403 2025-09-10 17:58:33 +02:00
Valentin Maerten
0fdb5e8665 feat: add some config to taskrc.yml (#2389)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2025-09-10 17:57:52 +02:00
renovate[bot]
534dfa089c chore(deps): update all non-major dependencies (#2410) 2025-09-08 10:11:02 -03:00
Valentin Maerten
51a3bcaacd fix: cloudsmith and add docs (#2383) 2025-09-03 19:59:06 +02:00
Andrey Nering
6289fcf34c chore: simplify blog post title 2025-08-29 17:49:40 -03:00
Andrey Nering
2959737d7d chore(changelog): fix typo 2025-08-27 11:39:45 -03:00
Andrey Nering
a3047d3cd8 chore(changelog): add entries for #197 and #2360 2025-08-27 11:37:45 -03:00
Andrey Nering
725600f220 docs: add blog post about the built-in core utilities 2025-08-27 11:29:38 -03:00
Andrey Nering
fd83414074 docs: update docs and faq to mention the new core utils 2025-08-27 11:29:38 -03:00
Andrey Nering
6c645a33f7 feat: add native core utils to improve compatibility on windows 2025-08-27 11:29:38 -03:00
Andrey Nering
9d969e5971 fix(website): remove og:* and twitter:* meta tags for now
Since we're not putting the right page title and description, it's not
really working as expected. It is currently generating the same title
and description for all pages. Removing makes socials at least use the
main `<title>` tag, which will be accurate.
2025-08-26 22:52:23 -03:00
Andrey Nering
8b382a3bae chore(website): taskfile -> task and change emoji 2025-08-26 22:24:21 -03:00
Andrey Nering
a34892ad94 chore: go mod tidy 2025-08-26 20:51:11 -03:00
renovate[bot]
e55bb29554 chore(deps): update all non-major dependencies (#2398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:16:56 -03:00
renovate[bot]
1168ef32df chore(deps): update pnpm to v10 (#2399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 10:04:45 -03:00
Valentin Maerten
245d7f747f chore: use gotestsum for test (#2381) 2025-08-24 18:47:41 +02:00
Ioannis Pinakoulakis
b216ae885c perf: pre-allocate known length arrays (#2354)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
2025-08-23 15:41:30 +01:00
Tatsuya Kyushima
61cb15ad01 chore: delete unnecessary whitespace (#2394) 2025-08-23 15:37:06 +01:00
Pete Davison
04579c0c44 chore: changelog for #2391 2025-08-20 11:22:11 +00:00
Pete Davison
39462cbfde feat: change XDG taskrc naming (#2391) 2025-08-20 12:13:26 +01:00
Valentin Maerten
72dfec68b0 chore: changelog for #2380 2025-08-18 22:50:09 +02:00
Pete Davison
f89c12ddf0 feat: XDG taskrc config (#2380)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2025-08-18 22:43:36 +02:00
renovate[bot]
c903d07332 chore(deps): update all non-major dependencies (#2386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 13:45:13 -03:00
renovate[bot]
138b9a5a4f chore(deps): update actions/checkout action to v5 (#2387)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 18:45:04 +02:00
Valentin Maerten
1e2121a99f chore: changelog for #2235 2025-08-16 13:09:30 +02:00
Valentin Maerten
9495fb2b1c feat: add experiments to taskrc.yml schema (#2235) 2025-08-16 10:54:47 +02:00
Pete Davison
1fda55910e chore: changelog for #2359, #2369, #2371, #2375, #2378, #2358 and #2358 2025-08-15 08:46:16 +00:00
Andrey Nering
e6c808c02b chore(readme): github doesn't like svg images 2025-08-14 13:35:43 -03:00
Valentin Maerten
0fc26a43a9 chore: bump minimun version to 1.24 (#2377) 2025-08-14 18:34:38 +02:00
Andrey Nering
c0b4c19443 chore(readme): fix images 2025-08-14 13:29:40 -03:00
Pete Davison
1a8df44e9e fix: readd environment reference (#2378) 2025-08-14 17:22:04 +01:00
Valentin Maerten
82ad1de8d0 docs: remove wrong <span v-pre> (#2375) 2025-08-14 10:39:46 +02:00
Valentin Maerten
d59c795502 fix: goreleaser with cloudsmith and npm (#2372) 2025-08-13 15:14:57 +02:00
Andrey Nering
504cb94e8b chore(website): add back google analytics 2025-08-12 17:58:44 -03:00
Valentin Maerten
e7606635fe docs: remove padding in team page and fix redirect (#2371) 2025-08-12 21:59:07 +02:00
Valentin Maerten
9a05ceaa80 docs: use Algolia as search engine (#2369) 2025-08-12 18:52:47 +02:00
Valentin Maerten
083654d8c9 build: publish npm package with goreleaser (#2363) 2025-08-12 18:51:20 +02:00
Valentin Maerten
79c93fb42b docs: migrate website to vitepress (#2359)
Co-authored-by: Pete Davison <pd93.uk@outlook.com>
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2025-08-12 18:09:19 +02:00
Valentin Maerten
64fc538a16 build: publish deb and rpm to cloudsmith (#2362) 2025-08-12 15:40:13 +02:00
renovate[bot]
4da081e5c3 chore(deps): update all non-major dependencies (#2364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 09:35:21 -03:00
Valentin Maerten
4bdfe5ce3b fix: publish nightly draft and title (#2358) 2025-08-09 16:03:46 +02:00
Valentin Maerten
26ef693417 chore: publish nightly (#2246)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com>
2025-08-06 20:29:36 +02:00
renovate[bot]
952f32d388 chore(deps): update all non-major dependencies (#2351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 10:51:19 -03:00
Andrey Nering
e72c35f79f fix(goreleaser): fix automatic submission of winget pr 2025-07-28 10:59:45 -03:00
Pete Davison
72991d4f04 v3.44.1 2025-07-23 20:59:38 +00:00
Pete Davison
6f965e3043 chore: changelog for #2265 2025-07-23 20:59:14 +00:00
Pete Davison
1c6d686356 chore: replace PPRemoveAbsolutePaths with generic fixture template data (#2265)
* chore: replace PPRemoveAbsolutePaths with generic fixture template data

* chore: update to goldie v2.7.1
2025-07-23 21:57:25 +01:00
renovate[bot]
dac5aa1954 chore(deps): update all non-major dependencies (#2333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 09:46:14 -03:00
Emil
303bd6ccb2 chore(goreleaser): add section field to deb package (#2331) 2025-07-21 09:45:08 -03:00
renovate[bot]
f736cfaaf1 chore(deps): update all non-major dependencies (#2326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 09:23:53 -03:00
Pete Davison
53f97889bc chore: changelog for #2323 2025-07-09 17:38:54 +00:00
Max Mizikar
fe2da74ea3 fix: don't suggest internal tasks (#2323)
Co-authored-by: Max Mizikar <maxmzkr@gmail.com>
2025-07-09 18:36:40 +01:00
Pete Davison
64fb66895b chore: added changelogs for #2316 and #2322 2025-07-09 15:26:47 +00:00
Pete Davison
d2bd834c81 fix: spaces in path (#2322) 2025-07-09 16:21:42 +01:00
Andrey Nering
8a43ca5d8f chore: move away from deprecated func 2025-07-07 10:14:50 -03:00
Andrey Nering
a10a9faabf chore(taskfile): add go.mod as source for the lint tasks 2025-07-07 10:14:50 -03:00
renovate[bot]
3d3ed0e403 chore(deps): update all non-major dependencies 2025-07-07 10:14:50 -03:00
Pete Davison
47dc87a2c9 fix: remove extra breaking randInt function (#2316) 2025-07-03 23:08:43 +01:00
Pete Davison
3b0a746f85 feat: update go-task/template to latest version 2025-07-03 21:46:09 +00:00
renovate[bot]
281edfe5b3 chore(deps): update all non-major dependencies (#2311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 09:32:44 -03:00
Alexander Kavon
7289ffce0b docs: add macports / freebsd installation instructions (#2308) 2025-06-30 09:31:48 -03:00
dependabot[bot]
61e1af50ff chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 in /website (#2298)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 10:39:42 -03:00
renovate[bot]
715a143735 chore(deps): update all non-major dependencies (#2297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 09:26:27 -03:00
Andrey Nering
a0b1605634 chore: add changelog entry for #2291 2025-06-09 14:12:03 -03:00
Timothy Rule
69fc13bd13 fix(release): fix install script for armv5/6/7 (#2291) 2025-06-09 14:07:11 -03:00
renovate[bot]
b42a52ba77 chore(deps): update all non-major dependencies (#2289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-08 22:09:51 -03:00
Andrey Nering
cb812476b3 v3.44.0 2025-06-08 21:34:13 -03:00
Andrey Nering
b09c6870fe docs: add note about watcher reliability 2025-06-08 21:31:47 -03:00
Andrey Nering
86e4a3aac7 chore(changelog): add entried for watch fixes 2025-06-08 21:19:03 -03:00
Andrey Nering
7782bc92ae fix(watcher): fix some v3.43.x regressions (#2271) 2025-06-08 19:44:08 -03:00
renovate[bot]
9cc2d65091 chore(deps): update all non-major dependencies (#2281) 2025-06-02 13:26:32 +00:00
Andrey Nering
b932e539d9 chore: go mod tidy 2025-05-28 22:08:27 -03:00
Teddy Sommavilla
be45eb04d9 refactor: watchTasks - Chmod operations are already filtered in the Deduper 2025-05-26 16:51:37 -03:00
Teddy Sommavilla
6b878980dc refactor(fsnotifyext): use Event.Has to check for chmod operations
As recommended by the Event.Op godoc. Op is a bitmask, and some systems may send multiple operations at once
2025-05-26 16:51:37 -03:00
Teddy Sommavilla
cd910abd45 doc(fsnotifyext): add godoc for GetChan method 2025-05-26 16:51:37 -03:00
Teddy Sommavilla
6e524bb2fa refactor(fsnotifyext): GetChan should return a receive only chan 2025-05-26 16:51:37 -03:00
Teddy Sommavilla
b4c8f5a0fe refactor(fsnotifyext): handle Deduper timers in own goroutine, avoid mutex use 2025-05-26 16:51:37 -03:00
renovate[bot]
09f85844ba chore(deps): update all non-major dependencies (#2270) 2025-05-26 16:39:01 -03:00
Pete Davison
d54d2ccabc chore: add special variables task to remote for testing 2025-05-24 13:33:55 +00:00
Pete Davison
cf81ab3112 chore: go mod tidy 2025-05-24 13:11:02 +00:00
Pete Davison
aaa7b7772d chore: changelog for #2223 2025-05-24 13:03:29 +00:00
Pete Davison
71eb8cdeea feat: checksum pinning (#2223) 2025-05-24 14:00:02 +01:00
Pete Davison
68ce8b1d84 chore: changelog for #2220 2025-05-24 12:41:31 +00:00
Pete Davison
5323990c72 feat: redact credentials in remote urls (#2220)
* feat: redact credentials in remote urls

* chore: improve function naming

* fix: TaskfileNotSecureError should use redacted URI

* feat: unexport all node implementation fields

* fix: unexport HTTPNode.url
2025-05-24 13:38:18 +01:00
Pete Davison
ec4e68d601 chore: changelog for #2256 2025-05-20 20:40:28 +00:00
Aleksander Sh.
bb5b045293 feat: add task name to json output (#2256) 2025-05-20 21:37:57 +01:00
renovate[bot]
89f29cb75b chore(deps): update all non-major dependencies (#2260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 13:38:05 +02:00
Andrey Nering
da4ce5b0a5 fix(expand): return nothing if there are no matches 2025-05-09 15:55:52 -03:00
renovate[bot]
fb68a5f79a chore(deps): update golangci/golangci-lint-action action to v8 (#2237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2025-05-06 20:45:06 +02:00
renovate[bot]
f40f389cb4 chore(deps): update all non-major dependencies (#2236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 20:43:16 +02:00
Valentin Maerten
a459eeaabb chore: changelog for #2233 2025-05-03 17:18:27 +02:00
Valentin Maerten
84f02a822f docs: mention that method key is allowed at root level (#2233) 2025-05-03 17:17:11 +02:00
Valentin Maerten
55d1aa260d chore: changelog for #2211 2025-05-03 17:12:31 +02:00
Valentin Maerten
e7084cdf26 chore: update schemas only when a release is done (#2211) 2025-05-03 17:11:56 +02:00
Pete Davison
ca55e9b621 chore: changelog for #2225 2025-05-01 17:58:47 +00:00
Pete Davison
6528b36caa feat: add uuid and rand number functions (#2225)
* feat: add uuid and rand number functions

* chore: remove randFloat for now
2025-05-01 17:58:01 +00:00
Pete Davison
f8736c5f77 chore: changelog for #2140 2025-05-01 17:51:47 +00:00
Pete Davison
6896accf86 feat: cli args list (#2140) 2025-05-01 18:43:43 +01:00
Pete Davison
c12ed49acb chore: remove unused any2 testdata 2025-04-28 21:04:24 +00:00
Pete Davison
d1bfd3e9f7 docs: move yaml templating functions to the correct section 2025-04-28 20:57:18 +00:00
renovate[bot]
fc17343fcc chore(deps): update all non-major dependencies (#2214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 14:37:44 +02:00
Pete Davison
d3e9be1520 chore: changelog for #2219 2025-04-28 12:21:26 +00:00
Pete Davison
d850d03c96 feat: add yaml templating functions (#2219)
* feat: add yaml templating functions

* docs: add yaml functions to templating reference

* refactor: remove some unnecessary function wrappers
2025-04-28 12:19:56 +00:00
Pete Davison
0058f18676 chore: changelog for #2216 2025-04-28 12:05:10 +00:00
Pete Davison
b3c4007756 fix: double escaped paths (#2216) 2025-04-28 13:02:46 +01:00
Pete Davison
9e8fd54be9 chore: changelog for #2200 2025-04-27 23:02:32 +00:00
Valentin Maerten
a33544101a fix: fuzzy model was not instanciated (#2200)
* fix: fuzzy model was not instanciated

* add test

* add test
2025-04-28 00:00:54 +01:00
Pete Davison
1c35358fcc v3.43.3 2025-04-27 22:29:34 +00:00
Pete Davison
13daa6dc35 feat: formatting with golangci-lint and gci 2025-04-27 22:28:42 +00:00
Pete Davison
20c1ffe098 docs: update variables example so that it doesn't error 2025-04-27 22:26:59 +00:00
Pete Davison
bd8ccb8d03 chore: changelogs for reverts 2025-04-27 22:26:29 +00:00
Pete Davison
8162b05f59 Revert "feat: process variables in include vars (#2113)"
This reverts commit f0414f162d.
2025-04-27 22:15:49 +00:00
Pete Davison
68d5095761 Revert "fix: .USER_WORKING_DIR should contain the value of --dir if given (#2186)"
This reverts commit 768dca053b.
2025-04-27 22:14:50 +00:00
Andrey Nering
6cb0a5a2f2 v3.43.2 2025-04-21 16:35:01 -03:00
Andrey Nering
08056924e0 chore: add changelog entry for #2191 2025-04-21 16:33:30 -03:00
Valentin Maerten
39706105e1 fix: CLI_ARGS is a string and not an array (#2191) 2025-04-21 16:31:18 -03:00
Andrey Nering
bf4e7960cb chore: show right version on changelog 2025-04-21 14:31:25 -03:00
Andrey Nering
3d36616e9e v3.43.1 2025-04-21 13:57:43 -03:00
Andrey Nering
3976e8372a chore: move the experiments package out of the internal/ dir
Closes #2014
2025-04-21 13:55:56 -03:00
283 changed files with 17045 additions and 19456 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,46 +13,31 @@ jobs:
name: Lint
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v8
with:
version: v2.0.2
version: v2.1.0
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: 3.12
python-version: 3.14
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/static/schema.json
check_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v46
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.')
run: check-jsonschema --check-metaschema website/src/public/schema.json

30
.github/workflows/release-nightly.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Release nightly
on:
workflow_dispatch:
schedule:
- cron: 0 0 * * *
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
args: release --clean --nightly -f .goreleaser-nightly.yml
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}

View File

@@ -10,21 +10,39 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.24.x
go-version: 1.25.x
- name: npm-login
run: |
npm config set '//registry.npmjs.org/:_authToken'=${{ secrets.NPM_TOKEN }}
- name: Install Task
uses: go-task/setup-task@v1
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
package_json_file: 'website/package.json'
run_install: 'true'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser-pro
version: latest
args: release --clean
args: release --clean --draft
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
- name: Deploy Website
shell: bash
run: |
task website:deploy:prod

View File

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

View File

@@ -5,8 +5,10 @@ formatters:
- gofmt
- gofumpt
- goimports
- gci
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
@@ -15,6 +17,12 @@ formatters:
goimports:
local-prefixes:
- github.com/go-task
gci:
sections:
- standard
- default
- prefix(github.com/go-task)
- localmodule
exclusions:
generated: lax
paths:

15
.goreleaser-nightly.yml Normal file
View File

@@ -0,0 +1,15 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
pro: true
release:
name_template: 'v{{.Version}}'
nightly:
publish_release: true
keep_single_release: true
version_template: "{{incminor .Version}}-nightly"
includes:
- from_file:
path: ./.goreleaser.yml

View File

@@ -30,13 +30,14 @@ builds:
flags:
- -trimpath
ldflags:
- -s -w # Don't set main.version.
- "-s -w"
- "{{if .IsNightly}}-X github.com/go-task/task/v3/internal/version.version={{.Version}}{{end}}"
gomod:
proxy: true
archives:
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
files:
- README.md
- LICENSE
@@ -45,27 +46,30 @@ archives:
- goos: windows
formats: [zip]
release:
draft: true
git:
ignore_tags:
- "{{if not .IsNightly}}nightly{{end}}"
snapshot:
version_template: "{{.Version}}"
version_template: '{{.Version}}'
checksum:
name_template: "task_checksums.txt"
name_template: 'task_checksums.txt'
nfpms:
- vendor: Task
homepage: https://taskfile.dev
maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go
section: golang
license: MIT
conflicts:
- taskwarrior
formats:
- deb
- rpm
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
- apk
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
contents:
- src: completion/bash/task.bash
dst: /etc/bash_completion.d/task
@@ -83,8 +87,7 @@ brews:
repository:
owner: go-task
name: homebrew-tap
test:
system "#{bin}/task", "--help"
test: system "#{bin}/task", "--help"
install: |-
bin.install "task"
bash_completion.install "completion/bash/task.bash" => "task"
@@ -107,7 +110,7 @@ winget:
commit_author:
name: task-bot
email: 106601941+task-bot@users.noreply.github.com
commit_msg_template: "chore: bump {{.PackageIdentifier}} to {{.Tag}}"
commit_msg_template: 'chore: release {{.PackageIdentifier}} {{.Tag}}'
release_notes_url: https://github.com/go-task/task/releases/tag/{{.Tag}}
tags:
- build
@@ -121,13 +124,49 @@ winget:
- task-runner
- taskfile
- tool
skip_upload: true
repository:
owner: microsoft
owner: go-task
name: winget-pkgs
branch: 'chore/task-{{.Version}}'
pull_request:
enabled: true
draft: false
check_boxes: true
base:
owner: go-task
owner: microsoft
name: winget-pkgs
branch: "bump-task-to-{{.Tag}}"
branch: master
npms:
- name: "@go-task/cli"
repository: "git+https://github.com/go-task/task.git"
bugs: https://github.com/go-task/task/issues
description: A task runner / simpler Make alternative written in Go
homepage: https://taskfile.dev
license: MIT
author: "The Task authors"
access: public
keywords:
- "task"
- "taskfile"
- "build-tool"
- "task-runner"
cloudsmiths:
- organization: "task"
repository: "{{if not .IsNightly}}task{{end}}"
formats:
- deb
- rpm
- apk
distributions:
deb:
- "any-distro/any-version"
rpm:
- "any-distro/any-version"
alpine:
- "alpine/any-version"
component: main
republish: true

1
.nvmrc
View File

@@ -1 +0,0 @@
22.14.0

View File

@@ -1,15 +1,12 @@
{
"yaml.schemas": {
"./website/static/schema.json": [
"Taskfile.yml",
"tmp/**/*.yml"
"./website/src/public/schema.json": [
"Taskfile.yml",
"Taskfile.yaml",
"taskfile.yml",
"taskfile.yaml"
]
},
"search.exclude": {
"**/versioned_docs": true,
"**/versioned_sidesbars": true,
"**/i18n": true
},
"gopls": {
"formatting.local": "github.com/go-task"
},

View File

@@ -1,6 +1,145 @@
# Changelog
## v3.43.0 - 2025-04-21
## v3.45.5 - 2025-11-11
- Fixed bug that made a generic message, instead of an useful one, appear when a
Taskfile could not be found (#2431 by @andreynering).
- Fixed a bug that caused an error when including a Remote Git Taskfile (#2438
by @twelvelabs).
- Fixed issue where `.taskrc.yml` was not returned if reading it failed, and
corrected handling of remote entrypoint Taskfiles (#2460, #2461 by @vmaerten).
- Improved performance of `--list` and `--list-all` by introducing a faster
compilation method that skips source globbing and checksum updates (#1322,
#2053 by @vmaerten).
- Fixed a concurrency bug with `output: group`. This ensures that begin/end
parts won't be mixed up from different tasks (#1208, #2349, #2350 by
@trulede).
- Do not re-evaluate variables for `defer:` (#2244, #2418 by @trulede).
- Improve error message when a Taskfile is not found (#2441, #2494 by @vmaerten).
- Fixed generic error message `exit status 1` when a dependency task failed
(#2286 by @GrahamDennis).
- Fixed YAML library from the unmaintained `gopkg.in/yaml.v3` to the new fork
maintained by the official YAML org (#2171, #2434 by @andreynering).
- On Windows, the built-in version of the `rm` core utils contains a fix related
to the `-f` flag (#2426,
[u-root/u-root#3464](https://github.com/u-root/u-root/pull/3464),
[mvdan/sh#1199](https://github.com/mvdan/sh/pull/1199),
#2506 by @andreynering).
## v3.45.4 - 2025-09-17
- Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423
by @vmaerten).
- Fixed a bug where `.taskrc.yml` files in parent folders were not read
correctly (#2424 by @vmaerten).
- Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by
@vmaerten).
## v3.45.3 - 2025-09-15
- Task now includes built-in core utilities to greatly improve compatibility on
Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any
other common core utility will now work by default on Windows, without extra
setup. This is something we wanted to address for many many years, and it's
finally being shipped!
[Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils).
(#197, #2360 by @andreynering).
- :sparkles: Built and deployed a [brand new website](https://taskfile.dev)
using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by
@vmaerten, @andreynering, @pd93).
- Began releasing
[nightly builds](https://github.com/go-task/task/releases/tag/nightly). This
will allow people to test our changes before they are fully released and
without having to install Go to build them (#2358 by @vmaerten).
- Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or
`$HOME/.taskrc.yml`. Check out our new
[configuration guide](https://taskfile.dev/docs/reference/config) for more
details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93).
- Added experiments to the taskrc schema to clarify the expected keys and values
(#2235 by @vmaerten).
- Added support for new properties in `.taskrc.yml`: insecure, verbose,
concurrency, remote offline, remote timeout, and remote expiry. :warning:
Note: setting offline via environment variable is no longer supported. (#2389
by @vmaerten)
- Added a `--nested` flag when outputting tasks using `--list --json`. This will
output tasks in a nested structure when tasks are namespaced (#2415 by @pd93).
- Enhanced support for tasks with wildcards: they are now logged correctly, and
wildcard parameters are fully considered during fingerprinting (#1808, #1795
by @vmaerten).
- Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417
by @trulede).
#### Package API
- Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten).
#### Other news
We recently released our
[official GitHub Action](https://github.com/go-task/setup-task). This is based
on the fantastic work by the Arduino team who created and maintained the
community version. Now that this is officially adopted, fixes/updates should be
more timely. We have already merged a couple of longstanding PRs in our
[first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by
@pd93, @shrink, @trim21 and all the previous contributors to
[arduino/setup-task](https://github.com/arduino/setup-task/)).
## v3.45.0-v3.45.2 - 2025-09-15
Failed due to an issue with our release process.
## v3.44.1 - 2025-07-23
- Internal tasks will no longer be shown as suggestions since they cannot be
called (#2309, #2323 by @maxmzkrcensys)
- Fixed install script for some ARM platforms (#1516, #2291 by @trulede).
- Fixed a regression where fingerprinting was not working correctly if the path
to you Taskfile contained a space (#2321, #2322 by @pd93).
- Reverted a breaking change to `randInt` (#2312, #2316 by @pd93).
- Made new variables `TEST_NAME` and `TEST_DIR` available in fixture tests
(#2265 by @pd93).
## v3.44.0 - 2025-06-08
- Added `uuid`, `randInt` and `randIntN` template functions (#1346, #2225 by
@pd93).
- Added new `CLI_ARGS_LIST` array variable which contains the arguments passed
to Task after the `--` (the same as `CLI_ARGS`, but an array instead of a
string). (#2138, #2139, #2140 by @pd93).
- Added `toYaml` and `fromYaml` templating functions (#2217, #2219 by @pd93).
- Added `task` field the `--list --json` output (#2256 by @aleksandersh).
- Added the ability to
[pin included taskfiles](https://taskfile.dev/next/experiments/remote-taskfiles/#manual-checksum-pinning)
by specifying a checksum. This works with both local and remote Taskfiles
(#2222, #2223 by @pd93).
- When using the
[Remote Taskfiles experiment](https://github.com/go-task/task/issues/1317),
any credentials used in the URL will now be redacted in Task's output (#2100,
#2220 by @pd93).
- Fixed fuzzy suggestions not working when misspelling a task name (#2192, #2200
by @vmaerten).
- Fixed a bug where taskfiles in directories containing spaces created
directories in the wrong location (#2208, #2216 by @pd93).
- Added support for dual JSON schema files, allowing changes without affecting
the current schema. The current schemas will only be updated during releases.
(#2211 by @vmaerten).
- Improved fingerprint documentation by specifying that the method can be set at
the root level to apply to all tasks (#2233 by @vmaerten).
- Fixed some watcher regressions after #2048 (#2199, #2202, #2241, #2196 by
@wazazaby, #2271 by @andreynering).
## v3.43.3 - 2025-04-27
Reverted the changes made in #2113 and #2186 that affected the
`USER_WORKING_DIR` and built-in variables. This fixes #2206, #2195, #2207 and
#2208.
## v3.43.2 - 2025-04-21
- Fixed regresion of `CLI_ARGS` being exposed as the wrong type (#2190, #2191 by
@vmaerten).
## v3.43.1 - 2025-04-21
- Significant improvements were made to the watcher. We migrated from
[watcher](https://github.com/radovskyb/watcher) to

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://taskfile.dev">
<img src="website/static/img/logo.svg" width="200px" height="200px" />
<img src="website/src/public/img/logo.svg" width="200px" height="200px" />
</a>
<h1>Task</h1>
@@ -19,7 +19,12 @@
<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" />
<img src="website/src/public/img/devowl.io.svg" height="100px" width="200px" title="devowl.io" />
</a>
</td>
<td align="center" valign="middle">
<a target="_blank" href="https://magic.dev/">
<img src="website/src/public/img/magic.png" height="100px" width="200px" title="Magic" />
</a>
</td>
</tr>

View File

@@ -8,6 +8,7 @@ includes:
vars:
BIN: "{{.ROOT_DIR}}/bin"
GOTESTSUM_FORMAT: '{{if .CI}}github-actions{{else}}pkgname{{end}}'
env:
CGO_ENABLED: '0'
@@ -53,9 +54,12 @@ tasks:
generate:fixtures:
desc: Runs tests and generates golden fixture files
aliases: [gen:fixtures, g:fixtures]
env:
GOLDIE_UPDATE: 'true'
GOLDIE_TEMPLATE: 'true'
cmds:
- find ./testdata -name '*.golden' -delete
- go test -update ./...
- go test ./...
install:mockery:
desc: Installs mockgen; a tool to generate mock files
@@ -87,6 +91,7 @@ tasks:
sources:
- './**/*.go'
- .golangci.yml
- go.mod
cmds:
- golangci-lint run
@@ -95,9 +100,19 @@ tasks:
sources:
- './**/*.go'
- .golangci.yml
- go.mod
cmds:
- golangci-lint run --fix
format:
desc: Runs golangci-lint and formats any Go files
aliases: [fmt, f]
sources:
- './**/*.go'
- .golangci.yml
cmds:
- golangci-lint fmt
sleepit:build:
desc: Builds the sleepit test helper
sources:
@@ -117,29 +132,37 @@ tasks:
test:
desc: Runs test suite
aliases: [t]
deps: [gotestsum:install]
sources:
- "**/*.go"
- "testdata/**/*"
cmds:
- go test ./...
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./...
test:watch:
desc: Runs test suite with watch tests included
deps: [sleepit:build]
deps: [sleepit:build, gotestsum:install]
cmds:
- go test ./... -tags 'watch'
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... -tags 'watch'
test:all:
desc: Runs test suite with signals and watch tests included
deps: [sleepit:build]
deps: [sleepit:build, gotestsum:install]
cmds:
- go test -tags 'signals watch' ./...
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
goreleaser:test:
desc: Tests release process without publishing
cmds:
- goreleaser --snapshot --clean
gotestsum:install:
desc: Installs gotestsum
status:
- command -v gotestsum
cmds:
- go install gotest.tools/gotestsum@latest
goreleaser:install:
desc: Installs goreleaser
cmds:
@@ -189,7 +212,6 @@ tasks:
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"
@@ -208,8 +230,3 @@ tasks:
- "git push origin tag v{{.VERSION}}"
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
silent: true
npm:publish:
desc: Publish release to npm
cmds:
- npm publish --access=public

View File

@@ -20,17 +20,7 @@ func Get() ([]string, []string, error) {
if doubleDashPos == -1 {
return args, nil, nil
}
var quotedCliArgs []string
for _, arg := range args[doubleDashPos:] {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return nil, nil, err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return args[:doubleDashPos], quotedCliArgs, nil
return args[:doubleDashPos], args[doubleDashPos:], nil
}
// Parse parses command line argument: tasks and global variables
@@ -51,6 +41,18 @@ func Parse(args ...string) ([]*task.Call, *ast.Vars) {
return calls, globals
}
func ToQuotedString(args []string) (string, error) {
var quotedCliArgs []string
for _, arg := range args {
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
if err != nil {
return "", err
}
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
}
return strings.Join(quotedCliArgs, " "), nil
}
func splitVar(s string) (string, string) {
pair := strings.SplitN(s, "=", 2)
return pair[0], pair[1]

View File

@@ -3,13 +3,11 @@ package main
import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/Masterminds/semver/v3"
"github.com/otiai10/copy"
"github.com/spf13/pflag"
"github.com/go-task/task/v3/errors"
@@ -17,15 +15,11 @@ import (
const (
changelogSource = "CHANGELOG.md"
changelogTarget = "website/docs/changelog.mdx"
docsSource = "website/docs"
docsTarget = "website/versioned_docs/version-latest"
changelogTarget = "website/src/docs/changelog.md"
versionFile = "internal/version/version.txt"
)
var (
changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`)
)
var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
// Flags
var (
@@ -49,7 +43,7 @@ func release() error {
return errors.New("error: expected version number")
}
version, err := getVersion()
version, err := getVersion(versionFile)
if err != nil {
return err
}
@@ -67,32 +61,18 @@ func release() error {
return err
}
if err := setVersionFile("internal/version/version.txt", version); err != nil {
return err
}
if err := setJSONVersion("package.json", version); err != nil {
return err
}
if err := setJSONVersion("package-lock.json", version); err != nil {
return err
}
if err := docs(); err != nil {
if err := setVersionFile(versionFile, version); err != nil {
return err
}
return nil
}
func getVersion() (*semver.Version, error) {
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
b, err := cmd.Output()
func getVersion(filename string) (*semver.Version, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return semver.NewVersion(strings.TrimSpace(string(b)))
}
@@ -151,27 +131,3 @@ func changelog(version *semver.Version) error {
func setVersionFile(fileName string, version *semver.Version) error {
return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644)
}
func setJSONVersion(fileName string, version *semver.Version) error {
// Read the JSON file
b, err := os.ReadFile(fileName)
if err != nil {
return err
}
// Replace the version
new := versionRegex.ReplaceAllString(string(b), fmt.Sprintf(` "version": "%s",`, version.String()))
// Write the JSON file
return os.WriteFile(fileName, []byte(new), 0o644)
}
func docs() error {
if err := os.RemoveAll(docsTarget); err != nil {
return err
}
if err := copy.Copy(docsSource, docsTarget); err != nil {
return err
}
return nil
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
@@ -76,7 +76,7 @@ func run() error {
if err != nil {
return err
}
_, args, err := args.Get()
args, _, err := args.Get()
if err != nil {
return err
}
@@ -128,6 +128,7 @@ func run() error {
flags.ListAll,
flags.ListJson,
flags.NoStatus,
flags.Nested,
)
if listOptions.ShouldListTasks() {
if flags.Silent {
@@ -144,18 +145,23 @@ func run() error {
}
// Parse the remaining arguments
argv, cliArgs, err := args.Get()
cliArgsPreDash, cliArgsPostDash, err := args.Get()
if err != nil {
return err
}
calls, globals := args.Parse(argv...)
calls, globals := args.Parse(cliArgsPreDash...)
// If there are no calls, run the default task instead
if len(calls) == 0 {
calls = append(calls, &task.Call{Task: "default"})
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
if err != nil {
return err
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
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})

View File

@@ -1,38 +0,0 @@
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
if err := run(ctx); err != nil {
fmt.Println(ctx.Err())
fmt.Println(err)
}
}
func run(ctx context.Context) error {
req, err := http.NewRequest("GET", "https://taskfile.dev/schema.json", nil)
if err != nil {
fmt.Println(1)
return err
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
if ctx.Err() != nil {
fmt.Println(2)
return err
}
fmt.Println(3)
return err
}
defer resp.Body.Close()
return nil
}

View File

@@ -16,21 +16,18 @@ function __task_list() {
if [[ -n "$taskfile" && -f "$taskfile" ]]; then
enabled=1
cmd+=(--taskfile "$taskfile")
else
for taskfile in {T,t}askfile{,.dist}.{yaml,yml}; do
if [[ -f "$taskfile" ]]; then
enabled=1
break
fi
done
fi
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
enabled=1
fi
(( enabled )) || return 0
scripts=()
for item in "${(@)${(f)$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION)}[2,-1]#\* }"; do
for item in "${(@)${(f)output}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )

View File

@@ -5,15 +5,12 @@ import (
"cmp"
"errors"
"fmt"
"regexp"
"strings"
"github.com/fatih/color"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
)
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
type (
TaskfileDecodeError struct {
Message string
@@ -53,10 +50,10 @@ func (err *TaskfileDecodeError) Error() string {
if len(te.Errors) > 1 {
fmt.Fprintln(buf, color.RedString("errs:"))
for _, message := range te.Errors {
fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
fmt.Fprintln(buf, color.RedString("- %s", message.Err.Error()))
}
} else {
fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0].Err.Error()))
}
} else {
// Otherwise print the error message normally
@@ -128,11 +125,3 @@ func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *T
err.Snippet = snippet
return err
}
func extractTypeErrorMessage(message string) string {
matches := typeErrorRegex.FindStringSubmatch(message)
if len(matches) == 2 {
return matches[1]
}
return message
}

View File

@@ -26,6 +26,7 @@ const (
CodeTaskfileNetworkTimeout
CodeTaskfileInvalid
CodeTaskfileCycle
CodeTaskfileDoesNotMatchChecksum
)
// Task related exit codes

View File

@@ -1,6 +1,7 @@
package errors
import (
"errors"
"fmt"
"strings"
@@ -46,12 +47,17 @@ func (err *TaskRunError) Code() int {
}
func (err *TaskRunError) TaskExitCode() int {
if c, ok := interp.IsExitStatus(err.Err); ok {
return int(c)
var exit interp.ExitStatus
if errors.As(err.Err, &exit) {
return int(exit)
}
return err.Code()
}
func (err *TaskRunError) Unwrap() error {
return err.Err
}
// TaskInternalError when the user attempts to invoke a task that is internal.
type TaskInternalError struct {
TaskName string
@@ -160,7 +166,7 @@ func (v MissingVar) String() string {
}
func (err *TaskMissingRequiredVarsError) Error() string {
var vars []string
vars := make([]string, 0, len(err.MissingVars))
for _, v := range err.MissingVars {
vars = append(vars, v.String())
}

View File

@@ -11,14 +11,18 @@ import (
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when
// searching the filesystem.
type TaskfileNotFoundError struct {
URI string
Walk bool
URI string
Walk bool
AskInit bool
}
func (err TaskfileNotFoundError) Error() string {
var walkText string
if err.Walk {
walkText = " (or any of the parent directories)"
walkText = " (or any of the parent directories)."
}
if err.AskInit {
walkText += " Run `task --init` to create a new Taskfile."
}
return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
}
@@ -187,3 +191,24 @@ func (err TaskfileCycleError) Error() string {
func (err TaskfileCycleError) Code() int {
return CodeTaskfileCycle
}
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
// match the one pinned in the parent Taskfile.
type TaskfileDoesNotMatchChecksum struct {
URI string
ExpectedChecksum string
ActualChecksum string
}
func (err *TaskfileDoesNotMatchChecksum) Error() string {
return fmt.Sprintf(
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
err.URI,
err.ActualChecksum,
err.ExpectedChecksum,
)
}
func (err *TaskfileDoesNotMatchChecksum) Code() int {
return CodeTaskfileDoesNotMatchChecksum
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"io"
"os"
"path/filepath"
"sync"
"time"
@@ -123,7 +122,6 @@ type dirOption struct {
}
func (o *dirOption) ApplyToExecutor(e *Executor) {
e.UserWorkingDir, _ = filepath.Abs(o.dir)
e.Dir = o.dir
}
@@ -242,7 +240,7 @@ func (o *timeoutOption) ApplyToExecutor(e *Executor) {
}
// WithCacheExpiryDuration sets the duration after which the cache is considered
// expired. By default, the cache is considered expired after 24 hours.
// expired. By default, the cache is 0 (disabled).
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration}
}

View File

@@ -3,7 +3,6 @@ package task_test
import (
"bytes"
"cmp"
"context"
"fmt"
"os"
"path/filepath"
@@ -13,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -50,7 +49,8 @@ func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
task: "default",
vars: map[string]any{},
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
},
}
// Apply the functional options
@@ -143,12 +143,12 @@ func (tt *ExecutorTest) run(t *testing.T) {
t.Helper()
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
var buffer SyncBuffer
opts := append(
tt.executorOpts,
task.WithStdout(&buf),
task.WithStderr(&buf),
task.WithStdout(&buffer),
task.WithStderr(&buffer),
)
// If the test has input, create a reader for it and add it to the
@@ -171,7 +171,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
return
} else {
require.NoError(t, err)
@@ -188,11 +188,11 @@ func (tt *ExecutorTest) run(t *testing.T) {
}
// Run the task and check for errors
ctx := context.Background()
ctx := t.Context()
if err := e.Run(ctx, call); tt.wantRunError {
require.Error(t, err)
tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
return
} else {
require.NoError(t, err)
@@ -205,7 +205,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
}
}
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
}
// Run the test (with a name if it has one)
@@ -232,7 +232,7 @@ func TestEmptyTaskfile(t *testing.T) {
task.WithDir("testdata/empty_taskfile"),
),
WithSetupError(),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
)
}
@@ -367,7 +367,7 @@ func TestSpecialVars(t *testing.T) {
task.WithVersionCheck(true),
),
WithTask(test),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
)
}
}
@@ -551,7 +551,7 @@ func TestStatus(t *testing.T) {
task.WithVerbose(true),
),
WithTask("gen-silent-baz"),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
)
}
@@ -665,6 +665,15 @@ func TestLabel(t *testing.T) {
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("label in error"),
WithExecutorOptions(
task.WithDir("testdata/label_error"),
),
WithTask("foo"),
WithRunError(),
)
}
func TestPromptInSummary(t *testing.T) {
@@ -777,7 +786,7 @@ func TestForCmds(t *testing.T) {
task.WithForce(true),
),
WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
}
if test.wantErr {
opts = append(opts, WithRunError())
@@ -822,7 +831,7 @@ func TestForDeps(t *testing.T) {
task.WithOutputStyle(ast.Output{Name: "group"}),
),
WithTask(test.name),
WithPostProcessFn(PPRemoveAbsolutePaths),
WithFixtureTemplating(),
WithPostProcessFn(PPSortedLines),
}
if test.wantErr {
@@ -937,3 +946,53 @@ func TestVarInheritance(t *testing.T) {
)
}
}
func TestFuzzyModel(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("instal"),
WithRunError(),
)
NewExecutorTest(t,
WithName("not-fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("install"),
)
NewExecutorTest(t,
WithName("intern"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("intern"),
WithRunError(),
)
}
func TestIncludeChecksum(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("correct"),
WithExecutorOptions(
task.WithDir("testdata/includes_checksum/correct"),
),
)
NewExecutorTest(t,
WithName("incorrect"),
WithExecutorOptions(
task.WithDir("testdata/includes_checksum/incorrect"),
),
WithSetupError(),
WithFixtureTemplating(),
)
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskrc/ast"
)

View File

@@ -9,6 +9,7 @@ import (
"github.com/joho/godotenv"
"github.com/go-task/task/v3/taskrc"
"github.com/go-task/task/v3/taskrc/ast"
)
const envPrefix = "TASK_X_"
@@ -31,16 +32,13 @@ var (
var xList []Experiment
func Parse(dir string) {
config, _ := taskrc.GetConfig(dir)
ParseWithConfig(dir, config)
}
func ParseWithConfig(dir string, config *ast.TaskRC) {
// Read any .env files
readDotEnv(dir)
// Create a node for the Task config reader
node, _ := taskrc.NewNode("", dir)
// Read the Task config file
reader := taskrc.NewReader()
config, _ := reader.Read(node)
// Initialize the experiments
GentleForce = New("GENTLE_FORCE", config, 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)

View File

@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -44,7 +44,8 @@ func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
task: "default",
vars: map[string]any{},
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
},
}
// Apply the functional options
@@ -218,3 +219,17 @@ func TestListDescInterpolation(t *testing.T) {
}),
)
}
func TestJsonListFormat(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithExecutorOptions(
task.WithDir("testdata/json_list_format"),
),
WithListOptions(task.ListOptions{
FormatTaskListAsJSON: true,
}),
WithFixtureTemplating(),
)
}

41
go.mod
View File

@@ -1,11 +1,11 @@
module github.com/go-task/task/v3
go 1.23.0
go 1.24.0
require (
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.3.1
github.com/alecthomas/chroma/v2 v2.16.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/dominikbraun/graph v0.23.0
@@ -13,22 +13,23 @@ require (
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.0
github.com/go-git/go-git/v5 v5.16.3
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.1.0
github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.5.5
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/sebdah/goldie/v2 v2.8.0
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.13.0
golang.org/x/term v0.31.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.11.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/sync v0.18.0
golang.org/x/term v0.37.0
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b
mvdan.cc/sh/v3 v3.12.0
)
require (
@@ -38,23 +39,29 @@ require (
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // 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-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pjbgf/sha1cd v0.3.2 // 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.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

116
go.sum
View File

@@ -2,31 +2,25 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA=
github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
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/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/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.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@@ -36,12 +30,12 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
@@ -50,8 +44,6 @@ 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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -62,22 +54,20 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
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.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@@ -86,8 +76,12 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
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/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -104,10 +98,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
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.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -121,46 +113,48 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY=
github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
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.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da h1:Vst9Tvq3G6f6pYBvxy7coi2arDsnOZ3Mkj8MkNarSK8=
github.com/u-root/u-root v0.15.1-0.20251014130006-62f7144b33da/go.mod h1:R49zft13memK20EgFAvmTbXBS0t29UvglnM0BCA1ldQ=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
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/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
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/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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=
@@ -170,18 +164,18 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -193,5 +187,7 @@ 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b h1:vTpx76nZDTP/BAGnnhEXYjM+8nPKe9+I86qCErBvjCw=
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b/go.mod h1:bDyKbUYKqkFunWmxxuSPrkYpln9QZcUsqu7W128qYW4=
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

64
help.go
View File

@@ -24,15 +24,17 @@ type ListOptions struct {
ListAllTasks bool
FormatTaskListAsJSON bool
NoStatus bool
Nested bool
}
// NewListOptions creates a new ListOptions instance
func NewListOptions(list, listAll, listAsJson, noStatus bool) ListOptions {
func NewListOptions(list, listAll, listAsJson, noStatus, nested bool) ListOptions {
return ListOptions{
ListOnlyTasksWithDescriptions: list,
ListAllTasks: listAll,
FormatTaskListAsJSON: listAsJson,
NoStatus: noStatus,
Nested: nested,
}
}
@@ -63,7 +65,7 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
return false, err
}
if o.FormatTaskListAsJSON {
output, err := e.ToEditorOutput(tasks, o.NoStatus)
output, err := e.ToEditorOutput(tasks, o.NoStatus, o.Nested)
if err != nil {
return false, err
}
@@ -135,32 +137,17 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
return nil
}
func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Taskfile, error) {
o := &editors.Taskfile{
Tasks: make([]editors.Task, len(tasks)),
Location: e.Taskfile.Location,
}
func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool) (*editors.Namespace, error) {
var g errgroup.Group
editorTasks := make([]editors.Task, len(tasks))
// Look over each task in parallel and turn it into an editor task
for i := range tasks {
aliases := []string{}
if len(tasks[i].Aliases) > 0 {
aliases = tasks[i].Aliases
}
g.Go(func() error {
o.Tasks[i] = editors.Task{
Name: tasks[i].Name(),
Desc: tasks[i].Desc,
Summary: tasks[i].Summary,
Aliases: aliases,
UpToDate: false,
Location: &editors.Location{
Line: tasks[i].Location.Line,
Column: tasks[i].Location.Column,
Taskfile: tasks[i].Location.Taskfile,
},
}
editorTask := editors.NewTask(tasks[i])
if noStatus {
editorTasks[i] = editorTask
return nil
}
@@ -179,10 +166,35 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
return err
}
o.Tasks[i].UpToDate = upToDate
editorTask.UpToDate = &upToDate
editorTasks[i] = editorTask
return nil
})
}
return o, g.Wait()
if err := g.Wait(); err != nil {
return nil, err
}
// Create the root namespace
var tasksLen int
if !nested {
tasksLen = len(editorTasks)
}
rootNamespace := &editors.Namespace{
Tasks: make([]editors.Task, tasksLen),
Location: e.Taskfile.Location,
}
// Recursively add namespaces to the root namespace or if nesting is
// disabled add them all to the root namespace
for i, task := range editorTasks {
taskNamespacePath := strings.Split(task.Task, ast.NamespaceSeparator)
if nested {
rootNamespace.AddNamespace(taskNamespacePath, task)
} else {
rootNamespace.Tasks[i] = task
}
}
return rootNamespace, g.Wait()
}

View File

@@ -64,21 +64,15 @@ get_binaries() {
case "$PLATFORM" in
darwin/amd64) BINARIES="task" ;;
darwin/arm64) BINARIES="task" ;;
darwin/armv5) BINARIES="task" ;;
darwin/armv6) BINARIES="task" ;;
darwin/armv7) BINARIES="task" ;;
darwin/arm) BINARIES="task" ;;
linux/386) BINARIES="task" ;;
linux/amd64) BINARIES="task" ;;
linux/arm64) BINARIES="task" ;;
linux/armv5) BINARIES="task" ;;
linux/armv6) BINARIES="task" ;;
linux/armv7) BINARIES="task" ;;
linux/arm) BINARIES="task" ;;
windows/386) BINARIES="task" ;;
windows/amd64) BINARIES="task" ;;
windows/arm64) BINARIES="task" ;;
windows/armv5) BINARIES="task" ;;
windows/armv6) BINARIES="task" ;;
windows/armv7) BINARIES="task" ;;
windows/arm) BINARIES="task" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1

View File

@@ -1,18 +1,24 @@
package editors
import (
"github.com/go-task/task/v3/taskfile/ast"
)
type (
// Taskfile wraps task list output for use in editor integrations (e.g. VSCode, etc)
Taskfile struct {
Tasks []Task `json:"tasks"`
Location string `json:"location"`
// Namespace wraps task list output for use in editor integrations (e.g. VSCode, etc)
Namespace struct {
Tasks []Task `json:"tasks"`
Namespaces map[string]*Namespace `json:"namespaces,omitempty"`
Location string `json:"location,omitempty"`
}
// Task describes a single task
Task struct {
Name string `json:"name"`
Task string `json:"task"`
Desc string `json:"desc"`
Summary string `json:"summary"`
Aliases []string `json:"aliases"`
UpToDate bool `json:"up_to_date"`
UpToDate *bool `json:"up_to_date,omitempty"`
Location *Location `json:"location"`
}
// Location describes a task's location in a taskfile
@@ -22,3 +28,59 @@ type (
Taskfile string `json:"taskfile"`
}
)
func NewTask(task *ast.Task) Task {
aliases := []string{}
if len(task.Aliases) > 0 {
aliases = task.Aliases
}
return Task{
Name: task.Name(),
Task: task.Task,
Desc: task.Desc,
Summary: task.Summary,
Aliases: aliases,
Location: &Location{
Line: task.Location.Line,
Column: task.Location.Column,
Taskfile: task.Location.Taskfile,
},
}
}
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
if len(namespacePath) == 0 {
return
}
// If there are no child namespaces, then we have found a task and we can
// simply add it to the current namespace
if len(namespacePath) == 1 {
parent.Tasks = append(parent.Tasks, task)
return
}
// Get the key of the current namespace in the path
namespaceKey := namespacePath[0]
// Add the namespace to the parent namespaces map using the namespace key
if parent.Namespaces == nil {
parent.Namespaces = make(map[string]*Namespace, 0)
}
// Search for the current namespace in the parent namespaces map
// If it doesn't exist, create it
namespace, ok := parent.Namespaces[namespaceKey]
if !ok {
namespace = &Namespace{}
parent.Namespaces[namespaceKey] = namespace
}
// Remove the current namespace key from the namespace path.
childNamespacePath := namespacePath[1:]
// If there are no child namespaces in the task name, then we have found the
// namespace of the task and we can add it to the current namespace.
// Otherwise, we need to go deeper
namespace.AddNamespace(childNamespacePath, task)
}

2
internal/env/env.go vendored
View File

@@ -5,7 +5,7 @@ import (
"os"
"strings"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile/ast"
)

View File

@@ -0,0 +1,20 @@
package execext
import (
"runtime"
"strconv"
"github.com/go-task/task/v3/internal/env"
)
var useGoCoreUtils bool
func init() {
// If TASK_CORE_UTILS is set to either true or false, respect that.
// By default, enable on Windows only.
if v, err := strconv.ParseBool(env.GetTaskEnv("CORE_UTILS")); err == nil {
useGoCoreUtils = v
} else {
useGoCoreUtils = runtime.GOOS == "windows"
}
}

View File

@@ -7,8 +7,8 @@ import (
"os"
"path/filepath"
"strings"
"time"
"mvdan.cc/sh/moreinterp/coreutils"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
@@ -59,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
r, err := interp.New(
interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandlers(execHandler),
interp.ExecHandlers(execHandlers()...),
interp.OpenHandler(openHandler),
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
dirOption(opts.Dir),
@@ -106,25 +106,17 @@ func ExpandLiteral(s string) (string, error) {
if s == "" {
return "", nil
}
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
word, err := p.Document(strings.NewReader(s))
if err != nil {
return "", err
}
if len(words) == 0 {
return "", nil
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
}
return expand.Literal(cfg, words[0])
return expand.Literal(cfg, word)
}
// ExpandFields is a wrapper around [expand.Fields]. It will escape the input
@@ -146,12 +138,16 @@ func ExpandFields(s string) ([]string, error) {
Env: expand.FuncEnviron(os.Getenv),
ReadDir2: os.ReadDir,
GlobStar: true,
NullGlob: true,
}
return expand.Fields(cfg, words...)
}
func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
return interp.DefaultExecHandler(15 * time.Second)
func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc) {
if useGoCoreUtils {
handlers = append(handlers, coreutils.ExecHandler)
}
return
}
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {

View File

@@ -1,7 +1,6 @@
package fingerprint
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@@ -164,7 +163,7 @@ func TestIsTaskUpToDate(t *testing.T) {
}
result, err := IsTaskUpToDate(
context.Background(),
t.Context(),
tt.task,
WithStatusChecker(mockStatusChecker),
WithSourcesChecker(mockSourcesChecker),

View File

@@ -5,17 +5,17 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"time"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/task/v3/taskrc"
taskrcast "github.com/go-task/task/v3/taskrc/ast"
)
const usage = `Usage: task [flags...] [task...]
@@ -51,6 +51,7 @@ var (
TaskSort string
Status bool
NoStatus bool
Nested bool
Insecure bool
Force bool
ForceAll bool
@@ -95,7 +96,9 @@ func init() {
// Parse the experiments
dir = cmp.Or(dir, filepath.Dir(entrypoint))
experiments.Parse(dir)
config, _ := taskrc.GetConfig(dir)
experiments.ParseWithConfig(dir, config)
// Parse the rest of the flags
log.SetFlags(0)
@@ -104,10 +107,7 @@ func init() {
log.Print(usage)
pflag.PrintDefaults()
}
offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
if err != nil {
offline = false
}
pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
@@ -118,9 +118,10 @@ func init() {
pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].")
pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.")
pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON")
pflag.BoolVar(&Insecure, "insecure", false, "Forces Task to download Taskfiles over insecure connections.")
pflag.BoolVar(&Nested, "nested", false, "Nest namespaces when listing tasks as JSON")
pflag.BoolVar(&Insecure, "insecure", getConfig(config, func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.")
pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.")
pflag.BoolVarP(&Verbose, "verbose", "v", false, "Enables verbose mode.")
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.")
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
@@ -134,7 +135,7 @@ func init() {
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.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 of tasks to run concurrently.")
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
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.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -150,12 +151,11 @@ func init() {
// Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
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.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", 0, "Expiry duration for cached remote Taskfiles.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
}
pflag.Parse()
}
@@ -196,6 +196,10 @@ func Validate() error {
return errors.New("task: --no-status only applies to --json with --list or --list-all")
}
if Nested && !ListJson {
return errors.New("task: --nested only applies to --json with --list or --list-all")
}
return nil
}
@@ -251,3 +255,16 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithVersionCheck(true),
)
}
// getConfig extracts a config value directly from a pointer field with a fallback default
func getConfig[T any](config *taskrcast.TaskRC, fieldFunc func() *T, fallback T) T {
if config == nil {
return fallback
}
field := fieldFunc()
if field != nil {
return *field
}
return fallback
}

View File

@@ -37,51 +37,87 @@ func DefaultDir(entrypoint, dir string) string {
return ""
}
// Search will look for files with the given possible filenames using the given
// entrypoint and directory. If the entrypoint is set, it will check if the
// ResolveDir returns an absolute path to the directory that the task should be
// run in. If the entrypoint and dir are BOTH set, then the Taskfile will not
// sit inside the directory specified by dir and we should ensure that the dir
// is absolute. Otherwise, the dir will always be the parent directory of the
// resolved entrypoint, so we should return that parent directory.
func ResolveDir(entrypoint, resolvedEntrypoint, dir string) (string, error) {
if entrypoint != "" && dir != "" {
return filepath.Abs(dir)
}
return filepath.Dir(resolvedEntrypoint), nil
}
// Search looks for files with the given possible filenames using the given
// entrypoint and directory. If the entrypoint is set, it checks if the
// entrypoint matches a file or if it matches a directory containing one of the
// possible filenames. Otherwise, it will walk up the file tree starting at the
// given directory and perform a search in each directory for the possible
// possible filenames. Otherwise, it walks up the file tree starting at the
// given directory and performs a search in each directory for the possible
// filenames until it finds a match or reaches the root directory. If the
// entrypoint and directory are both empty, it will default the directory to the
// current working directory and perform a recursive search starting there. If a
// match is found, the absolute path to the file will be returned with its
// directory. If no match is found, an error will be returned.
func Search(entrypoint, dir string, possibleFilenames []string) (string, string, error) {
// entrypoint and directory are both empty, it defaults the directory to the
// current working directory and performs a recursive search starting there. If
// a match is found, the absolute path to the file is returned with its
// directory. If no match is found, an error is returned.
func Search(entrypoint, dir string, possibleFilenames []string) (string, error) {
var err error
if entrypoint != "" {
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
if err != nil {
return "", "", err
return "", err
}
if dir == "" {
dir = filepath.Dir(entrypoint)
} else {
dir, err = filepath.Abs(dir)
if err != nil {
return "", "", err
}
}
return entrypoint, dir, nil
return entrypoint, nil
}
if dir == "" {
dir, err = os.Getwd()
if err != nil {
return "", "", err
return "", err
}
}
entrypoint, err = SearchPathRecursively(dir, possibleFilenames)
if err != nil {
return "", "", err
return "", err
}
dir = filepath.Dir(entrypoint)
return entrypoint, dir, nil
return entrypoint, nil
}
// Search will check if a file at the given path exists or not. If it does, it
// will return the path to it. If it does not, it will search for any files at
// the given path with any of the given possible names. If any of these match a
// file, the first matching path will be returned. If no files are found, an
// SearchAll looks for files with the given possible filenames using the given
// entrypoint and directory. If the entrypoint is set, it checks if the
// entrypoint matches a file or if it matches a directory containing one of the
// possible filenames and add it to a list of matches. It then walks up the file
// tree starting at the given directory and performs a search in each directory
// for the possible filenames until it finds a match or reaches the root
// directory. If the entrypoint and directory are both empty, it defaults the
// directory to the current working directory and performs a recursive search
// starting there. If matches are found, the absolute path to each file is added
// to the list and returned.
func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, error) {
var err error
var entrypoints []string
if entrypoint != "" {
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
if err != nil {
return nil, err
}
entrypoints = append(entrypoints, entrypoint)
}
if dir == "" {
dir, err = os.Getwd()
if err != nil {
return nil, err
}
}
paths, err := SearchNPathRecursively(dir, possibleFilenames, -1)
if err != nil {
return nil, err
}
return append(entrypoints, paths...), nil
}
// SearchPath will check if a file at the given path exists or not. If it does,
// it will return the path to it. If it does not, it will search for any files
// at the given path with any of the given possible names. If any of these match
// a file, the first matching path will be returned. If no files are found, an
// error will be returned.
func SearchPath(path string, possibleFilenames []string) (string, error) {
// Get file info about the path
@@ -111,36 +147,56 @@ func SearchPath(path string, possibleFilenames []string) (string, error) {
return "", os.ErrNotExist
}
// SearchRecursively will check if a file at the given path exists by calling
// the exists function. If a file is not found, it will walk up the directory
// tree calling the Search function until it finds a file or reaches the root
// directory. On supported operating systems, it will also check if the user ID
// of the directory changes and abort if it does.
// SearchPathRecursively walks up the directory tree starting at the given
// path, calling the Search function in each directory until it finds a matching
// file or reaches the root directory. On supported operating systems, it will
// also check if the user ID of the directory changes and abort if it does.
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
owner, err := sysinfo.Owner(path)
paths, err := SearchNPathRecursively(path, possibleFilenames, 1)
if err != nil {
return "", err
}
for {
if len(paths) == 0 {
return "", os.ErrNotExist
}
return paths[0], nil
}
// SearchNPathRecursively walks up the directory tree starting at the given
// path, calling the Search function in each directory and adding each matching
// file that it finds to a list until it reaches the root directory or the
// length of the list exceeds n. On supported operating systems, it will also
// check if the user ID of the directory changes and abort if it does.
func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]string, error) {
var paths []string
owner, err := sysinfo.Owner(path)
if err != nil {
return nil, err
}
for n == -1 || len(paths) < n {
fpath, err := SearchPath(path, possibleFilenames)
if err == nil {
return fpath, nil
paths = append(paths, fpath)
}
// Get the parent path/user id
parentPath := filepath.Dir(path)
parentOwner, err := sysinfo.Owner(parentPath)
if err != nil {
return "", err
return nil, err
}
// Error if we reached the root directory and still haven't found a file
// OR if the user id of the directory changes
if path == parentPath || (parentOwner != owner) {
return "", os.ErrNotExist
return paths, nil
}
owner = parentOwner
path = parentPath
}
return paths, nil
}

View File

@@ -71,35 +71,30 @@ func TestSearch(t *testing.T) {
dir string
possibleFilenames []string
expectedEntrypoint string
expectedDir string
}{
{
name: "find foo.txt using relative entrypoint",
entrypoint: "./testdata/foo.txt",
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute entrypoint",
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir",
dir: "./testdata",
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute dir",
dir: filepath.Join(wd, "testdata"),
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir and relative entrypoint",
@@ -107,7 +102,6 @@ func TestSearch(t *testing.T) {
dir: "./testdata/some/other/dir",
possibleFilenames: []string{"foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
},
{
name: "find fs.go using no entrypoint or dir",
@@ -115,7 +109,6 @@ func TestSearch(t *testing.T) {
dir: "",
possibleFilenames: []string{"fs.go"},
expectedEntrypoint: filepath.Join(wd, "fs.go"),
expectedDir: wd,
},
{
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
@@ -123,30 +116,109 @@ func TestSearch(t *testing.T) {
dir: "",
possibleFilenames: []string{"Taskfile.yml"},
expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
expectedDir: filepath.Join(wd, "..", ".."),
},
{
name: "find foo.txt first if listed first in possible filenames",
entrypoint: "./testdata",
possibleFilenames: []string{"foo.txt", "bar.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find bar.txt first if listed first in possible filenames",
entrypoint: "./testdata",
possibleFilenames: []string{"bar.txt", "foo.txt"},
expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
entrypoint, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)
require.NoError(t, err)
require.Equal(t, tt.expectedEntrypoint, entrypoint)
require.NoError(t, err)
})
}
}
func TestResolveDir(t *testing.T) {
t.Parallel()
wd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
entrypoint string
resolvedEntrypoint string
dir string
expectedDir string
}{
{
name: "find foo.txt using relative entrypoint",
entrypoint: "./testdata/foo.txt",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute entrypoint",
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
dir: "./testdata",
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using absolute dir",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
dir: filepath.Join(wd, "testdata"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find foo.txt using relative dir and relative entrypoint",
entrypoint: "./testdata/foo.txt",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
dir: "./testdata/some/other/dir",
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
},
{
name: "find fs.go using no entrypoint or dir",
entrypoint: "",
resolvedEntrypoint: filepath.Join(wd, "fs.go"),
dir: "",
expectedDir: wd,
},
{
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
entrypoint: "",
resolvedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
dir: "",
expectedDir: filepath.Join(wd, "..", ".."),
},
{
name: "find foo.txt first if listed first in possible filenames",
entrypoint: "./testdata",
resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
{
name: "find bar.txt first if listed first in possible filenames",
entrypoint: "./testdata",
resolvedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
expectedDir: filepath.Join(wd, "testdata"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
entrypoint, dir, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)
dir, err := ResolveDir(tt.entrypoint, tt.resolvedEntrypoint, tt.dir)
require.NoError(t, err)
require.Equal(t, tt.expectedEntrypoint, entrypoint)
require.Equal(t, tt.expectedDir, dir)
require.NoError(t, err)
})
}
}

View File

@@ -2,7 +2,6 @@ package fsnotifyext
import (
"math"
"sync"
"time"
"github.com/fsnotify/fsnotify"
@@ -11,7 +10,6 @@ import (
type Deduper struct {
w *fsnotify.Watcher
waitTime time.Duration
mutex sync.Mutex
}
func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
@@ -21,31 +19,28 @@ func NewDeduper(w *fsnotify.Watcher, waitTime time.Duration) *Deduper {
}
}
func (d *Deduper) GetChan() chan fsnotify.Event {
// GetChan returns a chan of deduplicated [fsnotify.Event].
//
// [fsnotify.Chmod] operations will be skipped.
func (d *Deduper) GetChan() <-chan fsnotify.Event {
channel := make(chan fsnotify.Event)
timers := make(map[string]*time.Timer)
go func() {
timers := make(map[string]*time.Timer)
for {
event, ok := <-d.w.Events
switch {
case !ok:
return
case event.Op == fsnotify.Chmod:
case event.Has(fsnotify.Chmod):
continue
}
d.mutex.Lock()
timer, ok := timers[event.String()]
d.mutex.Unlock()
if !ok {
timer = time.AfterFunc(math.MaxInt64, func() { channel <- event })
timer.Stop()
d.mutex.Lock()
timers[event.String()] = timer
d.mutex.Unlock()
}
timer.Reset(d.waitTime)

View File

@@ -12,8 +12,8 @@ import (
"github.com/fatih/color"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/internal/term"
)

View File

@@ -24,7 +24,6 @@ func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, cache *templater.Cache)
if g.ErrorOnly && err == nil {
return nil
}
return gw.close()
}
}
@@ -40,14 +39,22 @@ func (gw *groupWriter) Write(p []byte) (int, error) {
}
func (gw *groupWriter) close() error {
if gw.buff.Len() == 0 {
// don't print begin/end messages if there's no buffered entries
switch {
case gw.buff.Len() == 0:
return nil
}
if _, err := io.WriteString(gw.writer, gw.begin); err != nil {
case gw.begin == "" && gw.end == "":
_, err := io.Copy(gw.writer, &gw.buff)
return err
default:
_, err := io.Copy(gw.writer, gw.combinedBuff())
return err
}
gw.buff.WriteString(gw.end)
_, err := io.Copy(gw.writer, &gw.buff)
return err
}
func (gw *groupWriter) combinedBuff() io.Reader {
var b bytes.Buffer
_, _ = b.WriteString(gw.begin)
_, _ = io.Copy(&b, &gw.buff)
_, _ = b.WriteString(gw.end)
return &b
}

View File

@@ -2,11 +2,14 @@ package templater
import (
"maps"
"math/rand/v2"
"path/filepath"
"runtime"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"go.yaml.in/yaml/v4"
"mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax"
@@ -18,58 +21,27 @@ var templateFuncs template.FuncMap
func init() {
taskFuncs := template.FuncMap{
"OS": func() string { return runtime.GOOS },
"ARCH": func() string { return runtime.GOARCH },
"numCPU": func() int { return runtime.NumCPU() },
"catLines": func(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
},
"splitLines": func(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.Split(s, "\n")
},
"fromSlash": func(path string) string {
return filepath.FromSlash(path)
},
"toSlash": func(path string) string {
return filepath.ToSlash(path)
},
"exeExt": func() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
},
"shellQuote": func(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
},
"splitArgs": func(s string) ([]string, error) {
return shell.Fields(s, nil)
},
// IsSH is deprecated.
"IsSH": func() bool { return true },
"joinPath": func(elem ...string) string {
return filepath.Join(elem...)
},
"relPath": func(basePath, targetPath string) (string, error) {
return filepath.Rel(basePath, targetPath)
},
"merge": func(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
maps.Copy(result, base)
for _, m := range v {
maps.Copy(result, m)
}
return result
},
"spew": func(v any) string {
return spew.Sdump(v)
},
"OS": os,
"ARCH": arch,
"numCPU": runtime.NumCPU,
"catLines": catLines,
"splitLines": splitLines,
"fromSlash": filepath.FromSlash,
"toSlash": filepath.ToSlash,
"exeExt": exeExt,
"shellQuote": shellQuote,
"splitArgs": splitArgs,
"IsSH": IsSH, // Deprecated
"joinPath": filepath.Join,
"relPath": filepath.Rel,
"merge": merge,
"spew": spew.Sdump,
"fromYaml": fromYaml,
"mustFromYaml": mustFromYaml,
"toYaml": toYaml,
"mustToYaml": mustToYaml,
"uuid": uuid.New,
"randIntN": rand.IntN,
}
// aliases
@@ -83,3 +55,78 @@ func init() {
templateFuncs = template.FuncMap(sprig.TxtFuncMap())
maps.Copy(templateFuncs, taskFuncs)
}
func os() string {
return runtime.GOOS
}
func arch() string {
return runtime.GOARCH
}
func catLines(s string) string {
s = strings.ReplaceAll(s, "\r\n", " ")
return strings.ReplaceAll(s, "\n", " ")
}
func splitLines(s string) []string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.Split(s, "\n")
}
func exeExt() string {
if runtime.GOOS == "windows" {
return ".exe"
}
return ""
}
func shellQuote(str string) (string, error) {
return syntax.Quote(str, syntax.LangBash)
}
func splitArgs(s string) ([]string, error) {
return shell.Fields(s, nil)
}
// Deprecated: now always returns true
func IsSH() bool {
return true
}
func merge(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {
cap += len(m)
}
result := make(map[string]any, cap)
maps.Copy(result, base)
for _, m := range v {
maps.Copy(result, m)
}
return result
}
func fromYaml(v string) any {
output, _ := mustFromYaml(v)
return output
}
func mustFromYaml(v string) (any, error) {
var output any
err := yaml.Unmarshal([]byte(v), &output)
return output, err
}
func toYaml(v any) string {
output, _ := yaml.Marshal(v)
return string(output)
}
func mustToYaml(v any) (string, error) {
output, err := yaml.Marshal(v)
if err != nil {
return "", err
}
return string(output), nil
}

View File

@@ -6,9 +6,10 @@ import (
"maps"
"strings"
"github.com/go-task/template"
"github.com/go-task/task/v3/internal/deepcopy"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/template"
)
// Cache is a help struct that allow us to call "replaceX" funcs multiple

View File

@@ -1 +1 @@
3.43.0
3.45.5

32
package-lock.json generated
View File

@@ -1,32 +0,0 @@
{
"name": "@go-task/cli",
"version": "3.43.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@go-task/cli",
"version": "3.26.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@go-task/go-npm": "^0.2.0"
}
},
"node_modules/@go-task/go-npm": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@go-task/go-npm/-/go-npm-0.2.0.tgz",
"integrity": "sha512-vQbdtBvesHm8EUFHX8QKg4rbBodmu9VsAXH1ozpbiN5jdTMOYHTCMM31EurAYmY+rNNtxJQ4JGy6t383RPlqbw==",
"bin": {
"go-npm": "bin/index.js"
}
}
},
"dependencies": {
"@go-task/go-npm": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@go-task/go-npm/-/go-npm-0.2.0.tgz",
"integrity": "sha512-vQbdtBvesHm8EUFHX8QKg4rbBodmu9VsAXH1ozpbiN5jdTMOYHTCMM31EurAYmY+rNNtxJQ4JGy6t383RPlqbw=="
}
}
}

View File

@@ -1,34 +0,0 @@
{
"name": "@go-task/cli",
"version": "3.43.0",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"postinstall": "go-npm install",
"preuninstall": "go-npm uninstall"
},
"goBinary": {
"name": "task",
"path": "./bin",
"url": "https://github.com/go-task/task/releases/download/v{{version}}/task_{{platform}}_{{arch}}{{archive_ext}}"
},
"files": [],
"repository": {
"type": "git",
"url": "https://github.com/go-task/task.git"
},
"keywords": [
"task",
"taskfile",
"build-tool",
"task-runner"
],
"author": "The Task authors",
"license": "MIT",
"bugs": {
"url": "https://github.com/go-task/task/issues"
},
"homepage": "https://taskfile.dev",
"dependencies": {
"@go-task/go-npm": "^0.2.0"
}
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/version"
@@ -56,6 +57,13 @@ func (e *Executor) Setup() error {
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
if os.IsNotExist(err) {
return nil, errors.TaskfileNotFoundError{
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
Walk: true,
AskInit: true,
}
}
if err != nil {
return nil, err
}
@@ -95,7 +103,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
}
func (e *Executor) setupFuzzyModel() {
if e.Taskfile != nil {
if e.Taskfile == nil {
return
}
@@ -104,6 +112,9 @@ func (e *Executor) setupFuzzyModel() {
var words []string
for name, task := range e.Taskfile.Tasks.All(nil) {
if task.Internal {
continue
}
words = append(words, name)
words = slices.Concat(words, task.Aliases)
}

43
task.go
View File

@@ -8,6 +8,9 @@ import (
"slices"
"sync/atomic"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
@@ -19,9 +22,6 @@ import (
"github.com/go-task/task/v3/internal/summary"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/v3/interp"
)
const (
@@ -150,7 +150,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
release := e.acquireConcurrencyLimit()
defer release()
return e.startExecution(ctx, t, func(ctx context.Context) error {
if err = e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil {
return err
@@ -172,7 +172,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
if t.Method != "" {
method = t.Method
}
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
@@ -211,7 +210,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
for i := range t.Cmds {
if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i, &deferredExitCode)
defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode)
continue
}
@@ -220,25 +219,25 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
}
exitCode, isExitError := interp.IsExitStatus(err)
if isExitError {
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) {
if t.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
continue
}
deferredExitCode = exitCode
deferredExitCode = uint8(exitCode)
}
if call.Indirect {
return err
}
return &errors.TaskRunError{TaskName: t.Task, Err: err}
return err
}
}
e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
return nil
})
}); err != nil {
return &errors.TaskRunError{TaskName: t.Name(), Err: err}
}
return nil
}
func (e *Executor) mkdir(t *ast.Task) error {
@@ -278,17 +277,11 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait()
}
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i]
vars, _ := e.Compiler.GetVariables(origTask, call)
cache := &templater.Cache{Vars: vars}
extra := map[string]any{}
@@ -356,7 +349,8 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
if closeErr := closer(err); closeErr != nil {
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
}
if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
return nil
}
@@ -466,7 +460,6 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
DidYouMean: didYouMean,
}
}
return matchingTask, nil
}
@@ -499,7 +492,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks
for i := range tasks {
g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task})
compiledTask, err := e.CompiledTaskForTaskList(&Call{Task: tasks[i].Task})
if err != nil {
return err
}

View File

@@ -2,10 +2,10 @@ package task_test
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
"maps"
rand "math/rand/v2"
"net/http"
"net/http/httptest"
@@ -27,7 +27,7 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -42,9 +42,11 @@ type (
FormatterTestOption
}
TaskTest struct {
name string
experiments map[*experiments.Experiment]int
postProcessFns []PostProcessFn
name string
experiments map[*experiments.Experiment]int
postProcessFns []PostProcessFn
fixtureTemplateData map[string]any
fixtureTemplatingEnabled bool
}
)
@@ -79,7 +81,22 @@ func (tt *TaskTest) writeFixture(
if goldenFileSuffix != "" {
goldenFileName += "-" + goldenFileSuffix
}
g.Assert(t, goldenFileName, b)
// Create a set of data to be made available to every test fixture
wd, err := os.Getwd()
require.NoError(t, err)
if tt.fixtureTemplatingEnabled {
fixtureTemplateData := map[string]any{
"TEST_NAME": t.Name(),
"TEST_DIR": wd,
}
// If the test has additional template data, copy it into the map
if tt.fixtureTemplateData != nil {
maps.Copy(fixtureTemplateData, tt.fixtureTemplateData)
}
g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, b)
} else {
g.Assert(t, goldenFileName, b)
}
}
// writeFixtureBuffer is a wrapper for writing the main output of the task to a
@@ -234,23 +251,52 @@ func (opt *setupErrorTestOption) applyToFormatterTest(t *FormatterTest) {
t.wantSetupError = true
}
// WithFixtureTemplating enables templating for the golden fixture files with
// the default set of data. This is useful if the golden file is dynamic in some
// way (e.g. contains user-specific directories). To add more data, see
// WithFixtureTemplateData.
func WithFixtureTemplating() TestOption {
return &fixtureTemplatingTestOption{}
}
type fixtureTemplatingTestOption struct{}
func (opt *fixtureTemplatingTestOption) applyToExecutorTest(t *ExecutorTest) {
t.fixtureTemplatingEnabled = true
}
func (opt *fixtureTemplatingTestOption) applyToFormatterTest(t *FormatterTest) {
t.fixtureTemplatingEnabled = true
}
// WithFixtureTemplateData adds data to the golden fixture file templates. Keys
// given here will override any existing values. This option will also enable
// global templating, so you do not need to call WithFixtureTemplating as well.
func WithFixtureTemplateData(key string, value any) TestOption {
return &fixtureTemplateDataTestOption{key, value}
}
type fixtureTemplateDataTestOption struct {
k string
v any
}
func (opt *fixtureTemplateDataTestOption) applyToExecutorTest(t *ExecutorTest) {
t.fixtureTemplatingEnabled = true
t.fixtureTemplateData[opt.k] = opt.v
}
func (opt *fixtureTemplateDataTestOption) applyToFormatterTest(t *FormatterTest) {
t.fixtureTemplatingEnabled = true
t.fixtureTemplateData[opt.k] = opt.v
}
// Post-processing
// A PostProcessFn is a function that can be applied to the output of a test
// fixture before the file is written.
type PostProcessFn func(*testing.T, []byte) []byte
// PPRemoveAbsolutePaths removes any absolute paths from the output of the task.
// This is useful when the task output contains paths that are can be different
// in different environments such as home directories. The function looks for
// any paths that contain the current working directory and truncates them.
func PPRemoveAbsolutePaths(t *testing.T, b []byte) []byte {
t.Helper()
wd, err := os.Getwd()
require.NoError(t, err)
return bytes.ReplaceAll(b, []byte(wd), nil)
}
// PPSortedLines sorts the lines of the output of the task. This is useful when
// the order of the output is not important, but the output is expected to be
// the same each time the task is run (e.g. when running tasks in parallel).
@@ -309,7 +355,7 @@ func (fct fileContentTest) Run(t *testing.T) {
)
require.NoError(t, e.Setup(), "e.Setup()")
require.NoError(t, e.Run(context.Background(), &task.Call{Task: fct.Target}), "e.Run(target)")
require.NoError(t, e.Run(t.Context(), &task.Call{Task: fct.Target}), "e.Run(target)")
for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) {
path := filepathext.SmartJoin(e.Dir, name)
@@ -360,7 +406,7 @@ func TestGenerates(t *testing.T) {
fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask)
// Run task for the first time.
require.NoError(t, e.Run(context.Background(), &task.Call{Task: theTask}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: theTask}))
if _, err := os.Stat(srcFile); err != nil {
t.Errorf("File should exist: %v", err)
@@ -375,7 +421,7 @@ func TestGenerates(t *testing.T) {
buff.Reset()
// Re-run task to ensure it's now found to be up-to-date.
require.NoError(t, e.Run(context.Background(), &task.Call{Task: theTask}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: theTask}))
if buff.String() != upToDate {
t.Errorf("Wrong output message: %s", buff.String())
}
@@ -391,6 +437,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
task string
}{
{[]string{"generated.txt", ".task/checksum/build"}, "build"},
{[]string{"generated-wildcard.txt", ".task/checksum/build-wildcard"}, "build-wildcard"},
{[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"},
}
@@ -416,7 +463,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.task}))
for _, f := range test.files {
_, err := os.Stat(filepathext.SmartJoin(dir, f))
require.NoError(t, err)
@@ -429,7 +476,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
time := s.ModTime()
buff.Reset()
require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.task}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.task}))
assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String())
s, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task))
@@ -460,12 +507,12 @@ func TestStatusVariables(t *testing.T) {
task.WithVerbose(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-checksum"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-checksum"}))
assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a")
buff.Reset()
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-ts"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-ts"}))
inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt"))
require.NoError(t, err)
@@ -496,12 +543,12 @@ func TestCmdsVariables(t *testing.T) {
task.WithVerbose(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-checksum"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-checksum"}))
assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a")
buff.Reset()
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-ts"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-ts"}))
inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt"))
require.NoError(t, err)
ts := fmt.Sprintf("%d", inf.ModTime().Unix())
@@ -522,7 +569,9 @@ func TestCyclicDep(t *testing.T) {
task.WithStderr(io.Discard),
)
require.NoError(t, e.Setup())
assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(context.Background(), &task.Call{Task: "task-1"}))
err := e.Run(t.Context(), &task.Call{Task: "task-1"})
var taskCalledTooManyTimesError *errors.TaskCalledTooManyTimesError
assert.ErrorAs(t, err, &taskCalledTooManyTimesError)
}
func TestTaskVersion(t *testing.T) {
@@ -572,10 +621,10 @@ func TestTaskIgnoreErrors(t *testing.T) {
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-should-pass"}))
require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-should-fail"}))
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-pass"}))
require.Error(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-fail"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-should-pass"}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: "task-should-fail"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "cmd-should-pass"}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: "cmd-should-fail"}))
}
func TestExpand(t *testing.T) {
@@ -595,7 +644,7 @@ func TestExpand(t *testing.T) {
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "pwd"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "pwd"}))
assert.Equal(t, home, strings.TrimSpace(buff.String()))
}
@@ -616,7 +665,7 @@ func TestDry(t *testing.T) {
task.WithDry(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
assert.Equal(t, "task: [build] touch file.txt", strings.TrimSpace(buff.String()))
if _, err := os.Stat(file); err == nil {
@@ -645,13 +694,13 @@ func TestDryChecksum(t *testing.T) {
task.WithDry(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
_, err := os.Stat(checksumFile)
require.Error(t, err, "checksum file should not exist")
e.Dry = false
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
_, err = os.Stat(checksumFile)
require.NoError(t, err, "checksum file should exist")
}
@@ -702,6 +751,7 @@ func TestIncludesRemote(t *testing.T) {
enableExperimentForTest(t, &experiments.RemoteTaskfiles, 1)
dir := "testdata/includes_remote"
os.RemoveAll(filepath.Join(dir, ".task", "remote"))
srv := httptest.NewServer(http.FileServer(http.Dir(dir)))
defer srv.Close()
@@ -777,8 +827,8 @@ func TestIncludesRemote(t *testing.T) {
},
}
for j, e := range executors {
t.Run(fmt.Sprint(j), func(t *testing.T) {
for _, e := range executors {
t.Run(e.name, func(t *testing.T) {
require.NoError(t, e.executor.Setup())
for k, taskCall := range taskCalls {
@@ -792,7 +842,7 @@ func TestIncludesRemote(t *testing.T) {
path := filepath.Join(dir, outputFile)
require.NoError(t, os.RemoveAll(path))
require.NoError(t, e.executor.Run(context.Background(), taskCall))
require.NoError(t, e.executor.Run(t.Context(), taskCall))
actualContent, err := os.ReadFile(path)
require.NoError(t, err)
@@ -933,6 +983,7 @@ func TestIncludesHttp(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
task, err := e.CompiledTask(&task.Call{Task: tc.name})
require.NoError(t, err)
assert.Equal(t, tc.dir, task.Dir)
@@ -1003,7 +1054,7 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_implicit_false"
wd, _ := os.Getwd()
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
expected := fmt.Sprintf(message, wd, dir)
e := task.NewExecutor(
@@ -1023,7 +1074,7 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_explicit_false"
wd, _ := os.Getwd()
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
expected := fmt.Sprintf(message, wd, dir)
e := task.NewExecutor(
@@ -1071,11 +1122,11 @@ func TestIncludesRelativePath(t *testing.T) {
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "common:pwd"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "common:pwd"}))
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
buff.Reset()
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "included:common:pwd"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:common:pwd"}))
assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
}
@@ -1107,7 +1158,7 @@ func TestIncludesInternal(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: test.task})
err := e.Run(t.Context(), &task.Call{Task: test.task})
if test.expectedErr {
require.Error(t, err)
} else {
@@ -1154,7 +1205,7 @@ func TestIncludesFlatten(t *testing.T) {
assert.EqualError(t, err, test.expectedOutput)
} else {
require.NoError(t, err)
_ = e.Run(context.Background(), &task.Call{Task: test.task})
_ = e.Run(t.Context(), &task.Call{Task: test.task})
assert.Equal(t, test.expectedOutput, buff.String())
}
})
@@ -1186,7 +1237,7 @@ func TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: test.task})
err := e.Run(t.Context(), &task.Call{Task: test.task})
if test.expectedErr {
require.Error(t, err)
} else {
@@ -1209,20 +1260,20 @@ func TestIncludesWithExclude(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "included:bar"})
err := e.Run(t.Context(), &task.Call{Task: "included:bar"})
require.NoError(t, err)
assert.Equal(t, "bar\n", buff.String())
buff.Reset()
err = e.Run(context.Background(), &task.Call{Task: "included:foo"})
err = e.Run(t.Context(), &task.Call{Task: "included:foo"})
require.Error(t, err)
buff.Reset()
err = e.Run(context.Background(), &task.Call{Task: "bar"})
err = e.Run(t.Context(), &task.Call{Task: "bar"})
require.Error(t, err)
buff.Reset()
err = e.Run(context.Background(), &task.Call{Task: "foo"})
err = e.Run(t.Context(), &task.Call{Task: "foo"})
require.NoError(t, err)
assert.Equal(t, "foo\n", buff.String())
}
@@ -1252,7 +1303,7 @@ func TestIncludedTaskfileVarMerging(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: test.task})
err := e.Run(t.Context(), &task.Call{Task: test.task})
require.NoError(t, err)
assert.Contains(t, buff.String(), test.expectedOutput)
})
@@ -1287,7 +1338,7 @@ func TestInternalTask(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: test.task})
err := e.Run(t.Context(), &task.Call{Task: test.task})
if test.expectedErr {
require.Error(t, err)
} else {
@@ -1372,7 +1423,7 @@ func TestSummary(t *testing.T) {
task.WithSilent(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-with-summary"}, &task.Call{Task: "other-task-with-summary"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-with-summary"}, &task.Call{Task: "other-task-with-summary"}))
data, err := os.ReadFile(filepathext.SmartJoin(dir, "task-with-summary.txt"))
require.NoError(t, err)
@@ -1398,7 +1449,7 @@ func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "whereami"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"}))
// got should be the "dir" part of "testdata/dir"
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
@@ -1418,7 +1469,7 @@ func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "whereami"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
@@ -1444,7 +1495,7 @@ func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
t.Errorf("Directory should not exist: %v", err)
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: target}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
@@ -1473,7 +1524,7 @@ func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) {
t.Errorf("Directory should not exist: %v", err)
}
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: target}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
@@ -1544,7 +1595,7 @@ func TestShortTaskNotation(t *testing.T) {
task.WithSilent(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String())
}
@@ -1742,7 +1793,7 @@ func TestExitImmediately(t *testing.T) {
)
require.NoError(t, e.Setup())
require.Error(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: "default"}))
assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`)
}
@@ -1762,6 +1813,22 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
})
}
func TestRunOnlyRunsJobsHashOnceWithWildcard(t *testing.T) {
t.Parallel()
tt := fileContentTest{
Dir: "testdata/run",
Target: "deploy",
Files: map[string]string{
"wildcard.txt": "Deploy infra\nDeploy js\nDeploy go\n",
},
}
t.Run("", func(t *testing.T) {
t.Parallel()
tt.Run(t)
})
}
func TestRunOnceSharedDeps(t *testing.T) {
t.Parallel()
@@ -1775,7 +1842,7 @@ func TestRunOnceSharedDeps(t *testing.T) {
task.WithForceAll(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`)
matches := rx.FindAllStringSubmatch(buff.String(), -1)
@@ -1807,10 +1874,10 @@ task-1 ran successfully
task: [task-1] echo 'task-1 ran successfully'
task-1 ran successfully
`)
require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-2"}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: "task-2"}))
assert.Contains(t, buff.String(), expectedOutputOrder)
buff.Reset()
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "parent"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "parent"}))
assert.Contains(t, buff.String(), "child task deferred value-from-parent")
}
@@ -1826,7 +1893,7 @@ func TestExitCodeZero(t *testing.T) {
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "exit-zero"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "exit-zero"}))
assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String()))
}
@@ -1842,7 +1909,7 @@ func TestExitCodeOne(t *testing.T) {
)
require.NoError(t, e.Setup())
require.Error(t, e.Run(context.Background(), &task.Call{Task: "exit-one"}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: "exit-one"}))
assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String()))
}
@@ -1871,7 +1938,7 @@ func TestIgnoreNilElements(t *testing.T) {
task.WithSilent(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
assert.Equal(t, "string-slice-1\n", buff.String())
})
}
@@ -1899,7 +1966,7 @@ task: [bye] echo 'Bye!'
Bye!
::endgroup::
`)
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "bye"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "bye"}))
t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
}
@@ -1916,7 +1983,7 @@ func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) {
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "passing"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "passing"}))
t.Log(buff.String())
assert.Empty(t, buff.String())
}
@@ -1933,7 +2000,7 @@ func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) {
)
require.NoError(t, e.Setup())
require.Error(t, e.Run(context.Background(), &task.Call{Task: "failing"}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: "failing"}))
t.Log(buff.String())
assert.Contains(t, "failing-output", strings.TrimSpace(buff.String()))
assert.NotContains(t, "passing", strings.TrimSpace(buff.String()))
@@ -1964,12 +2031,8 @@ task: [included3:task1] echo "VAR_1 is included-default-var1"
VAR_1 is included-default-var1
task: [included3:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2
task: [included4:task1] echo "VAR_1 is included4-var1"
VAR_1 is included4-var1
task: [included4:task1] echo "VAR_2 is included-default-var2"
VAR_2 is included-default-var2
`)
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task1"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task1"}))
t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
}
@@ -2007,7 +2070,7 @@ Hello foo
task: [bar:lib:greet] echo 'Hello bar'
Hello bar
`)
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
t.Log(buff.String())
assert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String()))
}
@@ -2045,7 +2108,7 @@ func TestErrorCode(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: test.task})
err := e.Run(t.Context(), &task.Call{Task: test.task})
require.Error(t, err)
taskRunErr, ok := err.(*errors.TaskRunError)
assert.True(t, ok, "cannot cast returned error to *task.TaskRunError")
@@ -2097,7 +2160,7 @@ func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // canno
for _, test := range tests { // nolint:paralleltest // cannot run in parallel
t.Run(test.name, func(t *testing.T) {
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: test.task})
err := e.Run(t.Context(), &task.Call{Task: test.task})
require.NoError(t, err)
assert.Equal(t, test.expected, strings.TrimSpace(buff.String()))
buff.Reset()
@@ -2140,7 +2203,7 @@ func TestTaskfileWalk(t *testing.T) {
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
assert.Equal(t, test.expected, buff.String())
})
}
@@ -2151,14 +2214,14 @@ func TestUserWorkingDirectory(t *testing.T) {
var buff bytes.Buffer
e := task.NewExecutor(
task.WithEntrypoint("testdata/user_working_dir/Taskfile.yml"),
task.WithDir("testdata/user_working_dir"),
task.WithStdout(&buff),
task.WithStderr(&buff),
)
wd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "default"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
}
@@ -2180,7 +2243,7 @@ func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
require.NoError(t, err)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "included:echo"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:echo"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
}
@@ -2194,7 +2257,7 @@ func TestPlatforms(t *testing.T) {
task.WithStderr(&buff),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "build-" + runtime.GOOS}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build-" + runtime.GOOS}))
assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String())
}
@@ -2209,7 +2272,7 @@ func TestPOSIXShellOptsGlobalLevel(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "pipefail"})
err := e.Run(t.Context(), &task.Call{Task: "pipefail"})
require.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
@@ -2225,7 +2288,7 @@ func TestPOSIXShellOptsTaskLevel(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "pipefail"})
err := e.Run(t.Context(), &task.Call{Task: "pipefail"})
require.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
@@ -2241,7 +2304,7 @@ func TestPOSIXShellOptsCommandLevel(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "pipefail"})
err := e.Run(t.Context(), &task.Call{Task: "pipefail"})
require.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String())
}
@@ -2257,7 +2320,7 @@ func TestBashShellOptsGlobalLevel(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "globstar"})
err := e.Run(t.Context(), &task.Call{Task: "globstar"})
require.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
@@ -2273,7 +2336,7 @@ func TestBashShellOptsTaskLevel(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "globstar"})
err := e.Run(t.Context(), &task.Call{Task: "globstar"})
require.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
@@ -2289,7 +2352,7 @@ func TestBashShellOptsCommandLevel(t *testing.T) {
)
require.NoError(t, e.Setup())
err := e.Run(context.Background(), &task.Call{Task: "globstar"})
err := e.Run(t.Context(), &task.Call{Task: "globstar"})
require.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String())
}
@@ -2309,7 +2372,7 @@ func TestSplitArgs(t *testing.T) {
vars := ast.NewVars()
vars.Set("CLI_ARGS", ast.Var{Value: "foo bar 'foo bar baz'"})
err := e.Run(context.Background(), &task.Call{Task: "default", Vars: vars})
err := e.Run(t.Context(), &task.Call{Task: "default", Vars: vars})
require.NoError(t, err)
assert.Equal(t, "3\n", buff.String())
}
@@ -2350,14 +2413,14 @@ func TestSilence(t *testing.T) {
// Then test the two basic cases where the task is silent or not.
// A silenced task.
err = e.Run(context.Background(), &task.Call{Task: "silent"})
err = e.Run(t.Context(), &task.Call{Task: "silent"})
require.NoError(t, err)
require.Empty(t, buff.String(), "siWhile running lent: Expected not see output, because the task is silent")
buff.Reset()
// A chatty (not silent) task.
err = e.Run(context.Background(), &task.Call{Task: "chatty"})
err = e.Run(t.Context(), &task.Call{Task: "chatty"})
require.NoError(t, err)
require.NotEmpty(t, buff.String(), "chWhile running atty: Expected to see output, because the task is not silent")
@@ -2365,42 +2428,42 @@ func TestSilence(t *testing.T) {
// Then test invoking the two task from other tasks.
// A silenced task that calls a chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-silent-calls-chatty-non-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-silent-calls-chatty-non-silenced"})
require.NoError(t, err)
require.NotEmpty(t, buff.String(), "While running task-test-silent-calls-chatty-non-silenced: Expected to see output. The task is silenced, but the called task is not. Silence does not propagate to called tasks.")
buff.Reset()
// A silent task that does a silent call to a chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-silent-calls-chatty-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-silent-calls-chatty-silenced"})
require.NoError(t, err)
require.Empty(t, buff.String(), "While running task-test-silent-calls-chatty-silenced: Expected not to see output. The task calls chatty task, but the call is silenced.")
buff.Reset()
// A chatty task that does a call to a chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-chatty-non-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-chatty-calls-chatty-non-silenced"})
require.NoError(t, err)
require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-non-silenced: Expected to see output. Both caller and callee are chatty and not silenced.")
buff.Reset()
// A chatty task that does a silenced call to a chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-chatty-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-chatty-calls-chatty-silenced"})
require.NoError(t, err)
require.NotEmpty(t, buff.String(), "While running task-test-chatty-calls-chatty-silenced: Expected to see output. Call to a chatty task is silenced, but the parent task is not.")
buff.Reset()
// A chatty task with no cmd's of its own that does a silenced call to a chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-no-cmds-calls-chatty-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-no-cmds-calls-chatty-silenced"})
require.NoError(t, err)
require.Empty(t, buff.String(), "While running task-test-no-cmds-calls-chatty-silenced: Expected not to see output. While the task itself is not silenced, it does not have any cmds and only does an invocation of a silenced task.")
buff.Reset()
// A chatty task that does a silenced invocation of a task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-chatty-calls-silenced-cmd"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-chatty-calls-silenced-cmd"})
require.NoError(t, err)
require.Empty(t, buff.String(), "While running task-test-chatty-calls-silenced-cmd: Expected not to see output. While the task itself is not silenced, its call to the chatty task is silent.")
@@ -2408,21 +2471,21 @@ func TestSilence(t *testing.T) {
// Then test calls via dependencies.
// A silent task that depends on a chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-non-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-non-silenced"})
require.NoError(t, err)
require.NotEmpty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-non-silenced: Expected to see output. The task is silent and depends on a chatty task. Dependencies does not inherit silence.")
buff.Reset()
// A silent task that depends on a silenced chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-is-silent-depends-on-chatty-silenced"})
require.NoError(t, err)
require.Empty(t, buff.String(), "While running task-test-is-silent-depends-on-chatty-silenced: Expected not to see output. The task is silent and has a silenced dependency on a chatty task.")
buff.Reset()
// A chatty task that, depends on a silenced chatty task.
err = e.Run(context.Background(), &task.Call{Task: "task-test-is-chatty-depends-on-chatty-silenced"})
err = e.Run(t.Context(), &task.Call{Task: "task-test-is-chatty-depends-on-chatty-silenced"})
require.NoError(t, err)
require.Empty(t, buff.String(), "While running task-test-is-chatty-depends-on-chatty-silenced: Expected not to see output. The task is chatty but does not have commands and has a silenced dependency on a chatty task.")
@@ -2474,7 +2537,7 @@ func TestForce(t *testing.T) {
task.WithForceAll(tt.forceAll),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-with-dep"}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-with-dep"}))
})
}
}
@@ -2534,10 +2597,10 @@ func TestWildcard(t *testing.T) {
)
require.NoError(t, e.Setup())
if test.wantErr {
require.Error(t, e.Run(context.Background(), &task.Call{Task: test.call}))
require.Error(t, e.Run(t.Context(), &task.Call{Task: test.call}))
return
}
require.NoError(t, e.Run(context.Background(), &task.Call{Task: test.call}))
require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.call}))
assert.Equal(t, test.expectedOutput, buff.String())
})
}

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -5,7 +5,7 @@ import (
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -24,6 +24,7 @@ type (
AdvancedImport bool
Vars *Vars
Flatten bool
Checksum string
}
// Includes is an ordered map of namespaces to includes.
Includes struct {
@@ -165,6 +166,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
Aliases []string
Excludes []string
Vars *Vars
Checksum string
}
if err := node.Decode(&includedTaskfile); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -178,6 +180,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
include.AdvancedImport = true
include.Vars = includedTaskfile.Vars
include.Flatten = includedTaskfile.Flatten
include.Checksum = includedTaskfile.Checksum
return nil
}
@@ -200,5 +203,7 @@ func (include *Include) DeepCopy() *Include {
AdvancedImport: include.AdvancedImport,
Vars: include.Vars.DeepCopy(),
Flatten: include.Flatten,
Aliases: deepcopy.Slice(include.Aliases),
Checksum: include.Checksum,
}
}

View File

@@ -4,7 +4,7 @@ import (
"iter"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/goext"

View File

@@ -3,7 +3,7 @@ package ast
import (
"fmt"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/taskfile/ast"
)

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -5,7 +5,7 @@ import (
"regexp"
"strings"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -46,17 +46,22 @@ type Task struct {
Namespace string
IncludeVars *Vars
IncludedTaskfileVars *Vars
FullName string
}
func (t *Task) Name() string {
if t.Label != "" {
return t.Label
}
if t.FullName != "" {
return t.FullName
}
return t.Task
}
func (t *Task) LocalName() string {
name := t.Task
name := t.FullName
name = strings.TrimPrefix(name, t.Namespace)
name = strings.TrimPrefix(name, ":")
return name
@@ -220,6 +225,7 @@ func (t *Task) DeepCopy() *Task {
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
FullName: t.FullName,
}
return c
}

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)

View File

@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/taskfile/ast"
)

View File

@@ -8,7 +8,7 @@ import (
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
)
@@ -18,7 +18,10 @@ type Var struct {
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
key := node.Content[0].Value
key := "<none>"
if len(node.Content) > 0 {
key = node.Content[0].Value
}
switch key {
case "sh", "ref", "map":
var m struct {

View File

@@ -5,7 +5,7 @@ import (
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -113,7 +113,7 @@ func (vars *Vars) ToCacheMap() (m map[string]any) {
m[k] = v.Value
}
}
return
return m
}
// Merge loops over other and merges it values with the variables in vars. If

View File

@@ -8,7 +8,7 @@ import (
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/fsext"
)
@@ -17,6 +17,8 @@ type Node interface {
Parent() Node
Location() string
Dir() string
Checksum() string
Verify(checksum string) bool
ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error)
}
@@ -70,6 +72,16 @@ func NewNode(
return node, err
}
func isRemoteEntrypoint(entrypoint string) bool {
scheme, _ := getScheme(entrypoint)
switch scheme {
case "git", "http", "https":
return true
default:
return false
}
}
func getScheme(uri string) (string, error) {
u, err := giturls.Parse(uri)
if u == nil {

View File

@@ -1,19 +1,20 @@
package taskfile
type (
NodeOption func(*BaseNode)
// BaseNode is a generic node that implements the Parent() methods of the
NodeOption func(*baseNode)
// baseNode is a generic node that implements the Parent() methods of the
// NodeReader interface. It does not implement the Read() method and it
// designed to be embedded in other node types so that this boilerplate code
// does not need to be repeated.
BaseNode struct {
parent Node
dir string
baseNode struct {
parent Node
dir string
checksum string
}
)
func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
node := &BaseNode{
func NewBaseNode(dir string, opts ...NodeOption) *baseNode {
node := &baseNode{
parent: nil,
dir: dir,
}
@@ -27,15 +28,29 @@ func NewBaseNode(dir string, opts ...NodeOption) *BaseNode {
}
func WithParent(parent Node) NodeOption {
return func(node *BaseNode) {
return func(node *baseNode) {
node.parent = parent
}
}
func (node *BaseNode) Parent() Node {
func WithChecksum(checksum string) NodeOption {
return func(node *baseNode) {
node.checksum = checksum
}
}
func (node *baseNode) Parent() Node {
return node.parent
}
func (node *BaseNode) Dir() string {
func (node *baseNode) Dir() string {
return node.dir
}
func (node *baseNode) Checksum() string {
return node.checksum
}
func (node *baseNode) Verify(checksum string) bool {
return node.checksum == "" || node.checksum == checksum
}

View File

@@ -11,13 +11,13 @@ import (
const remoteCacheDir = "remote"
type CacheNode struct {
*BaseNode
*baseNode
source RemoteNode
}
func NewCacheNode(source RemoteNode, dir string) *CacheNode {
return &CacheNode{
BaseNode: &BaseNode{
baseNode: &baseNode{
dir: filepath.Join(dir, remoteCacheDir),
},
source: source,

View File

@@ -4,8 +4,8 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
@@ -13,25 +13,34 @@ import (
// A FileNode is a node that reads a taskfile from the local filesystem.
type FileNode struct {
*BaseNode
Entrypoint string
*baseNode
entrypoint string
}
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error
base := NewBaseNode(dir, opts...)
entrypoint, base.dir, err = fsext.Search(entrypoint, base.dir, defaultTaskfiles)
// Find the entrypoint file
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, defaultTaskfiles)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
}
return nil, err
}
// Resolve the directory
resolvedDir, err := fsext.ResolveDir(entrypoint, resolvedEntrypoint, dir)
if err != nil {
return nil, err
}
return &FileNode{
BaseNode: base,
Entrypoint: entrypoint,
baseNode: NewBaseNode(resolvedDir, opts...),
entrypoint: resolvedEntrypoint,
}, nil
}
func (node *FileNode) Location() string {
return node.Entrypoint
return node.entrypoint
}
func (node *FileNode) Read() ([]byte, error) {
@@ -45,10 +54,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}
@@ -63,7 +69,7 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// 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.Entrypoint)
entrypointDir := filepath.Dir(node.entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil
}
@@ -79,6 +85,6 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
// 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.Entrypoint)
entrypointDir := filepath.Dir(node.entrypoint)
return filepathext.SmartJoin(entrypointDir, path), nil
}

View File

@@ -21,8 +21,8 @@ import (
// An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct {
*BaseNode
URL *url.URL
*baseNode
url *url.URL
rawUrl string
ref string
path string
@@ -40,23 +40,20 @@ func NewGitNode(
return nil, err
}
basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
return x[0], x[1]
}()
basePath, path := splitURLOnDoubleSlash(u)
ref := u.Query().Get("ref")
rawUrl := u.String()
rawUrl := u.Redacted()
u.RawQuery = ""
u.Path = basePath
if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
return nil, &errors.TaskfileNotSecureError{URI: u.Redacted()}
}
return &GitNode{
BaseNode: base,
URL: u,
baseNode: base,
url: u,
rawUrl: rawUrl,
ref: ref,
path: path,
@@ -79,7 +76,7 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(),
URL: node.url.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
@@ -101,8 +98,13 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
}
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
@@ -127,11 +129,23 @@ func (node *GitNode) ResolveDir(dir string) (string, error) {
func (node *GitNode) CacheKey() string {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=")
prefix := filepath.Base(filepath.Dir(node.path))
lastDir := filepath.Base(node.path)
lastDir := filepath.Base(filepath.Dir(node.path))
prefix := filepath.Base(node.path)
// Means it's not "", nor "." nor "/", so it's a valid directory
if len(lastDir) > 1 {
prefix = fmt.Sprintf("%s-%s", lastDir, prefix)
prefix = fmt.Sprintf("%s.%s", lastDir, prefix)
}
return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum)
}
func splitURLOnDoubleSlash(u *url.URL) (string, string) {
x := strings.Split(u.Path, "//")
switch len(x) {
case 0:
return "", ""
case 1:
return x[0], ""
default:
return x[0], x[1]
}
return fmt.Sprintf("%s.%s", prefix, checksum)
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGitNode_ssh(t *testing.T) {
@@ -13,13 +14,24 @@ func TestGitNode_ssh(t *testing.T) {
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())
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location())
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_sshWithAltRepo(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
entrypoint, err := node.ResolveEntrypoint("git@github.com:foo/other.git//Taskfile.yml?ref=dev")
assert.NoError(t, err)
assert.Equal(t, "git@github.com:foo/other.git//Taskfile.yml?ref=dev", entrypoint)
}
func TestGitNode_sshWithDir(t *testing.T) {
t.Parallel()
@@ -27,8 +39,8 @@ func TestGitNode_sshWithDir(t *testing.T) {
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())
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location())
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)
@@ -41,8 +53,8 @@ func TestGitNode_https(t *testing.T) {
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())
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.Location())
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)
@@ -55,8 +67,8 @@ func TestGitNode_httpsWithDir(t *testing.T) {
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())
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.Location())
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)
@@ -65,18 +77,28 @@ func TestGitNode_httpsWithDir(t *testing.T) {
func TestGitNode_CacheKey(t *testing.T) {
t.Parallel()
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, "Taskfile.yml-directory.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb", key)
tests := []struct {
entrypoint string
expectedKey string
}{
{
entrypoint: "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main",
expectedKey: "git.github.com.directory.Taskfile.yml.f1ddddac425a538870230a3e38fc0cded4ec5da250797b6cab62c82477718fbb",
},
{
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
expectedKey: "git.github.com.Taskfile.yml.39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4",
},
{
entrypoint: "https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main",
expectedKey: "git.github.com.directory.Taskfile.yml.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31",
},
}
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
key = node.CacheKey()
assert.Equal(t, "Taskfile.yml-..39d28c1ff36f973705ae188b991258bbabaffd6d60bcdde9693d157d00d5e3a4", key)
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
key = node.CacheKey()
assert.Equal(t, "Taskfile.yml-directory.1b6d145e01406dcc6c0aa572e5a5d1333be1ccf2cae96d18296d725d86197d31", key)
for _, tt := range tests {
node, err := NewGitNode(tt.entrypoint, "", false)
require.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, tt.expectedKey, key)
}
}

View File

@@ -16,9 +16,8 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*BaseNode
URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
entrypoint string // stores entrypoint url. used for building graph vertices.
*baseNode
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
}
func NewHTTPNode(
@@ -33,18 +32,16 @@ func NewHTTPNode(
return nil, err
}
if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}
}
return &HTTPNode{
BaseNode: base,
URL: url,
entrypoint: entrypoint,
baseNode: base,
url: url,
}, nil
}
func (node *HTTPNode) Location() string {
return node.entrypoint
return node.url.Redacted()
}
func (node *HTTPNode) Read() ([]byte, error) {
@@ -52,14 +49,13 @@ func (node *HTTPNode) Read() ([]byte, error) {
}
func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.URL)
url, err := RemoteExists(ctx, *node.url)
if err != nil {
return nil, err
}
node.URL = url
req, err := http.NewRequest("GET", node.URL.String(), nil)
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
@@ -67,12 +63,12 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
if ctx.Err() != nil {
return nil, err
}
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.TaskfileFetchFailedError{
URI: node.URL.String(),
URI: node.Location(),
HTTPStatusCode: resp.StatusCode,
}
}
@@ -91,7 +87,7 @@ func (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) {
if err != nil {
return "", err
}
return node.URL.ResolveReference(ref).String(), nil
return node.url.ResolveReference(ref).String(), nil
}
func (node *HTTPNode) ResolveDir(dir string) (string, error) {
@@ -116,12 +112,12 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
func (node *HTTPNode) CacheKey() string {
checksum := strings.TrimRight(checksum([]byte(node.Location())), "=")
dir, filename := filepath.Split(node.entrypoint)
dir, filename := filepath.Split(node.url.Path)
lastDir := filepath.Base(dir)
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)
prefix = fmt.Sprintf("%s.%s", lastDir, filename)
}
return fmt.Sprintf("%s.%s", prefix, checksum)
return fmt.Sprintf("http.%s.%s.%s", node.url.Host, prefix, checksum)
}

View File

@@ -0,0 +1,49 @@
package taskfile
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTPNode_CacheKey(t *testing.T) {
t.Parallel()
tests := []struct {
entrypoint string
expectedKey string
}{
{
entrypoint: "https://github.com",
expectedKey: "http.github.com..996e1f714b08e971ec79e3bea686287e66441f043177999a13dbc546d8fe402a",
},
{
entrypoint: "https://github.com/Taskfile.yml",
expectedKey: "http.github.com.Taskfile.yml.85b3c3ad71b78dc74e404c7b4390fc13672925cb644a4d26c21b9f97c17b5fc0",
},
{
entrypoint: "https://github.com/foo",
expectedKey: "http.github.com.foo.df3158dafc823e6847d9bcaf79328446c4877405e79b100723fa6fd545ed3e2b",
},
{
entrypoint: "https://github.com/foo/Taskfile.yml",
expectedKey: "http.github.com.foo.Taskfile.yml.aea946ea7eb6f6bb4e159e8b840b6b50975927778b2e666df988c03bbf10c4c4",
},
{
entrypoint: "https://github.com/foo/bar",
expectedKey: "http.github.com.foo.bar.d3514ad1d4daedf9cc2825225070b49ebc8db47fa5177951b2a5b9994597570c",
},
{
entrypoint: "https://github.com/foo/bar/Taskfile.yml",
expectedKey: "http.github.com.bar.Taskfile.yml.b9cf01e01e47c0e96ea536e1a8bd7b3a6f6c1f1881bad438990d2bfd4ccd0ac0",
},
}
for _, tt := range tests {
node, err := NewHTTPNode(tt.entrypoint, "", false)
require.NoError(t, err)
key := node.CacheKey()
assert.Equal(t, tt.expectedKey, key)
}
}

View File

@@ -4,7 +4,6 @@ import (
"bufio"
"fmt"
"os"
"strings"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
@@ -12,12 +11,12 @@ import (
// A StdinNode is a node that reads a taskfile from the standard input stream.
type StdinNode struct {
*BaseNode
*baseNode
}
func NewStdinNode(dir string) (*StdinNode, error) {
return &StdinNode{
BaseNode: NewBaseNode(dir),
baseNode: NewBaseNode(dir),
}, nil
}
@@ -43,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -8,8 +8,8 @@ import (
"time"
"github.com/dominikbraun/graph"
"go.yaml.in/yaml/v4"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
@@ -249,7 +249,8 @@ func (r *Reader) include(ctx context.Context, node Node) error {
Aliases: include.Aliases,
AdvancedImport: include.AdvancedImport,
Excludes: include.Excludes,
Vars: templater.ReplaceVars(include.Vars, cache),
Vars: include.Vars,
Checksum: include.Checksum,
}
if err := cache.Err(); err != nil {
return err
@@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
WithParent(node),
WithChecksum(include.Checksum),
)
if err != nil {
if include.Optional {
@@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
if node, isRemote := node.(RemoteNode); isRemote {
return r.readRemoteNodeContent(ctx, node)
}
return node.Read()
// Read the Taskfile
b, err := node.Read()
if err != nil {
return nil, err
}
// If the given checksum doesn't match the sum pinned in the Taskfile
checksum := checksum(b)
if !node.Verify(checksum) {
return nil, &errors.TaskfileDoesNotMatchChecksum{
URI: node.Location(),
ExpectedChecksum: node.Checksum(),
ActualChecksum: checksum,
}
}
return b, nil
}
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
@@ -427,17 +446,29 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
}
r.debugf("found remote file at %q\n", node.Location())
checksum := checksum(downloadedBytes)
prompt := cache.ChecksumPrompt(checksum)
// Prompt the user if required
if prompt != "" {
if err := func() error {
r.promptMutex.Lock()
defer r.promptMutex.Unlock()
return r.promptf(prompt, node.Location())
}(); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
// If the given checksum doesn't match the sum pinned in the Taskfile
checksum := checksum(downloadedBytes)
if !node.Verify(checksum) {
return nil, &errors.TaskfileDoesNotMatchChecksum{
URI: node.Location(),
ExpectedChecksum: node.Checksum(),
ActualChecksum: checksum,
}
}
// If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" {
// Prompt the user if required
prompt := cache.ChecksumPrompt(checksum)
if prompt != "" {
if err := func() error {
r.promptMutex.Lock()
defer r.promptMutex.Unlock()
return r.promptf(prompt, node.Location())
}(); err != nil {
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
}
}
}

View File

@@ -36,11 +36,11 @@ var (
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
// Request the given URL
@@ -49,7 +49,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
if ctx.Err() != nil {
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
}
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
defer resp.Body.Close()
@@ -61,7 +61,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
return strings.Contains(contentType, s)
}) {
return u, nil
return &u, nil
}
// If the request was not successful, append the default Taskfile names to
@@ -78,7 +78,7 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
// Try the alternative URL
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}
defer resp.Body.Close()
@@ -88,5 +88,5 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
}
}
return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
return nil, errors.TaskfileNotFoundError{URI: u.Redacted(), Walk: false}
}

View File

@@ -1,8 +1,48 @@
package ast
import "github.com/Masterminds/semver/v3"
import (
"cmp"
"maps"
"time"
"github.com/Masterminds/semver/v3"
)
type TaskRC struct {
Version *semver.Version `yaml:"version"`
Verbose *bool `yaml:"verbose"`
Concurrency *int `yaml:"concurrency"`
Remote Remote `yaml:"remote"`
Experiments map[string]int `yaml:"experiments"`
}
type Remote struct {
Insecure *bool `yaml:"insecure"`
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
}
// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
func (t *TaskRC) Merge(other *TaskRC) {
if other == nil {
return
}
t.Version = cmp.Or(other.Version, t.Version)
if t.Experiments == nil && other.Experiments != nil {
t.Experiments = other.Experiments
} else if t.Experiments != nil && other.Experiments != nil {
maps.Copy(t.Experiments, other.Experiments)
}
// Merge Remote fields
t.Remote.Insecure = cmp.Or(other.Remote.Insecure, t.Remote.Insecure)
t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline)
t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)
t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)
t.Verbose = cmp.Or(other.Verbose, t.Verbose)
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
}

View File

@@ -1,24 +1,24 @@
package taskrc
import "github.com/go-task/task/v3/internal/fsext"
import (
"github.com/go-task/task/v3/internal/fsext"
)
type Node struct {
entrypoint string
dir string
}
func NewNode(
entrypoint string,
dir string,
possibleFileNames []string,
) (*Node, error) {
dir = fsext.DefaultDir(entrypoint, dir)
var err error
entrypoint, dir, err = fsext.Search(entrypoint, dir, defaultTaskRCs)
resolvedEntrypoint, err := fsext.SearchPath(dir, possibleFileNames)
if err != nil {
return nil, err
}
return &Node{
entrypoint: entrypoint,
dir: dir,
entrypoint: resolvedEntrypoint,
}, nil
}

View File

@@ -3,7 +3,7 @@ package taskrc
import (
"os"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/taskrc/ast"
)

View File

@@ -1,6 +1,92 @@
package taskrc
var defaultTaskRCs = []string{
".taskrc.yml",
".taskrc.yaml",
import (
"os"
"path/filepath"
"slices"
"strings"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/taskrc/ast"
)
var (
defaultXDGTaskRCs = []string{
"taskrc.yml",
"taskrc.yaml",
}
defaultTaskRCs = []string{
".taskrc.yml",
".taskrc.yaml",
}
)
// GetConfig loads and merges local and global Task configuration files
func GetConfig(dir string) (*ast.TaskRC, error) {
var config *ast.TaskRC
reader := NewReader()
// Read the XDG config file
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
xdgConfigNode, err := NewNode("", filepath.Join(xdgConfigHome, "task"), defaultXDGTaskRCs)
if err == nil && xdgConfigNode != nil {
xdgConfig, err := reader.Read(xdgConfigNode)
if err != nil {
return nil, err
}
config = xdgConfig
}
}
// If the current path does not contain $HOME
// If it does contain $HOME, then we will find this config later anyway
home, err := os.UserHomeDir()
if err == nil && !strings.Contains(home, dir) {
homeNode, err := NewNode("", home, defaultTaskRCs)
if err == nil && homeNode != nil {
homeConfig, err := reader.Read(homeNode)
if err != nil {
return nil, err
}
if config == nil {
config = homeConfig
} else {
config.Merge(homeConfig)
}
}
}
// Find all the nodes from the given directory up to the users home directory
absDir, err := filepath.Abs(dir)
if err != nil {
return config, err
}
entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs)
if err != nil {
return config, err
}
// Reverse the entrypoints since we want the child files to override parent ones
slices.Reverse(entrypoints)
// Loop over the nodes, and merge them into the main config
for _, entrypoint := range entrypoints {
node, err := NewNode("", entrypoint, defaultTaskRCs)
if err != nil {
return nil, err
}
localConfig, err := reader.Read(node)
if err != nil {
return nil, err
}
if localConfig == nil {
continue
}
if config == nil {
config = localConfig
continue
}
config.Merge(localConfig)
}
return config, nil
}

137
taskrc/taskrc_test.go Normal file
View File

@@ -0,0 +1,137 @@
package taskrc
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-task/task/v3/taskrc/ast"
)
const (
xdgConfigYAML = `
experiments:
FOO: 1
BAR: 1
BAZ: 1
`
homeConfigYAML = `
experiments:
FOO: 2
BAR: 2
`
localConfigYAML = `
experiments:
FOO: 3
`
)
func setupDirs(t *testing.T) (string, string, string) {
t.Helper()
xdgConfigDir := t.TempDir()
xdgTaskConfigDir := filepath.Join(xdgConfigDir, "task")
require.NoError(t, os.Mkdir(xdgTaskConfigDir, 0o755))
homeDir := t.TempDir()
localDir := filepath.Join(homeDir, "local")
require.NoError(t, os.Mkdir(localDir, 0o755))
t.Setenv("XDG_CONFIG_HOME", xdgConfigDir)
t.Setenv("HOME", homeDir)
return xdgTaskConfigDir, homeDir, localDir
}
func writeFile(t *testing.T, dir, filename, content string) {
t.Helper()
err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644)
assert.NoError(t, err)
}
func TestGetConfig_NoConfigFiles(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Nil(t, cfg)
}
func TestGetConfig_OnlyXDG(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgDir, _, localDir := setupDirs(t)
writeFile(t, xdgDir, "taskrc.yml", xdgConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 1,
"BAR": 1,
"BAZ": 1,
},
}, cfg)
}
func TestGetConfig_OnlyHome(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, homeDir, localDir := setupDirs(t)
writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 2,
"BAR": 2,
},
}, cfg)
}
func TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
writeFile(t, localDir, ".taskrc.yml", localConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 3,
},
}, cfg)
}
func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)
// Write local config
writeFile(t, localDir, ".taskrc.yml", localConfigYAML)
// Write home config
writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML)
// Write XDG config
writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfigYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, &ast.TaskRC{
Version: nil,
Experiments: map[string]int{
"FOO": 3,
"BAR": 2,
"BAZ": 1,
},
}, cfg)
}

View File

@@ -12,6 +12,14 @@ tasks:
generates:
- ./generated.txt
method: checksum
build-*:
cmds:
- cp ./source.txt ./generated-{{index .MATCH 0}}.txt
sources:
- ./source.txt
generates:
- ./generated-{{index .MATCH 0}}.txt
method: checksum
build-with-status:
cmds:

View File

@@ -0,0 +1 @@
Hello, World!

View File

@@ -1 +1 @@
task: Missing schema version in Taskfile "/testdata/empty_taskfile/Taskfile.yml"
task: Missing schema version in Taskfile "{{.TEST_DIR}}/testdata/empty_taskfile/Taskfile.yml"

View File

@@ -1,2 +1,2 @@
task: Failed to parse /testdata/for/cmds/Taskfile.yml:
task: Failed to parse {{.TEST_DIR}}/testdata/for/cmds/Taskfile.yml:
matrix reference ".NOT_A_LIST" must resolve to a list

View File

@@ -1,2 +1,2 @@
matrix reference ".NOT_A_LIST" must resolve to a list
task: Failed to parse /testdata/for/deps/Taskfile.yml:
task: Failed to parse {{.TEST_DIR}}/testdata/for/deps/Taskfile.yml:

9
testdata/fuzzy/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 3
tasks:
install: echo 'install'
internal:
internal: true
cmds:
- echo "internal"

View File

@@ -0,0 +1 @@
task: Task "instal" does not exist. Did you mean "install"?

View File

@@ -0,0 +1 @@
task: No tasks with description available. Try --list-all to list all tasks

View File

@@ -0,0 +1 @@
task: Task "intern" does not exist

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