Compare commits

...

164 Commits

Author SHA1 Message Date
Valentin Maerten
442aab1f3e ci(github): use setup-task with output grouping for tests
Restore the --output group options for better GitHub Actions log
grouping, while keeping the separate build job for compilation check.
2025-12-10 21:51:37 +01:00
Valentin Maerten
17576081b3 ci(github): merge build step into test job 2025-12-07 22:46:05 +01:00
Valentin Maerten
2cb7eaa3cc ci(github): improve workflow structure and add build job 2025-12-07 21:48:18 +01:00
Valentin Maerten
8cc70d5922 ci(github): consolidate test and lint into single workflow 2025-12-07 21:48:18 +01:00
renovate[bot]
8cd51af3b0 chore(deps): update all non-major dependencies (#2540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 21:45:11 +01:00
Valentin Maerten
a40ddd4949 refactor: optimize fuzzy matching with lazy initialization (#2523) 2025-12-07 21:43:26 +01:00
Andrey Nering
b1814277c2 docs(changelog): fix typo 2025-12-07 17:32:31 -03:00
Andrey Nering
500ab8b941 docs(changelog): add entry for #2433 2025-12-07 17:31:25 -03:00
Andrey Nering
745633dc0e fix: a couple of fixes and improvements on task --init (#2433)
* Fixed check for an existing Taskfile: look for all possibilities, and
  not only `Taskfile.yml` specifically.
* Added a description (`desc`) to the `default` task. Important to at
  least `task --list` work by default (a core feature).
* Changed top comment to YAML language server comment.
2025-12-07 20:29:51 +00:00
Andrey Nering
9b99866224 feat: add --failfast and failtest: true to control dependencies (#2525) 2025-12-07 17:23:08 -03:00
Valentin Maerten
54e4905432 ci(renovate): track golangci-lint version in workflows (#2557) 2025-12-07 12:53:56 +01:00
Valentin Maerten
c95805e0e0 build(deps): update crypto dependencies (#2555) 2025-12-07 12:44:05 +01:00
Valentin Maerten
4560589652 ci(lint): update golangci-lint-action to v2.7.1 (#2556) 2025-12-07 12:41:44 +01:00
renovate[bot]
084d6444b4 chore(deps): update actions/setup-node action to v6 (#2553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-07 12:28:50 +01:00
Valentin Maerten
3fb7919577 build(deps): upgrade xsync from v3 to v4 (#2554) 2025-12-07 12:28:31 +01:00
Valentin Maerten
69b345efc9 chore: changelog for #2495 2025-12-07 12:21:30 +01:00
Valentin Maerten
4af5278d73 fix: autocomplete works with other binary than 'task' (#2495) 2025-12-07 12:20:45 +01:00
Valentin Maerten
12fbdd3ec7 chore: changelog for #2491 2025-12-07 12:19:02 +01:00
Maciej Lech
72a349b0e9 feat: add --trusted-hosts CLI and remote.trusted-hosts config for remote tasks (#2491)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2025-12-07 12:17:54 +01:00
Valentin Maerten
896d65b21f ci(release): switch to npm trusted publishers with OIDC (#2550) 2025-12-07 09:55:18 +01:00
Valentin Maerten
2161f33b5c chore: changelog for #2536 2025-12-02 20:38:02 +01:00
Valentin Maerten
b93638b97a fix: allow application/octet-stream for a Remote taskfile (#2536) 2025-12-02 20:36:35 +01:00
Valentin Maerten
47b78ca879 chore: changelog for #1844 2025-11-30 10:57:40 +01:00
boiledfroginthewell
f0b15d397b fix: CLI_ARGS completion for fish and zsh (#1844) 2025-11-30 10:55:36 +01:00
Valentin Maerten
eb285fa3d2 chore: changelog for #2513 2025-11-29 12:41:56 +01:00
Valentin Maerten
02b13a687a feat(website): add llms.txt for AI agents (#2513) 2025-11-29 12:40:44 +01:00
Valentin Maerten
a085d62727 feat(completion): add missing flags and dynamic experimental feature detection (#2532) 2025-11-29 12:16:58 +01:00
Valentin Maerten
4ab1958df1 feat(summary): add vars, env, and requires display (#2524) 2025-11-29 11:14:20 +01:00
Valentin Maerten
54ca217b92 fix(release): wrap changelog with v-pre directive (#2526) 2025-11-29 11:05:37 +01:00
renovate[bot]
a6c0c1daba chore(deps): update all non-major dependencies (#2515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 16:31:43 -03:00
renovate[bot]
9cc1c7b40b chore(deps): update actions/checkout action to v6 (#2527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 13:59:45 -03:00
Andrey Nering
7901cce831 chore: run gofumpt 2025-11-22 18:09:50 -03:00
Andrey Nering
c7b4f26900 chore: run modernize 2025-11-22 17:30:30 -03:00
Andrey Nering
3ed403b839 chore(changelog): add entry for #2511 2025-11-22 17:20:46 -03:00
Timothy Rule
386dcbc1a0 fix: adjust run: when_changed to work correctly with imported tasks (#2511) 2025-11-22 17:17:13 -03:00
Andrey Nering
799bc85498 docs(readme): update links 2025-11-19 10:02:58 -03:00
Daniel Thorngren
0d9e8dd71b docs: corrected substr templating example (#2519) 2025-11-18 18:03:48 +00:00
Samuel Krieg
a927ffb31e docs: add tasks.task.dir (#2489) 2025-11-15 17:53:10 +01:00
renovate[bot]
42ad618205 chore(deps): update golangci/golangci-lint-action action to v9 (#2502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 17:51:13 +01:00
Andrey Nering
2b713f564f chore(goreleaser): remove / from branch name
As an attempt to fix the 404 error for `winget`.
2025-11-13 10:40:57 -03:00
Andrey Nering
cb8e94aa33 ci(goreleaser): add /cc to maintainers on winget pr 2025-11-12 09:12:13 -03:00
Andrey Nering
6bc339d714 chore: go mod tidy 2025-11-12 09:12:13 -03:00
Valentin Maerten
5712c463f5 chore: changelog for #2507 2025-11-12 10:27:33 +01:00
Valentin Maerten
78cc6e5fd3 fix: RPM upload for Cloudsmith (#2507) 2025-11-12 10:15:56 +01:00
Valentin Maerten
38e07ea812 fix: changelog for website 2025-11-11 21:54:51 +01:00
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
256 changed files with 18087 additions and 19816 deletions

View File

@@ -8,6 +8,6 @@ charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_style = tab indent_style = tab
[*.{md,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_style = space
indent_size = 2 indent_size = 2

11
.github/renovate.json vendored
View File

@@ -8,6 +8,17 @@
], ],
"mode": "full", "mode": "full",
"addLabels":["area: dependencies"], "addLabels":["area: dependencies"],
"customManagers": [
{
"customType": "regex",
"fileMatch": ["^\\.github/workflows/.*\\.ya?ml$"],
"matchStrings": [
"uses:\\s*golangci/golangci-lint-action@\\S+\\s+with:\\s+version:\\s*(?<currentValue>v[\\d.]+)"
],
"datasourceTemplate": "github-releases",
"depNameTemplate": "golangci/golangci-lint"
}
],
"packageRules": [ "packageRules": [
{ {
"matchManagers": ["github-actions"], "matchManagers": ["github-actions"],

112
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: CI
on:
pull_request:
push:
tags:
- v*
branches:
- main
concurrency:
group: ci-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
build:
name: 🔨 Build (${{ matrix.go-version }})
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: 🔨 Build
run: go build -v ./cmd/task
test:
name: 🧪 Test (${{ matrix.go-version }}, ${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: ⬇️ Setup Task
uses: go-task/setup-task@v1
- name: 🧪 Test
run: task test --output group --output-group-begin '::group::{{.TASK}}' --output-group-end '::endgroup::'
lint:
name: 🔍 Lint (${{ matrix.go-version }})
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: 🔍 Lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.7.1
lint-jsonschema:
name: 📋 Lint JSON Schema
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: ⬇️ Setup Python
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: ⬇️ Install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: 📋 Validate JSON Schema
run: check-jsonschema --check-metaschema website/src/public/schema.json
ci-status:
name: ✅ CI
runs-on: ubuntu-latest
needs: [build, test, lint, lint-jsonschema]
if: always()
steps:
- name: ✅ Check CI status
run: |
if [[ "${{ needs.build.result }}" != "success" ]] || \
[[ "${{ needs.test.result }}" != "success" ]] || \
[[ "${{ needs.lint.result }}" != "success" ]] || \
[[ "${{ needs.lint-jsonschema.result }}" != "success" ]]; then
echo "CI failed"
exit 1
fi
echo "CI passed"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,74 +0,0 @@
name: Lint
on:
pull_request:
push:
tags:
- v*
branches:
- main
jobs:
lint:
name: Lint
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1.0
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: 3.12
- uses: actions/checkout@v4
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/static/schema.json
check_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@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.')
check_schema:
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/static/schema.json
website/static/schema-taskrc.json
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('schema.json or schema-taskrc.json has changed. Instead you need to update next-schema.json or next-schema-taskrc.json.')

View File

@@ -1,4 +1,4 @@
name: Realease nightly name: Release nightly
on: on:
workflow_dispatch: workflow_dispatch:
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: 1.24.x go-version: 1.25.x
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
@@ -27,3 +27,4 @@ jobs:
env: env:
GITHUB_TOKEN: ${{secrets.GH_PAT}} GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}

View File

@@ -5,19 +5,40 @@ on:
tags: tags:
- 'v*' - 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: 1.24.x go-version: 1.25.x
- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Update npm
run: npm install -g npm@latest
- 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 - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
@@ -28,3 +49,9 @@ jobs:
env: env:
GITHUB_TOKEN: ${{secrets.GH_PAT}} GITHUB_TOKEN: ${{secrets.GH_PAT}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}} GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
- name: Deploy Website
shell: bash
run: |
task website:deploy:prod

View File

@@ -1,38 +0,0 @@
name: Test
on:
pull_request:
push:
tags:
- v*
branches:
- main
jobs:
test:
name: Test
strategy:
matrix:
go-version: [1.23.x, 1.24.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
with:
go-version: ${{matrix.go-version}}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Download Go modules
run: go mod download
env:
GOPROXY: https://proxy.golang.org
- name: Build
run: go build -o ./bin/task -v ./cmd/task
- name: Test
run: ./bin/task test --output=group --output-group-begin='::group::{{.TASK}}' --output-group-end='::endgroup::'

View File

@@ -68,7 +68,8 @@ nfpms:
formats: formats:
- deb - deb
- rpm - rpm
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}' - apk
file_name_template: '{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}'
contents: contents:
- src: completion/bash/task.bash - src: completion/bash/task.bash
dst: /etc/bash_completion.d/task dst: /etc/bash_completion.d/task
@@ -126,7 +127,7 @@ winget:
repository: repository:
owner: go-task owner: go-task
name: winget-pkgs name: winget-pkgs
branch: 'chore/task-{{.Version}}' branch: 'task-{{.Version}}'
pull_request: pull_request:
enabled: true enabled: true
draft: false draft: false
@@ -135,3 +136,39 @@ winget:
owner: microsoft owner: microsoft
name: winget-pkgs name: winget-pkgs
branch: master branch: master
body: |
/cc @andreynering @pd93 @vmaerten
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:
- "any-distro/any-version"
component: main
republish: true

1
.nvmrc
View File

@@ -1 +0,0 @@
22.18.0

View File

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

View File

@@ -1,5 +1,129 @@
# Changelog # Changelog
## Unreleased
- A small behavior change was made to dependencies. Task will now wait for all
dependencies to finish running before continuing, even if any of them fail.
To opt for the previous behavior, set `failfast: true` either on your
`.taskrc.yml` or per task, or use the `--failfast` flag, which will also work
for `--parallel` (#1246, #2525 by @andreynering).
- Fix RPM upload to Cloudsmith by including the version in the filename to
ensure unique filenames (#2507 by @vmaerten).
- Fix `run: when_changed` to work properly for Taskfiles included multiple times
(#2508, #2511 by @trulede).
- The `--summary` flag now displays `vars:` (both global and task-level),
`env:`, and `requires:` sections. Dynamic variables show their shell command
(e.g., `sh: echo "hello"`) instead of the evaluated value (#2486 ,#2524 by
@vmaerten).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Improved performance of fuzzy task name matching by implementing lazy
initialization. Added `--disable-fuzzy` flag and `disable-fuzzy` taskrc option
to allow disabling fuzzy matching entirely (#2521, #2523 by @vmaerten).
- Added LLM-optimized documentation via VitePress plugin, generating `llms.txt`
and `llms-full.txt` for AI-powered development tools (#2513 by @vmaerten).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- Added `--trusted-hosts` CLI flag and `remote.trusted-hosts` config option to
skip confirmation prompts for specified hosts when using Remote Taskfiles
(#2491, #2473 by @maciejlech).
- Shell completion now works when Task is installed or aliased under a different
binary name via TASK_EXE environment variable (#2495, #2468 by @vmaerten).
- Some small fixes and improvements were made to `task --init` and to the
default Taskfile it generates (#2433 by @andreynering).
## 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 ## v3.44.1 - 2025-07-23
- Internal tasks will no longer be shown as suggestions since they cannot be - Internal tasks will no longer be shown as suggestions since they cannot be

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://taskfile.dev"> <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> </a>
<h1>Task</h1> <h1>Task</h1>
@@ -10,7 +10,7 @@
</p> </p>
<p> <p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a> <a href="https://taskfile.dev/docs/installation">Installation</a> | <a href="https://taskfile.dev/docs/getting-started">Getting Started</a> | <a href="https://taskfile.dev/docs/guide">Docs</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p> </p>
<h1>Gold Sponsors</h1> <h1>Gold Sponsors</h1>
@@ -19,7 +19,12 @@
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a target="_blank" href="https://devowl.io"> <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> </a>
</td> </td>
</tr> </tr>

View File

@@ -8,6 +8,7 @@ includes:
vars: vars:
BIN: "{{.ROOT_DIR}}/bin" BIN: "{{.ROOT_DIR}}/bin"
GOTESTSUM_FORMAT: '{{if .CI}}github-actions{{else}}pkgname{{end}}'
env: env:
CGO_ENABLED: '0' CGO_ENABLED: '0'
@@ -131,29 +132,37 @@ tasks:
test: test:
desc: Runs test suite desc: Runs test suite
aliases: [t] aliases: [t]
deps: [gotestsum:install]
sources: sources:
- "**/*.go" - "**/*.go"
- "testdata/**/*" - "testdata/**/*"
cmds: cmds:
- go test ./... - gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./...
test:watch: test:watch:
desc: Runs test suite with watch tests included desc: Runs test suite with watch tests included
deps: [sleepit:build] deps: [sleepit:build, gotestsum:install]
cmds: cmds:
- go test ./... -tags 'watch' - gotestsum -f '{{.GOTESTSUM_FORMAT}}' ./... -tags 'watch'
test:all: test:all:
desc: Runs test suite with signals and watch tests included desc: Runs test suite with signals and watch tests included
deps: [sleepit:build] deps: [sleepit:build, gotestsum:install]
cmds: cmds:
- go test -tags 'signals watch' ./... - gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
goreleaser:test: goreleaser:test:
desc: Tests release process without publishing desc: Tests release process without publishing
cmds: cmds:
- goreleaser --snapshot --clean - goreleaser --snapshot --clean
gotestsum:install:
desc: Installs gotestsum
status:
- command -v gotestsum
cmds:
- go install gotest.tools/gotestsum@latest
goreleaser:install: goreleaser:install:
desc: Installs goreleaser desc: Installs goreleaser
cmds: cmds:
@@ -203,7 +212,6 @@ tasks:
Please wait for the CI to finish and then do the following: Please wait for the CI to finish and then do the following:
- Copy the changelog for v{{.VERSION}} to the GitHub release - 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 - Update and push the snapcraft manifest in https://github.com/go-task/snap/blob/main/snap/snapcraft.yaml
preconditions: preconditions:
- sh: test $(git rev-parse --abbrev-ref HEAD) = "main" - sh: test $(git rev-parse --abbrev-ref HEAD) = "main"
@@ -222,8 +230,3 @@ tasks:
- "git push origin tag v{{.VERSION}}" - "git push origin tag v{{.VERSION}}"
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}' - cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
silent: true silent: true
npm:publish:
desc: Publish release to npm
cmds:
- npm publish --access=public

View File

@@ -3,33 +3,23 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/otiai10/copy"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )
const ( const (
changelogSource = "CHANGELOG.md" changelogSource = "CHANGELOG.md"
changelogTarget = "website/docs/changelog.mdx" changelogTarget = "website/src/docs/changelog.md"
docsSource = "website/docs" versionFile = "internal/version/version.txt"
docsTarget = "website/versioned_docs/version-latest"
schemaSource = "website/static/next-schema.json"
schemaTarget = "website/static/schema.json"
schemaTaskrcSource = "website/static/next-schema-taskrc.json"
schemaTaskrcTarget = "website/static/schema-taskrc.json"
) )
var ( var changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
changelogReleaseRegex = regexp.MustCompile(`## Unreleased`)
versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`)
)
// Flags // Flags
var ( var (
@@ -53,7 +43,7 @@ func release() error {
return errors.New("error: expected version number") return errors.New("error: expected version number")
} }
version, err := getVersion() version, err := getVersion(versionFile)
if err != nil { if err != nil {
return err return err
} }
@@ -71,36 +61,18 @@ func release() error {
return err return err
} }
if err := setVersionFile("internal/version/version.txt", version); err != nil { if err := setVersionFile(versionFile, 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 {
return err
}
if err := schema(); err != nil {
return err return err
} }
return nil return nil
} }
func getVersion() (*semver.Version, error) { func getVersion(filename string) (*semver.Version, error) {
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0") b, err := os.ReadFile(filename)
b, err := cmd.Output()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return semver.NewVersion(strings.TrimSpace(string(b))) return semver.NewVersion(strings.TrimSpace(string(b)))
} }
@@ -149,47 +121,17 @@ func changelog(version *semver.Version) error {
return err return err
} }
// Wrap the changelog content with v-pre directive for VitePress to prevent
// Vue from interpreting template syntax like {{.TASK_VERSION}}
changelogWithVPre := strings.Replace(changelog, "# Changelog\n\n", "# Changelog\n\n::: v-pre\n\n", 1) + "\n:::"
// Add the frontmatter to the changelog // Add the frontmatter to the changelog
changelog = fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelog) changelogWithFrontmatter := fmt.Sprintf("---\n%s\n---\n\n%s", frontmatter, changelogWithVPre)
// Write the changelog to the target file // Write the changelog to the target file
return os.WriteFile(changelogTarget, []byte(changelog), 0o644) return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644)
} }
func setVersionFile(fileName string, version *semver.Version) error { func setVersionFile(fileName string, version *semver.Version) error {
return os.WriteFile(fileName, []byte(version.String()+"\n"), 0o644) 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
}
func schema() error {
if err := copy.Copy(schemaSource, schemaTarget); err != nil {
return err
}
if err := copy.Copy(schemaTaskrcSource, schemaTaskrcTarget); err != nil {
return err
}
return nil
}

View File

@@ -128,6 +128,7 @@ func run() error {
flags.ListAll, flags.ListAll,
flags.ListJson, flags.ListJson,
flags.NoStatus, flags.NoStatus,
flags.Nested,
) )
if listOptions.ShouldListTasks() { if listOptions.ShouldListTasks() {
if flags.Silent { if flags.Silent {

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

@@ -61,13 +61,14 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
newVar := templater.ReplaceVar(v, cache) newVar := templater.ReplaceVar(v, cache)
// If the variable should not be evaluated, but is nil, set it to an empty string // If the variable should not be evaluated, but is nil, set it to an empty string
// This stops empty interface errors when using the templater to replace values later // This stops empty interface errors when using the templater to replace values later
// Preserve the Sh field so it can be displayed in summary
if !evaluateShVars && newVar.Value == nil { if !evaluateShVars && newVar.Value == nil {
result.Set(k, ast.Var{Value: ""}) result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
return nil return nil
} }
// If the variable should not be evaluated and it is set, we can set it and return // If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars { if !evaluateShVars {
result.Set(k, ast.Var{Value: newVar.Value}) result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
return nil return nil
} }
// Now we can check for errors since we've handled all the cases when we don't want to evaluate // Now we can check for errors since we've handled all the cases when we don't want to evaluate

View File

@@ -1,6 +1,7 @@
# vim: set tabstop=2 shiftwidth=2 expandtab: # vim: set tabstop=2 shiftwidth=2 expandtab:
_GO_TASK_COMPLETION_LIST_OPTION='--list-all' _GO_TASK_COMPLETION_LIST_OPTION='--list-all'
TASK_CMD="${TASK_EXE:-task}"
function _task() function _task()
{ {
@@ -52,4 +53,4 @@ function _task()
__ltrim_colon_completions "$cur" __ltrim_colon_completions "$cur"
} }
complete -F _task task complete -F _task "$TASK_CMD"

View File

@@ -1,4 +1,31 @@
set -l GO_TASK_PROGNAME task set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
# Cache variables for experiments (global)
set -g __task_experiments_cache ""
set -g __task_experiments_cache_time 0
# Helper function to get experiments with 1-second cache
function __task_get_experiments
set -l now (date +%s)
set -l ttl 1 # Cache for 1 second only
# Return cached value if still valid
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
printf '%s\n' $__task_experiments_cache
return
end
# Refresh cache
set -g __task_experiments_cache (task --experiments 2>/dev/null)
set -g __task_experiments_cache_time $now
printf '%s\n' $__task_experiments_cache
end
# Helper function to check if an experiment is enabled
function __task_is_experiment_enabled
set -l experiment $argv[1]
__task_get_experiments | string match -qr "^\* $experiment:.*on"
end
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
# Check if the global task is requested # Check if the global task is requested
@@ -33,22 +60,56 @@ function __task_get_tasks --description "Prints all available tasks with their d
end end
end end
complete -c $GO_TASK_PROGNAME -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was complete -c $GO_TASK_PROGNAME \
specified.' -xa "(__task_get_tasks)" -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was specified.' \
-xa "(__task_get_tasks)" \
-n "not __fish_seen_subcommand_from --"
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)' # Standard flags
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'sets directory of execution' complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks'
complete -c $GO_TASK_PROGNAME -l dry -d 'compiles and prints tasks in the order that they would be run, without executing them' complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
complete -c $GO_TASK_PROGNAME -s f -l force -d 'forces execution even when the task is up-to-date' complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'shows Task usage' complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell"
complete -c $GO_TASK_PROGNAME -s i -l init -d 'creates a new Taskfile.yml in the current folder' complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution'
complete -c $GO_TASK_PROGNAME -s l -l list -d 'lists tasks with description of current Taskfile' complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'sets output style: [interleaved|group|prefixed]' -xa "interleaved group prefixed" complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing'
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'executes tasks provided on command line in parallel' complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disables echoing' complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments'
complete -c $GO_TASK_PROGNAME -l status -d 'exits with non-zero exit code if any of the given tasks is not up-to-date' complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails'
complete -c $GO_TASK_PROGNAME -l summary -d 'show summary about a task' complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date'
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose which Taskfile to run. Defaults to "Taskfile.yml"' complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory'
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'enables verbose mode' complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help'
complete -c $GO_TASK_PROGNAME -l version -d 'show Task version' complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'enables watch of the given task' complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads'
complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes'
complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON'
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks'
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing'
complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none"
complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date'
complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary'
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run'
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output'
complete -c $GO_TASK_PROGNAME -l version -d 'show version'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes'
complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts'
# Experimental flags (dynamically checked at completion time via -n condition)
# GentleForce experiment
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'
# RemoteTaskfiles experiment - Options
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
# RemoteTaskfiles experiment - Operations
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l clear-cache -d 'clear remote Taskfile cache'

View File

@@ -5,22 +5,81 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
if ($commandName.StartsWith('-')) { if ($commandName.StartsWith('-')) {
$completions = @( $completions = @(
[CompletionResult]::new('--list-all ', '--list-all ', [CompletionResultType]::ParameterName, 'list all tasks'), # Standard flags (alphabetical order)
[CompletionResult]::new('--color ', '--color', [CompletionResultType]::ParameterName, '--color'), [CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('--concurrency=', '--concurrency=', [CompletionResultType]::ParameterName, 'concurrency'), [CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('--interval=', '--interval=', [CompletionResultType]::ParameterName, 'interval'), [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('--output=interleaved ', '--output=interleaved', [CompletionResultType]::ParameterName, '--output='), [CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('--output=group ', '--output=group', [CompletionResultType]::ParameterName, '--output='), [CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--output=prefixed ', '--output=prefixed', [CompletionResultType]::ParameterName, '--output='), [CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--dry ', '--dry', [CompletionResultType]::ParameterName, '--dry'), [CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),
[CompletionResult]::new('--force ', '--force', [CompletionResultType]::ParameterName, '--force'), [CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--parallel ', '--parallel', [CompletionResultType]::ParameterName, '--parallel'), [CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--silent ', '--silent', [CompletionResultType]::ParameterName, '--silent'), [CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),
[CompletionResult]::new('--status ', '--status', [CompletionResultType]::ParameterName, '--status'), [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('--verbose ', '--verbose', [CompletionResultType]::ParameterName, '--verbose'), [CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('--watch ', '--watch', [CompletionResultType]::ParameterName, '--watch') [CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),
[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),
[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),
[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),
[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),
[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),
[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),
[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),
[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),
[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),
[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes')
) )
# Experimental flags (dynamically added based on enabled experiments)
$experiments = & task --experiments 2>$null | Out-String
if ($experiments -match '\* GENTLE_FORCE:.*on') {
$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')
}
if ($experiments -match '\* REMOTE_TASKFILES:.*on') {
# Options
$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
# Operations
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')
}
return $completions.Where{ $_.CompletionText.StartsWith($commandName) } return $completions.Where{ $_.CompletionText.StartsWith($commandName) }
} }

View File

@@ -1,36 +1,47 @@
#compdef task #compdef task
compdef _task task
typeset -A opt_args typeset -A opt_args
TASK_CMD="${TASK_EXE:-task}"
compdef _task "$TASK_CMD"
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}" _GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
# Check if an experiment is enabled
function __task_is_experiment_enabled() {
local experiment=$1
task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on"
}
# Listing commands from Taskfile.yml # Listing commands from Taskfile.yml
function __task_list() { function __task_list() {
local -a scripts cmd local -a scripts cmd
local -i enabled=0 local -i enabled=0
local taskfile item task desc local taskfile item task desc
cmd=(task) cmd=($TASK_CMD)
taskfile=${(Qv)opt_args[(i)-t|--taskfile]} taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
taskfile=${taskfile//\~/$HOME} taskfile=${taskfile//\~/$HOME}
for arg in "${words[@]:0:$CURRENT}"; do
if [[ "$arg" = "--" ]]; then
# Use default completion for words after `--` as they are CLI_ARGS.
_default
return 0
fi
done
if [[ -n "$taskfile" && -f "$taskfile" ]]; then if [[ -n "$taskfile" && -f "$taskfile" ]]; then
enabled=1
cmd+=(--taskfile "$taskfile") cmd+=(--taskfile "$taskfile")
else fi
for taskfile in {T,t}askfile{,.dist}.{yaml,yml}; do
if [[ -f "$taskfile" ]]; then
enabled=1 if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
break enabled=1
fi
done
fi fi
(( enabled )) || return 0 (( enabled )) || return 0
scripts=() 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:]]*}" task="${item%%:[[:space:]]*}"
desc="${item##[^[:space:]]##[[:space:]]##}" desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" ) scripts+=( "${task//:/\\:}:$desc" )
@@ -39,29 +50,78 @@ function __task_list() {
} }
_task() { _task() {
_arguments \ local -a standard_args operation_args
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \ standard_args=(
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \ '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '
'(-c --color)'{-c,--color}'[colored output]' \ '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \ '(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'
'(--dry)--dry[dry-run mode, compile and print tasks only]' \ '(-f --force)'{-f,--force}'[run even if task is up-to-date]'
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \ '(-c --color)'{-c,--color}'[colored output]'
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \ '(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \ '(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'
'(-s --silent)'{-s,--silent}'[disable echoing]' \ '(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \ '(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'
'(--summary)--summary[show summary\: field from tasks instead of running them]' \ '(--dry)--dry[dry-run mode, compile and print tasks only]'
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \ '(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \ '(--experiments)--experiments[list available experiments]'
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \ '(-g --global)'{-g,--global}'[run global Taskfile from home directory]'
+ '(operation)' \ '(--insecure)--insecure[allow insecure Taskfile downloads]'
{-l,--list}'[list describable tasks]' \ '(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '
{-a,--list-all}'[list all tasks]' \ '(-j --json)'{-j,--json}'[format task list as JSON]'
{-i,--init}'[create new Taskfile.yml]' \ '(--nested)--nested[nest namespaces when listing as JSON]'
'(-*)'{-h,--help}'[show help]' \ '(--no-status)--no-status[ignore status when listing as JSON]'
'(-*)--version[show version and exit]' \ '(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
'*: :__task_list' '(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
'(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'
'(-s --silent)'{-s,--silent}'[disable echoing]'
'(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'
'(--status)--status[exit non-zero if supplied tasks not up-to-date]'
'(--summary)--summary[show summary\: field from tasks instead of running them]'
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'
'(-v --verbose)'{-v,--verbose}'[verbose mode]'
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'
'(-y --yes)'{-y,--yes}'[assume yes to all prompts]'
)
# Experimental flags (dynamically added based on enabled experiments)
# Options (modify behavior)
if __task_is_experiment_enabled "GENTLE_FORCE"; then
standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')
fi
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
standard_args+=(
'(--offline --download)--offline[use only local or cached Taskfiles]'
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
'(--expiry)--expiry[cache expiry duration]:duration: '
)
fi
operation_args=(
# Task names completion (can be specified multiple times)
'(operation)*: :__task_list'
# Operational args completion (mutually exclusive)
+ '(operation)'
'(*)'{-l,--list}'[list describable tasks]'
'(*)'{-a,--list-all}'[list all tasks]'
'(*)'{-i,--init}'[create new Taskfile.yml]'
'(- *)'{-h,--help}'[show help]'
'(- *)--version[show version and exit]'
)
# Experimental operations (dynamically added based on enabled experiments)
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
standard_args+=(
'(--offline --clear-cache)--download[download remote Taskfile]'
)
operation_args+=(
'(* --download)--clear-cache[clear remote Taskfile cache]'
)
fi
_arguments -S $standard_args $operation_args
} }
# don't run the completion function when being source-ed or eval-ed # don't run the completion function when being source-ed or eval-ed

View File

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

View File

@@ -54,6 +54,10 @@ func (err *TaskRunError) TaskExitCode() int {
return err.Code() return err.Code()
} }
func (err *TaskRunError) Unwrap() error {
return err.Err
}
// TaskInternalError when the user attempts to invoke a task that is internal. // TaskInternalError when the user attempts to invoke a task that is internal.
type TaskInternalError struct { type TaskInternalError struct {
TaskName string TaskName string
@@ -162,7 +166,7 @@ func (v MissingVar) String() string {
} }
func (err *TaskMissingRequiredVarsError) Error() string { func (err *TaskMissingRequiredVarsError) Error() string {
var vars []string vars := make([]string, 0, len(err.MissingVars))
for _, v := range err.MissingVars { for _, v := range err.MissingVars {
vars = append(vars, v.String()) vars = append(vars, v.String())
} }

View File

@@ -11,14 +11,18 @@ import (
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when // TaskfileNotFoundError is returned when no appropriate Taskfile is found when
// searching the filesystem. // searching the filesystem.
type TaskfileNotFoundError struct { type TaskfileNotFoundError struct {
URI string URI string
Walk bool Walk bool
AskInit bool
} }
func (err TaskfileNotFoundError) Error() string { func (err TaskfileNotFoundError) Error() string {
var walkText string var walkText string
if err.Walk { 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) return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
} }

View File

@@ -7,7 +7,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/puzpuzpuz/xsync/v3" "github.com/puzpuzpuz/xsync/v4"
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -34,11 +34,13 @@ type (
Insecure bool Insecure bool
Download bool Download bool
Offline bool Offline bool
TrustedHosts []string
Timeout time.Duration Timeout time.Duration
CacheExpiryDuration time.Duration CacheExpiryDuration time.Duration
Watch bool Watch bool
Verbose bool Verbose bool
Silent bool Silent bool
DisableFuzzy bool
AssumeYes bool AssumeYes bool
AssumeTerm bool // Used for testing AssumeTerm bool // Used for testing
Dry bool Dry bool
@@ -47,6 +49,7 @@ type (
Color bool Color bool
Concurrency int Concurrency int
Interval time.Duration Interval time.Duration
Failfast bool
// I/O // I/O
Stdin io.Reader Stdin io.Reader
@@ -63,14 +66,15 @@ type (
UserWorkingDir string UserWorkingDir string
EnableVersionCheck bool EnableVersionCheck bool
fuzzyModel *fuzzy.Model fuzzyModel *fuzzy.Model
fuzzyModelOnce sync.Once
concurrencySemaphore chan struct{} concurrencySemaphore chan struct{}
taskCallCount map[string]*int32 taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context executionHashes map[string]context.Context
executionHashesMutex sync.Mutex executionHashesMutex sync.Mutex
watchedDirs *xsync.MapOf[string, bool] watchedDirs *xsync.Map[string, bool]
} }
TempDir struct { TempDir struct {
Remote string Remote string
@@ -225,6 +229,20 @@ func (o *offlineOption) ApplyToExecutor(e *Executor) {
e.Offline = o.offline e.Offline = o.offline
} }
// WithTrustedHosts configures the [Executor] with a list of trusted hosts for remote
// Taskfiles. Hosts in this list will not prompt for user confirmation.
func WithTrustedHosts(trustedHosts []string) ExecutorOption {
return &trustedHostsOption{trustedHosts}
}
type trustedHostsOption struct {
trustedHosts []string
}
func (o *trustedHostsOption) ApplyToExecutor(e *Executor) {
e.TrustedHosts = o.trustedHosts
}
// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By // WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
// default, the timeout is set to 10 seconds. // default, the timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ExecutorOption { func WithTimeout(timeout time.Duration) ExecutorOption {
@@ -240,7 +258,7 @@ func (o *timeoutOption) ApplyToExecutor(e *Executor) {
} }
// WithCacheExpiryDuration sets the duration after which the cache is considered // 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 { func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration} return &cacheExpiryDurationOption{duration: duration}
} }
@@ -296,6 +314,19 @@ func (o *silentOption) ApplyToExecutor(e *Executor) {
e.Silent = o.silent e.Silent = o.silent
} }
// WithDisableFuzzy tells the [Executor] to disable fuzzy matching for task names.
func WithDisableFuzzy(disableFuzzy bool) ExecutorOption {
return &disableFuzzyOption{disableFuzzy}
}
type disableFuzzyOption struct {
disableFuzzy bool
}
func (o *disableFuzzyOption) ApplyToExecutor(e *Executor) {
e.DisableFuzzy = o.disableFuzzy
}
// WithAssumeYes tells the [Executor] to assume "yes" for all prompts. // WithAssumeYes tells the [Executor] to assume "yes" for all prompts.
func WithAssumeYes(assumeYes bool) ExecutorOption { func WithAssumeYes(assumeYes bool) ExecutorOption {
return &assumeYesOption{assumeYes} return &assumeYesOption{assumeYes}
@@ -502,3 +533,16 @@ type versionCheckOption struct {
func (o *versionCheckOption) ApplyToExecutor(e *Executor) { func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
e.EnableVersionCheck = o.enableVersionCheck e.EnableVersionCheck = o.enableVersionCheck
} }
// WithFailfast tells the [Executor] whether or not to check the version of
func WithFailfast(failfast bool) ExecutorOption {
return &failfastOption{failfast}
}
type failfastOption struct {
failfast bool
}
func (o *failfastOption) ApplyToExecutor(e *Executor) {
e.Failfast = o.failfast
}

View File

@@ -3,7 +3,6 @@ package task_test
import ( import (
"bytes" "bytes"
"cmp" "cmp"
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -144,12 +143,12 @@ func (tt *ExecutorTest) run(t *testing.T) {
t.Helper() t.Helper()
f := func(t *testing.T) { f := func(t *testing.T) {
t.Helper() t.Helper()
var buf bytes.Buffer var buffer SyncBuffer
opts := append( opts := append(
tt.executorOpts, tt.executorOpts,
task.WithStdout(&buf), task.WithStdout(&buffer),
task.WithStderr(&buf), task.WithStderr(&buffer),
) )
// If the test has input, create a reader for it and add it to the // If the test has input, create a reader for it and add it to the
@@ -172,7 +171,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
if err := e.Setup(); tt.wantSetupError { if err := e.Setup(); tt.wantSetupError {
require.Error(t, err) require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err) tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf) tt.writeFixtureBuffer(t, g, buffer.buf)
return return
} else { } else {
require.NoError(t, err) require.NoError(t, err)
@@ -189,11 +188,11 @@ func (tt *ExecutorTest) run(t *testing.T) {
} }
// Run the task and check for errors // Run the task and check for errors
ctx := context.Background() ctx := t.Context()
if err := e.Run(ctx, call); tt.wantRunError { if err := e.Run(ctx, call); tt.wantRunError {
require.Error(t, err) require.Error(t, err)
tt.writeFixtureErrRun(t, g, err) tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buf) tt.writeFixtureBuffer(t, g, buffer.buf)
return return
} else { } else {
require.NoError(t, err) require.NoError(t, err)
@@ -206,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) // Run the test (with a name if it has one)
@@ -622,6 +621,30 @@ func TestAlias(t *testing.T) {
) )
} }
func TestSummaryWithVarsAndRequires(t *testing.T) {
t.Parallel()
// Test basic case from prompt.md - vars and requires
NewExecutorTest(t,
WithName("vars-and-requires"),
WithExecutorOptions(
task.WithDir("testdata/summary-vars-requires"),
task.WithSummary(true),
),
WithTask("mytask"),
)
// Test with shell variables
NewExecutorTest(t,
WithName("shell-vars"),
WithExecutorOptions(
task.WithDir("testdata/summary-vars-requires"),
task.WithSummary(true),
),
WithTask("with-sh-var"),
)
}
func TestLabel(t *testing.T) { func TestLabel(t *testing.T) {
t.Parallel() t.Parallel()
@@ -666,6 +689,15 @@ func TestLabel(t *testing.T) {
), ),
WithTask("foo"), WithTask("foo"),
) )
NewExecutorTest(t,
WithName("label in error"),
WithExecutorOptions(
task.WithDir("testdata/label_error"),
),
WithTask("foo"),
WithRunError(),
)
} }
func TestPromptInSummary(t *testing.T) { func TestPromptInSummary(t *testing.T) {
@@ -988,3 +1020,50 @@ func TestIncludeChecksum(t *testing.T) {
WithFixtureTemplating(), WithFixtureTemplating(),
) )
} }
func TestFailfast(t *testing.T) {
t.Parallel()
t.Run("Default", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Option", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/failfast/default"),
task.WithSilent(true),
task.WithFailfast(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
t.Run("Task", func(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("task"),
WithExecutorOptions(
task.WithDir("testdata/failfast/task"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
WithRunError(),
)
})
}

View File

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

38
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/go-task/task/v3 module github.com/go-task/task/v3
go 1.23.0 go 1.24.0
require ( require (
github.com/Ladicle/tabwriter v1.0.0 github.com/Ladicle/tabwriter v1.0.0
@@ -12,50 +12,56 @@ require (
github.com/elliotchance/orderedmap/v3 v3.1.0 github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2 github.com/go-git/go-billy/v5 v5.7.0
github.com/go-git/go-git/v5 v5.16.2 github.com/go-git/go-git/v5 v5.16.4
github.com/go-task/slim-sprig/v3 v3.0.0 github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.2.0 github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.1 github.com/puzpuzpuz/xsync/v4 v4.2.0
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0 github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.7.1 github.com/sebdah/goldie/v2 v2.8.0
github.com/spf13/pflag v1.0.7 github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/zeebo/xxh3 v1.0.2 github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.16.0 go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/term v0.33.0 golang.org/x/sync v0.18.0
gopkg.in/yaml.v3 v3.0.1 golang.org/x/term v0.37.0
mvdan.cc/sh/moreinterp v0.0.0-20251109230715-65adef8e2c5b
mvdan.cc/sh/v3 v3.12.0 mvdan.cc/sh/v3 v3.12.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // 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/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // 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/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-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/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/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // 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 github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.38.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

82
go.sum
View File

@@ -7,16 +7,14 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 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 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 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/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= 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/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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 h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -36,6 +34,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 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 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/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 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
@@ -52,10 +52,12 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
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 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 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-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -76,8 +78,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/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 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 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 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -94,10 +100,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/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 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -105,46 +109,54 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= 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.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 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 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/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 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -154,14 +166,14 @@ 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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -173,5 +185,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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/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 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=

65
help.go
View File

@@ -24,15 +24,17 @@ type ListOptions struct {
ListAllTasks bool ListAllTasks bool
FormatTaskListAsJSON bool FormatTaskListAsJSON bool
NoStatus bool NoStatus bool
Nested bool
} }
// NewListOptions creates a new ListOptions instance // 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{ return ListOptions{
ListOnlyTasksWithDescriptions: list, ListOnlyTasksWithDescriptions: list,
ListAllTasks: listAll, ListAllTasks: listAll,
FormatTaskListAsJSON: listAsJson, FormatTaskListAsJSON: listAsJson,
NoStatus: noStatus, NoStatus: noStatus,
Nested: nested,
} }
} }
@@ -63,7 +65,7 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
return false, err return false, err
} }
if o.FormatTaskListAsJSON { if o.FormatTaskListAsJSON {
output, err := e.ToEditorOutput(tasks, o.NoStatus) output, err := e.ToEditorOutput(tasks, o.NoStatus, o.Nested)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -135,33 +137,17 @@ func (e *Executor) ListTaskNames(allTasks bool) error {
return nil return nil
} }
func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Taskfile, error) { func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool, nested bool) (*editors.Namespace, error) {
o := &editors.Taskfile{
Tasks: make([]editors.Task, len(tasks)),
Location: e.Taskfile.Location,
}
var g errgroup.Group 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 { for i := range tasks {
aliases := []string{}
if len(tasks[i].Aliases) > 0 {
aliases = tasks[i].Aliases
}
g.Go(func() error { g.Go(func() error {
o.Tasks[i] = editors.Task{ editorTask := editors.NewTask(tasks[i])
Name: tasks[i].Name(),
Task: tasks[i].Task,
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,
},
}
if noStatus { if noStatus {
editorTasks[i] = editorTask
return nil return nil
} }
@@ -180,10 +166,35 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
return err return err
} }
o.Tasks[i].UpToDate = upToDate editorTask.UpToDate = &upToDate
editorTasks[i] = editorTask
return nil 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()
} }

25
init.go
View File

@@ -6,9 +6,10 @@ import (
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
) )
const defaultTaskFilename = "Taskfile.yml" const defaultFilename = "Taskfile.yml"
//go:embed taskfile/templates/default.yml //go:embed taskfile/templates/default.yml
var DefaultTaskfile string var DefaultTaskfile string
@@ -20,22 +21,30 @@ var DefaultTaskfile string
// //
// The final file path is always returned and may be different from the input path. // The final file path is always returned and may be different from the input path.
func InitTaskfile(path string) (string, error) { func InitTaskfile(path string) (string, error) {
fi, err := os.Stat(path) info, err := os.Stat(path)
if err == nil && !fi.IsDir() { if err == nil && !info.IsDir() {
return path, errors.TaskfileAlreadyExistsError{} return path, errors.TaskfileAlreadyExistsError{}
} }
if fi != nil && fi.IsDir() { if info != nil && info.IsDir() {
path = filepathext.SmartJoin(path, defaultTaskFilename) // path was a directory, check if there is a Taskfile already
// path was a directory, so check if Taskfile.yml exists in it if hasDefaultTaskfile(path) {
if _, err := os.Stat(path); err == nil {
return path, errors.TaskfileAlreadyExistsError{} return path, errors.TaskfileAlreadyExistsError{}
} }
path = filepathext.SmartJoin(path, defaultFilename)
} }
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil { if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
return path, err return path, err
} }
return path, nil return path, nil
} }
func hasDefaultTaskfile(dir string) bool {
for _, name := range taskfile.DefaultTaskfiles {
if _, err := os.Stat(filepathext.SmartJoin(dir, name)); err == nil {
return true
}
}
return false
}

View File

@@ -1,10 +1,15 @@
package editors package editors
import (
"github.com/go-task/task/v3/taskfile/ast"
)
type ( type (
// Taskfile wraps task list output for use in editor integrations (e.g. VSCode, etc) // Namespace wraps task list output for use in editor integrations (e.g. VSCode, etc)
Taskfile struct { Namespace struct {
Tasks []Task `json:"tasks"` Tasks []Task `json:"tasks"`
Location string `json:"location"` Namespaces map[string]*Namespace `json:"namespaces,omitempty"`
Location string `json:"location,omitempty"`
} }
// Task describes a single task // Task describes a single task
Task struct { Task struct {
@@ -13,7 +18,7 @@ type (
Desc string `json:"desc"` Desc string `json:"desc"`
Summary string `json:"summary"` Summary string `json:"summary"`
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
UpToDate bool `json:"up_to_date"` UpToDate *bool `json:"up_to_date,omitempty"`
Location *Location `json:"location"` Location *Location `json:"location"`
} }
// Location describes a task's location in a taskfile // Location describes a task's location in a taskfile
@@ -23,3 +28,59 @@ type (
Taskfile string `json:"taskfile"` 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)
}

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" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"mvdan.cc/sh/moreinterp/coreutils"
"mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
@@ -59,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
r, err := interp.New( r, err := interp.New(
interp.Params(params...), interp.Params(params...),
interp.Env(expand.ListEnviron(environ...)), interp.Env(expand.ListEnviron(environ...)),
interp.ExecHandlers(execHandler), interp.ExecHandlers(execHandlers()...),
interp.OpenHandler(openHandler), interp.OpenHandler(openHandler),
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr), interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
dirOption(opts.Dir), dirOption(opts.Dir),
@@ -143,8 +143,11 @@ func ExpandFields(s string) ([]string, error) {
return expand.Fields(cfg, words...) return expand.Fields(cfg, words...)
} }
func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc { func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc) {
return interp.DefaultExecHandler(15 * time.Second) if useGoCoreUtils {
handlers = append(handlers, coreutils.ExecHandler)
}
return handlers
} }
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {

View File

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

View File

@@ -5,7 +5,6 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -13,9 +12,10 @@ import (
"github.com/go-task/task/v3" "github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments" "github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast" "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...] const usage = `Usage: task [flags...] [task...]
@@ -51,12 +51,14 @@ var (
TaskSort string TaskSort string
Status bool Status bool
NoStatus bool NoStatus bool
Nested bool
Insecure bool Insecure bool
Force bool Force bool
ForceAll bool ForceAll bool
Watch bool Watch bool
Verbose bool Verbose bool
Silent bool Silent bool
DisableFuzzy bool
AssumeYes bool AssumeYes bool
Dry bool Dry bool
Summary bool Summary bool
@@ -68,10 +70,12 @@ var (
Output ast.Output Output ast.Output
Color bool Color bool
Interval time.Duration Interval time.Duration
Failfast bool
Global bool Global bool
Experiments bool Experiments bool
Download bool Download bool
Offline bool Offline bool
TrustedHosts []string
ClearCache bool ClearCache bool
Timeout time.Duration Timeout time.Duration
CacheExpiryDuration time.Duration CacheExpiryDuration time.Duration
@@ -95,7 +99,9 @@ func init() {
// Parse the experiments // Parse the experiments
dir = cmp.Or(dir, filepath.Dir(entrypoint)) dir = cmp.Or(dir, filepath.Dir(entrypoint))
experiments.Parse(dir)
config, _ := taskrc.GetConfig(dir)
experiments.ParseWithConfig(dir, config)
// Parse the rest of the flags // Parse the rest of the flags
log.SetFlags(0) log.SetFlags(0)
@@ -104,10 +110,7 @@ func init() {
log.Print(usage) log.Print(usage)
pflag.PrintDefaults() 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.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
@@ -118,10 +121,12 @@ func init() {
pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].") 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(&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(&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(&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(&Silent, "silent", "s", false, "Disables echoing.")
pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.") 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.") pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.") pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
@@ -134,8 +139,9 @@ func init() {
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.") pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.") pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.") pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number 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.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, func() *bool { return &config.Failfast }, false), "When running tasks in parallel, stop all tasks if one fails.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.") pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.") pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -150,12 +156,12 @@ func init() {
// Remote Taskfiles experiment will adds the "download" and "offline" flags // Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled() { if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.") pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached 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", time.Second*10, "Timeout for downloading remote Taskfiles.") pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", config.Remote.TrustedHosts, "List of trusted hosts for remote Taskfiles (comma-separated).")
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.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() pflag.Parse()
} }
@@ -196,6 +202,10 @@ func Validate() error {
return errors.New("task: --no-status only applies to --json with --list or --list-all") 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 return nil
} }
@@ -234,11 +244,13 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithInsecure(Insecure), task.WithInsecure(Insecure),
task.WithDownload(Download), task.WithDownload(Download),
task.WithOffline(Offline), task.WithOffline(Offline),
task.WithTrustedHosts(TrustedHosts),
task.WithTimeout(Timeout), task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration), task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithWatch(Watch), task.WithWatch(Watch),
task.WithVerbose(Verbose), task.WithVerbose(Verbose),
task.WithSilent(Silent), task.WithSilent(Silent),
task.WithDisableFuzzy(DisableFuzzy),
task.WithAssumeYes(AssumeYes), task.WithAssumeYes(AssumeYes),
task.WithDry(Dry || Status), task.WithDry(Dry || Status),
task.WithSummary(Summary), task.WithSummary(Summary),
@@ -249,5 +261,19 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithOutputStyle(Output), task.WithOutputStyle(Output),
task.WithTaskSorter(sorter), task.WithTaskSorter(sorter),
task.WithVersionCheck(true), task.WithVersionCheck(true),
task.WithFailfast(Failfast),
) )
} }
// 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 "" return ""
} }
// Search will look for files with the given possible filenames using the given // ResolveDir returns an absolute path to the directory that the task should be
// entrypoint and directory. If the entrypoint is set, it will check if the // 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 // 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 // possible filenames. Otherwise, it walks up the file tree starting at the
// given directory and perform a search in each directory for the possible // 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 // 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 // entrypoint and directory are both empty, it defaults the directory to the
// current working directory and perform a recursive search starting there. If a // current working directory and performs a recursive search starting there. If
// match is found, the absolute path to the file will be returned with its // a match is found, the absolute path to the file is returned with its
// directory. If no match is found, an error will be returned. // directory. If no match is found, an error is returned.
func Search(entrypoint, dir string, possibleFilenames []string) (string, string, error) { func Search(entrypoint, dir string, possibleFilenames []string) (string, error) {
var err error var err error
if entrypoint != "" { if entrypoint != "" {
entrypoint, err = SearchPath(entrypoint, possibleFilenames) entrypoint, err = SearchPath(entrypoint, possibleFilenames)
if err != nil { if err != nil {
return "", "", err return "", err
} }
if dir == "" { return entrypoint, nil
dir = filepath.Dir(entrypoint)
} else {
dir, err = filepath.Abs(dir)
if err != nil {
return "", "", err
}
}
return entrypoint, dir, nil
} }
if dir == "" { if dir == "" {
dir, err = os.Getwd() dir, err = os.Getwd()
if err != nil { if err != nil {
return "", "", err return "", err
} }
} }
entrypoint, err = SearchPathRecursively(dir, possibleFilenames) entrypoint, err = SearchPathRecursively(dir, possibleFilenames)
if err != nil { if err != nil {
return "", "", err return "", err
} }
dir = filepath.Dir(entrypoint) return entrypoint, nil
return entrypoint, dir, nil
} }
// Search will check if a file at the given path exists or not. If it does, it // SearchAll looks for files with the given possible filenames using the given
// will return the path to it. If it does not, it will search for any files at // entrypoint and directory. If the entrypoint is set, it checks if the
// the given path with any of the given possible names. If any of these match a // entrypoint matches a file or if it matches a directory containing one of the
// file, the first matching path will be returned. If no files are found, an // 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. // error will be returned.
func SearchPath(path string, possibleFilenames []string) (string, error) { func SearchPath(path string, possibleFilenames []string) (string, error) {
// Get file info about the path // Get file info about the path
@@ -111,36 +147,56 @@ func SearchPath(path string, possibleFilenames []string) (string, error) {
return "", os.ErrNotExist return "", os.ErrNotExist
} }
// SearchRecursively will check if a file at the given path exists by calling // SearchPathRecursively walks up the directory tree starting at the given
// the exists function. If a file is not found, it will walk up the directory // path, calling the Search function in each directory until it finds a matching
// tree calling the Search function until it finds a file or reaches the root // file or reaches the root directory. On supported operating systems, it will
// directory. On supported operating systems, it will also check if the user ID // also check if the user ID of the directory changes and abort if it does.
// of the directory changes and abort if it does.
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) { func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
owner, err := sysinfo.Owner(path) paths, err := SearchNPathRecursively(path, possibleFilenames, 1)
if err != nil { if err != nil {
return "", err 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) fpath, err := SearchPath(path, possibleFilenames)
if err == nil { if err == nil {
return fpath, nil paths = append(paths, fpath)
} }
// Get the parent path/user id // Get the parent path/user id
parentPath := filepath.Dir(path) parentPath := filepath.Dir(path)
parentOwner, err := sysinfo.Owner(parentPath) parentOwner, err := sysinfo.Owner(parentPath)
if err != nil { if err != nil {
return "", err return nil, err
} }
// Error if we reached the root directory and still haven't found a file // Error if we reached the root directory and still haven't found a file
// OR if the user id of the directory changes // OR if the user id of the directory changes
if path == parentPath || (parentOwner != owner) { if path == parentPath || (parentOwner != owner) {
return "", os.ErrNotExist return paths, nil
} }
owner = parentOwner owner = parentOwner
path = parentPath path = parentPath
} }
return paths, nil
} }

View File

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

View File

@@ -20,5 +20,5 @@ func Name(t *ast.Task) (string, error) {
func Hash(t *ast.Task) (string, error) { func Hash(t *ast.Task) (string, error) {
h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil) h, err := hashstructure.Hash(t, hashstructure.FormatV2, nil)
return fmt.Sprintf("%s:%d", t.Task, h), err return fmt.Sprintf("%s:%s:%d", t.Location.Taskfile, t.LocalName(), h), err
} }

View File

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

@@ -1,6 +1,8 @@
package summary package summary
import ( import (
"fmt"
"os"
"strings" "strings"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@@ -29,6 +31,9 @@ func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
func PrintTask(l *logger.Logger, t *ast.Task) { func PrintTask(l *logger.Logger, t *ast.Task) {
printTaskName(l, t) printTaskName(l, t)
printTaskDescribingText(t, l) printTaskDescribingText(t, l)
printTaskVars(l, t)
printTaskEnv(l, t)
printTaskRequires(l, t)
printTaskDependencies(l, t) printTaskDependencies(l, t)
printTaskAliases(l, t) printTaskAliases(l, t)
printTaskCommands(l, t) printTaskCommands(l, t)
@@ -118,3 +123,168 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
} }
} }
} }
func printTaskVars(l *logger.Logger, t *ast.Task) {
if t.Vars == nil || t.Vars.Len() == 0 {
return
}
osEnvVars := getEnvVarNames()
taskfileEnvVars := make(map[string]bool)
if t.Env != nil {
for key := range t.Env.All() {
taskfileEnvVars[key] = true
}
}
hasNonEnvVars := false
for key := range t.Vars.All() {
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
hasNonEnvVars = true
break
}
}
if !hasNonEnvVars {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "vars:\n")
for key, value := range t.Vars.All() {
// Only display variables that are not from OS environment or Taskfile env
if !isEnvVar(key, osEnvVars) && !taskfileEnvVars[key] {
formattedValue := formatVarValue(value)
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
}
}
}
func printTaskEnv(l *logger.Logger, t *ast.Task) {
if t.Env == nil || t.Env.Len() == 0 {
return
}
envVars := getEnvVarNames()
hasNonEnvVars := false
for key := range t.Env.All() {
if !isEnvVar(key, envVars) {
hasNonEnvVars = true
break
}
}
if !hasNonEnvVars {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "env:\n")
for key, value := range t.Env.All() {
// Only display variables that are not from OS environment
if !isEnvVar(key, envVars) {
formattedValue := formatVarValue(value)
l.Outf(logger.Yellow, " %s: %s\n", key, formattedValue)
}
}
}
// formatVarValue formats a variable value based on its type.
// Handles static values, shell commands (sh:), references (ref:), and maps.
func formatVarValue(v ast.Var) string {
// Shell command - check this first before Value
// because dynamic vars may have both Sh and an empty Value
if v.Sh != nil {
return fmt.Sprintf("sh: %s", *v.Sh)
}
// Reference
if v.Ref != "" {
return fmt.Sprintf("ref: %s", v.Ref)
}
// Static value
if v.Value != nil {
// Check if it's a map or complex type
if m, ok := v.Value.(map[string]any); ok {
return formatMap(m, 4)
}
// Simple string value
return fmt.Sprintf(`"%v"`, v.Value)
}
return `""`
}
// formatMap formats a map value with proper indentation for YAML.
func formatMap(m map[string]any, indent int) string {
if len(m) == 0 {
return "{}"
}
var result strings.Builder
result.WriteString("\n")
spaces := strings.Repeat(" ", indent)
for k, v := range m {
result.WriteString(fmt.Sprintf("%s%s: %v\n", spaces, k, v))
}
return result.String()
}
func printTaskRequires(l *logger.Logger, t *ast.Task) {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return
}
l.Outf(logger.Default, "\n")
l.Outf(logger.Default, "requires:\n")
l.Outf(logger.Default, " vars:\n")
for _, v := range t.Requires.Vars {
// If the variable has enum constraints, format accordingly
if len(v.Enum) > 0 {
l.Outf(logger.Yellow, " - %s:\n", v.Name)
l.Outf(logger.Yellow, " enum:\n")
for _, enumValue := range v.Enum {
l.Outf(logger.Yellow, " - %s\n", enumValue)
}
} else {
// Simple required variable
l.Outf(logger.Yellow, " - %s\n", v.Name)
}
}
}
func getEnvVarNames() map[string]bool {
envMap := make(map[string]bool)
for _, e := range os.Environ() {
parts := strings.SplitN(e, "=", 2)
if len(parts) > 0 {
envMap[parts[0]] = true
}
}
return envMap
}
// isEnvVar checks if a variable is from OS environment or auto-generated by Task.
func isEnvVar(key string, envVars map[string]bool) bool {
// Filter out auto-generated Task variables
if strings.HasPrefix(key, "TASK_") ||
strings.HasPrefix(key, "CLI_") ||
strings.HasPrefix(key, "ROOT_") ||
key == "TASK" ||
key == "TASKFILE" ||
key == "TASKFILE_DIR" ||
key == "USER_WORKING_DIR" ||
key == "ALIAS" ||
key == "MATCH" {
return true
}
return envVars[key]
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/uuid" "github.com/google/uuid"
"gopkg.in/yaml.v3" "go.yaml.in/yaml/v4"
"mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"

View File

@@ -1 +1 @@
3.44.1 3.45.5

32
package-lock.json generated
View File

@@ -1,32 +0,0 @@
{
"name": "@go-task/cli",
"version": "3.44.1",
"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.44.1",
"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/env"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/version" "github.com/go-task/task/v3/internal/version"
@@ -35,7 +36,6 @@ func (e *Executor) Setup() error {
if err := e.readTaskfile(node); err != nil { if err := e.readTaskfile(node); err != nil {
return err return err
} }
e.setupFuzzyModel()
e.setupStdFiles() e.setupStdFiles()
if err := e.setupOutput(); err != nil { if err := e.setupOutput(); err != nil {
return err return err
@@ -56,6 +56,13 @@ func (e *Executor) Setup() error {
func (e *Executor) getRootNode() (taskfile.Node, error) { func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout) 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 { if err != nil {
return nil, err return nil, err
} }
@@ -76,6 +83,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
taskfile.WithInsecure(e.Insecure), taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download), taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline), taskfile.WithOffline(e.Offline),
taskfile.WithTrustedHosts(e.TrustedHosts),
taskfile.WithTempDir(e.TempDir.Remote), taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration), taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithDebugFunc(debugFunc), taskfile.WithDebugFunc(debugFunc),

49
task.go
View File

@@ -78,9 +78,11 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
return err return err
} }
g, ctx := errgroup.WithContext(ctx) g := &errgroup.Group{}
if e.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
for _, c := range regularCalls { for _, c := range regularCalls {
c := c
if e.Parallel { if e.Parallel {
g.Go(func() error { return e.RunTask(ctx, c) }) g.Go(func() error { return e.RunTask(ctx, c) })
} else { } else {
@@ -113,7 +115,7 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
regularCalls = append(regularCalls, c) regularCalls = append(regularCalls, c)
} }
} }
return return regularCalls, watchCalls, err
} }
// RunTask runs a task by its name // RunTask runs a task by its name
@@ -150,7 +152,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
release := e.acquireConcurrencyLimit() release := e.acquireConcurrencyLimit()
defer release() 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) e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil { if err := e.runDeps(ctx, t); err != nil {
return err return err
@@ -172,7 +174,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
if t.Method != "" { if t.Method != "" {
method = t.Method method = t.Method
} }
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t, upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method), fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir.Fingerprint), fingerprint.WithTempDir(e.TempDir.Fingerprint),
@@ -211,7 +212,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
for i := range t.Cmds { for i := range t.Cmds {
if t.Cmds[i].Defer { if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i, &deferredExitCode) defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode)
continue continue
} }
@@ -229,16 +230,16 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
deferredExitCode = uint8(exitCode) deferredExitCode = uint8(exitCode)
} }
if call.Indirect { return err
return err
}
return &errors.TaskRunError{TaskName: t.Task, Err: err}
} }
} }
e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task) e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
return nil return nil
}) }); err != nil {
return &errors.TaskRunError{TaskName: t.Name(), Err: err}
}
return nil
} }
func (e *Executor) mkdir(t *ast.Task) error { func (e *Executor) mkdir(t *ast.Task) error {
@@ -259,13 +260,15 @@ func (e *Executor) mkdir(t *ast.Task) error {
} }
func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error { func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
g, ctx := errgroup.WithContext(ctx) g := &errgroup.Group{}
if e.Failfast || t.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
reacquire := e.releaseConcurrencyLimit() reacquire := e.releaseConcurrencyLimit()
defer reacquire() defer reacquire()
for _, d := range t.Deps { for _, d := range t.Deps {
d := d
g.Go(func() error { g.Go(func() error {
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}) err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil { if err != nil {
@@ -278,17 +281,11 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait() return g.Wait()
} }
func (e *Executor) runDeferred(t *ast.Task, call *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()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i] cmd := t.Cmds[i]
vars, _ := e.Compiler.GetVariables(origTask, call)
cache := &templater.Cache{Vars: vars} cache := &templater.Cache{Vars: vars}
extra := map[string]any{} extra := map[string]any{}
@@ -459,15 +456,17 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// If we found no tasks // If we found no tasks
if len(aliasedTasks) == 0 { if len(aliasedTasks) == 0 {
didYouMean := "" didYouMean := ""
if e.fuzzyModel != nil { if !e.DisableFuzzy {
didYouMean = e.fuzzyModel.SpellCheck(call.Task) e.fuzzyModelOnce.Do(e.setupFuzzyModel)
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
}
} }
return nil, &errors.TaskNotFoundError{ return nil, &errors.TaskNotFoundError{
TaskName: call.Task, TaskName: call.Task,
DidYouMean: didYouMean, DidYouMean: didYouMean,
} }
} }
return matchingTask, nil return matchingTask, nil
} }
@@ -500,7 +499,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks // Compile the list of tasks
for i := range tasks { for i := range tasks {
g.Go(func() error { 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 { if err != nil {
return err return err
} }

View File

@@ -2,7 +2,6 @@ package task_test
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -10,6 +9,7 @@ import (
rand "math/rand/v2" rand "math/rand/v2"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -356,7 +356,7 @@ func (fct fileContentTest) Run(t *testing.T) {
) )
require.NoError(t, e.Setup(), "e.Setup()") 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 { for name, expectContent := range fct.Files {
t.Run(fct.name(name), func(t *testing.T) { t.Run(fct.name(name), func(t *testing.T) {
path := filepathext.SmartJoin(e.Dir, name) path := filepathext.SmartJoin(e.Dir, name)
@@ -407,7 +407,7 @@ func TestGenerates(t *testing.T) {
fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask) fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask)
// Run task for the first time. // 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 { if _, err := os.Stat(srcFile); err != nil {
t.Errorf("File should exist: %v", err) t.Errorf("File should exist: %v", err)
@@ -422,7 +422,7 @@ func TestGenerates(t *testing.T) {
buff.Reset() buff.Reset()
// Re-run task to ensure it's now found to be up-to-date. // 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 { if buff.String() != upToDate {
t.Errorf("Wrong output message: %s", buff.String()) t.Errorf("Wrong output message: %s", buff.String())
} }
@@ -438,6 +438,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
task string task string
}{ }{
{[]string{"generated.txt", ".task/checksum/build"}, "build"}, {[]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"}, {[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"},
} }
@@ -463,7 +464,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
) )
require.NoError(t, e.Setup()) 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 { for _, f := range test.files {
_, err := os.Stat(filepathext.SmartJoin(dir, f)) _, err := os.Stat(filepathext.SmartJoin(dir, f))
require.NoError(t, err) require.NoError(t, err)
@@ -476,7 +477,7 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
time := s.ModTime() time := s.ModTime()
buff.Reset() 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()) 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)) s, err = os.Stat(filepathext.SmartJoin(tempDir.Fingerprint, "checksum/"+test.task))
@@ -507,12 +508,12 @@ func TestStatusVariables(t *testing.T) {
task.WithVerbose(true), task.WithVerbose(true),
) )
require.NoError(t, e.Setup()) 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") assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a")
buff.Reset() 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")) inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt"))
require.NoError(t, err) require.NoError(t, err)
@@ -543,12 +544,12 @@ func TestCmdsVariables(t *testing.T) {
task.WithVerbose(true), task.WithVerbose(true),
) )
require.NoError(t, e.Setup()) 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") assert.Contains(t, buff.String(), "3e464c4b03f4b65d740e1e130d4d108a")
buff.Reset() 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")) inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt"))
require.NoError(t, err) require.NoError(t, err)
ts := fmt.Sprintf("%d", inf.ModTime().Unix()) ts := fmt.Sprintf("%d", inf.ModTime().Unix())
@@ -569,7 +570,9 @@ func TestCyclicDep(t *testing.T) {
task.WithStderr(io.Discard), task.WithStderr(io.Discard),
) )
require.NoError(t, e.Setup()) 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) { func TestTaskVersion(t *testing.T) {
@@ -619,10 +622,10 @@ func TestTaskIgnoreErrors(t *testing.T) {
) )
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "task-should-pass"})) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "task-should-pass"}))
require.Error(t, e.Run(context.Background(), &task.Call{Task: "task-should-fail"})) require.Error(t, e.Run(t.Context(), &task.Call{Task: "task-should-fail"}))
require.NoError(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-pass"})) require.NoError(t, e.Run(t.Context(), &task.Call{Task: "cmd-should-pass"}))
require.Error(t, e.Run(context.Background(), &task.Call{Task: "cmd-should-fail"})) require.Error(t, e.Run(t.Context(), &task.Call{Task: "cmd-should-fail"}))
} }
func TestExpand(t *testing.T) { func TestExpand(t *testing.T) {
@@ -642,7 +645,7 @@ func TestExpand(t *testing.T) {
task.WithStderr(&buff), task.WithStderr(&buff),
) )
require.NoError(t, e.Setup()) 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())) assert.Equal(t, home, strings.TrimSpace(buff.String()))
} }
@@ -663,7 +666,7 @@ func TestDry(t *testing.T) {
task.WithDry(true), task.WithDry(true),
) )
require.NoError(t, e.Setup()) 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())) assert.Equal(t, "task: [build] touch file.txt", strings.TrimSpace(buff.String()))
if _, err := os.Stat(file); err == nil { if _, err := os.Stat(file); err == nil {
@@ -692,13 +695,13 @@ func TestDryChecksum(t *testing.T) {
task.WithDry(true), task.WithDry(true),
) )
require.NoError(t, e.Setup()) 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) _, err := os.Stat(checksumFile)
require.Error(t, err, "checksum file should not exist") require.Error(t, err, "checksum file should not exist")
e.Dry = false 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) _, err = os.Stat(checksumFile)
require.NoError(t, err, "checksum file should exist") require.NoError(t, err, "checksum file should exist")
} }
@@ -784,6 +787,11 @@ func TestIncludesRemote(t *testing.T) {
var buff SyncBuffer var buff SyncBuffer
// Extract host from server URL for trust testing
parsedURL, err := url.Parse(srv.URL)
require.NoError(t, err)
trustedHost := parsedURL.Host
executors := []struct { executors := []struct {
name string name string
executor *task.Executor executor *task.Executor
@@ -823,6 +831,23 @@ func TestIncludesRemote(t *testing.T) {
task.WithOffline(true), task.WithOffline(true),
), ),
}, },
{
name: "with trusted hosts, no prompts",
executor: task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithTimeout(time.Minute),
task.WithInsecure(true),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithVerbose(true),
// With trusted hosts
task.WithTrustedHosts([]string{trustedHost}),
task.WithDownload(true),
),
},
} }
for _, e := range executors { for _, e := range executors {
@@ -840,7 +865,7 @@ func TestIncludesRemote(t *testing.T) {
path := filepath.Join(dir, outputFile) path := filepath.Join(dir, outputFile)
require.NoError(t, os.RemoveAll(path)) 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) actualContent, err := os.ReadFile(path)
require.NoError(t, err) require.NoError(t, err)
@@ -1052,7 +1077,7 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_implicit_false" const dir = "testdata/includes_optional_implicit_false"
wd, _ := os.Getwd() 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) expected := fmt.Sprintf(message, wd, dir)
e := task.NewExecutor( e := task.NewExecutor(
@@ -1072,7 +1097,7 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_explicit_false" const dir = "testdata/includes_optional_explicit_false"
wd, _ := os.Getwd() 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) expected := fmt.Sprintf(message, wd, dir)
e := task.NewExecutor( e := task.NewExecutor(
@@ -1120,11 +1145,11 @@ func TestIncludesRelativePath(t *testing.T) {
require.NoError(t, e.Setup()) 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") assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
buff.Reset() 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") assert.Contains(t, buff.String(), "testdata/includes_rel_path/common")
} }
@@ -1156,7 +1181,7 @@ func TestIncludesInternal(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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 { if test.expectedErr {
require.Error(t, err) require.Error(t, err)
} else { } else {
@@ -1203,7 +1228,7 @@ func TestIncludesFlatten(t *testing.T) {
assert.EqualError(t, err, test.expectedOutput) assert.EqualError(t, err, test.expectedOutput)
} else { } else {
require.NoError(t, err) 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()) assert.Equal(t, test.expectedOutput, buff.String())
} }
}) })
@@ -1235,7 +1260,7 @@ func TestIncludesInterpolation(t *testing.T) { // nolint:paralleltest // cannot
) )
require.NoError(t, e.Setup()) 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 { if test.expectedErr {
require.Error(t, err) require.Error(t, err)
} else { } else {
@@ -1258,20 +1283,20 @@ func TestIncludesWithExclude(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "bar\n", buff.String()) assert.Equal(t, "bar\n", buff.String())
buff.Reset() 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) require.Error(t, err)
buff.Reset() buff.Reset()
err = e.Run(context.Background(), &task.Call{Task: "bar"}) err = e.Run(t.Context(), &task.Call{Task: "bar"})
require.Error(t, err) require.Error(t, err)
buff.Reset() buff.Reset()
err = e.Run(context.Background(), &task.Call{Task: "foo"}) err = e.Run(t.Context(), &task.Call{Task: "foo"})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "foo\n", buff.String()) assert.Equal(t, "foo\n", buff.String())
} }
@@ -1301,7 +1326,7 @@ func TestIncludedTaskfileVarMerging(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Contains(t, buff.String(), test.expectedOutput) assert.Contains(t, buff.String(), test.expectedOutput)
}) })
@@ -1336,7 +1361,7 @@ func TestInternalTask(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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 { if test.expectedErr {
require.Error(t, err) require.Error(t, err)
} else { } else {
@@ -1421,7 +1446,7 @@ func TestSummary(t *testing.T) {
task.WithSilent(true), task.WithSilent(true),
) )
require.NoError(t, e.Setup()) 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")) data, err := os.ReadFile(filepathext.SmartJoin(dir, "task-with-summary.txt"))
require.NoError(t, err) require.NoError(t, err)
@@ -1447,7 +1472,7 @@ func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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 should be the "dir" part of "testdata/dir"
got := strings.TrimSuffix(filepath.Base(out.String()), "\n") got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
@@ -1467,7 +1492,7 @@ func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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") got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory") assert.Equal(t, expected, got, "Mismatch in the working directory")
@@ -1493,7 +1518,7 @@ func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
t.Errorf("Directory should not exist: %v", err) t.Errorf("Directory should not exist: %v", err)
} }
require.NoError(t, e.Setup()) 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") got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory") assert.Equal(t, expected, got, "Mismatch in the working directory")
@@ -1522,7 +1547,7 @@ func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) {
t.Errorf("Directory should not exist: %v", err) t.Errorf("Directory should not exist: %v", err)
} }
require.NoError(t, e.Setup()) 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") got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory") assert.Equal(t, expected, got, "Mismatch in the working directory")
@@ -1593,7 +1618,7 @@ func TestShortTaskNotation(t *testing.T) {
task.WithSilent(true), task.WithSilent(true),
) )
require.NoError(t, e.Setup()) 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()) assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String())
} }
@@ -1791,7 +1816,7 @@ func TestExitImmediately(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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`) assert.Contains(t, buff.String(), `"this_should_fail": executable file not found in $PATH`)
} }
@@ -1811,6 +1836,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) { func TestRunOnceSharedDeps(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1824,7 +1865,7 @@ func TestRunOnceSharedDeps(t *testing.T) {
task.WithForceAll(true), task.WithForceAll(true),
) )
require.NoError(t, e.Setup()) 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"`) rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`)
matches := rx.FindAllStringSubmatch(buff.String(), -1) matches := rx.FindAllStringSubmatch(buff.String(), -1)
@@ -1833,6 +1874,29 @@ func TestRunOnceSharedDeps(t *testing.T) {
assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`) assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`)
} }
func TestRunWhenChanged(t *testing.T) {
t.Parallel()
const dir = "testdata/run_when_changed"
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithForceAll(true),
task.WithSilent(true),
)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "start"}))
expectedOutputOrder := strings.TrimSpace(`
login server=fubar user=fubar
login server=foo user=foo
login server=bar user=bar
`)
assert.Contains(t, buff.String(), expectedOutputOrder)
}
func TestDeferredCmds(t *testing.T) { func TestDeferredCmds(t *testing.T) {
t.Parallel() t.Parallel()
@@ -1856,10 +1920,10 @@ task-1 ran successfully
task: [task-1] echo 'task-1 ran successfully' task: [task-1] echo 'task-1 ran successfully'
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) assert.Contains(t, buff.String(), expectedOutputOrder)
buff.Reset() 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") assert.Contains(t, buff.String(), "child task deferred value-from-parent")
} }
@@ -1875,7 +1939,7 @@ func TestExitCodeZero(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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())) assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String()))
} }
@@ -1891,7 +1955,7 @@ func TestExitCodeOne(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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())) assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String()))
} }
@@ -1920,7 +1984,7 @@ func TestIgnoreNilElements(t *testing.T) {
task.WithSilent(true), task.WithSilent(true),
) )
require.NoError(t, e.Setup()) 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()) assert.Equal(t, "string-slice-1\n", buff.String())
}) })
} }
@@ -1948,7 +2012,7 @@ task: [bye] echo 'Bye!'
Bye! Bye!
::endgroup:: ::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()) t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
} }
@@ -1965,7 +2029,7 @@ func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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()) t.Log(buff.String())
assert.Empty(t, buff.String()) assert.Empty(t, buff.String())
} }
@@ -1982,7 +2046,7 @@ func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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()) t.Log(buff.String())
assert.Contains(t, "failing-output", strings.TrimSpace(buff.String())) assert.Contains(t, "failing-output", strings.TrimSpace(buff.String()))
assert.NotContains(t, "passing", strings.TrimSpace(buff.String())) assert.NotContains(t, "passing", strings.TrimSpace(buff.String()))
@@ -2014,7 +2078,7 @@ VAR_1 is included-default-var1
task: [included3:task1] echo "VAR_2 is included-default-var2" task: [included3:task1] echo "VAR_2 is included-default-var2"
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()) t.Log(buff.String())
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder) assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
} }
@@ -2052,7 +2116,7 @@ Hello foo
task: [bar:lib:greet] echo 'Hello bar' task: [bar:lib:greet] echo 'Hello bar'
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()) t.Log(buff.String())
assert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String())) assert.Equal(t, expectedOutputOrder, strings.TrimSpace(buff.String()))
} }
@@ -2090,7 +2154,7 @@ func TestErrorCode(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.Error(t, err)
taskRunErr, ok := err.(*errors.TaskRunError) taskRunErr, ok := err.(*errors.TaskRunError)
assert.True(t, ok, "cannot cast returned error to *task.TaskRunError") assert.True(t, ok, "cannot cast returned error to *task.TaskRunError")
@@ -2142,7 +2206,7 @@ func TestEvaluateSymlinksInPaths(t *testing.T) { // nolint:paralleltest // canno
for _, test := range tests { // nolint:paralleltest // cannot run in parallel for _, test := range tests { // nolint:paralleltest // cannot run in parallel
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, test.expected, strings.TrimSpace(buff.String())) assert.Equal(t, test.expected, strings.TrimSpace(buff.String()))
buff.Reset() buff.Reset()
@@ -2185,7 +2249,7 @@ func TestTaskfileWalk(t *testing.T) {
task.WithStderr(&buff), task.WithStderr(&buff),
) )
require.NoError(t, e.Setup()) 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()) assert.Equal(t, test.expected, buff.String())
}) })
} }
@@ -2203,7 +2267,7 @@ func TestUserWorkingDirectory(t *testing.T) {
wd, err := os.Getwd() wd, err := os.Getwd()
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, e.Setup()) 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()) assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
} }
@@ -2225,7 +2289,7 @@ func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, e.Setup()) 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()) assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
} }
@@ -2239,7 +2303,7 @@ func TestPlatforms(t *testing.T) {
task.WithStderr(&buff), task.WithStderr(&buff),
) )
require.NoError(t, e.Setup()) 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()) 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())
} }
@@ -2254,7 +2318,7 @@ func TestPOSIXShellOptsGlobalLevel(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String()) assert.Equal(t, "pipefail\ton\n", buff.String())
} }
@@ -2270,7 +2334,7 @@ func TestPOSIXShellOptsTaskLevel(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String()) assert.Equal(t, "pipefail\ton\n", buff.String())
} }
@@ -2286,7 +2350,7 @@ func TestPOSIXShellOptsCommandLevel(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "pipefail\ton\n", buff.String()) assert.Equal(t, "pipefail\ton\n", buff.String())
} }
@@ -2302,7 +2366,7 @@ func TestBashShellOptsGlobalLevel(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String()) assert.Equal(t, "globstar\ton\n", buff.String())
} }
@@ -2318,7 +2382,7 @@ func TestBashShellOptsTaskLevel(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String()) assert.Equal(t, "globstar\ton\n", buff.String())
} }
@@ -2334,7 +2398,7 @@ func TestBashShellOptsCommandLevel(t *testing.T) {
) )
require.NoError(t, e.Setup()) 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) require.NoError(t, err)
assert.Equal(t, "globstar\ton\n", buff.String()) assert.Equal(t, "globstar\ton\n", buff.String())
} }
@@ -2354,7 +2418,7 @@ func TestSplitArgs(t *testing.T) {
vars := ast.NewVars() vars := ast.NewVars()
vars.Set("CLI_ARGS", ast.Var{Value: "foo bar 'foo bar baz'"}) 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) require.NoError(t, err)
assert.Equal(t, "3\n", buff.String()) assert.Equal(t, "3\n", buff.String())
} }
@@ -2395,14 +2459,14 @@ func TestSilence(t *testing.T) {
// Then test the two basic cases where the task is silent or not. // Then test the two basic cases where the task is silent or not.
// A silenced task. // 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.NoError(t, err)
require.Empty(t, buff.String(), "siWhile running lent: Expected not see output, because the task is silent") require.Empty(t, buff.String(), "siWhile running lent: Expected not see output, because the task is silent")
buff.Reset() buff.Reset()
// A chatty (not silent) task. // 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.NoError(t, err)
require.NotEmpty(t, buff.String(), "chWhile running atty: Expected to see output, because the task is not silent") require.NotEmpty(t, buff.String(), "chWhile running atty: Expected to see output, because the task is not silent")
@@ -2410,42 +2474,42 @@ func TestSilence(t *testing.T) {
// Then test invoking the two task from other tasks. // Then test invoking the two task from other tasks.
// A silenced task that calls a chatty task. // 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.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.") 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() buff.Reset()
// A silent task that does a silent call to a chatty task. // 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.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.") 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() buff.Reset()
// A chatty task that does a call to a chatty task. // 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.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.") 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() buff.Reset()
// A chatty task that does a silenced call to a chatty task. // 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.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.") 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() buff.Reset()
// A chatty task with no cmd's of its own that does a silenced call to a chatty task. // 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.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.") 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() buff.Reset()
// A chatty task that does a silenced invocation of a task. // 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.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.") 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.")
@@ -2453,21 +2517,21 @@ func TestSilence(t *testing.T) {
// Then test calls via dependencies. // Then test calls via dependencies.
// A silent task that depends on a chatty task. // 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.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.") 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() buff.Reset()
// A silent task that depends on a silenced chatty task. // 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.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.") 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() buff.Reset()
// A chatty task that, depends on a silenced chatty task. // 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.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.") 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.")
@@ -2519,7 +2583,7 @@ func TestForce(t *testing.T) {
task.WithForceAll(tt.forceAll), task.WithForceAll(tt.forceAll),
) )
require.NoError(t, e.Setup()) 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"}))
}) })
} }
} }
@@ -2579,10 +2643,10 @@ func TestWildcard(t *testing.T) {
) )
require.NoError(t, e.Setup()) require.NoError(t, e.Setup())
if test.wantErr { 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 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()) assert.Equal(t, test.expectedOutput, buff.String())
}) })
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import (
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "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/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"

View File

@@ -4,7 +4,7 @@ import (
"iter" "iter"
"github.com/elliotchance/orderedmap/v3" "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/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import (
"regexp" "regexp"
"strings" "strings"
"gopkg.in/yaml.v3" "go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/internal/deepcopy"
@@ -13,7 +13,7 @@ import (
// Task represents a task // Task represents a task
type Task struct { type Task struct {
Task string Task string `hash:"ignore"`
Cmds []*Cmd Cmds []*Cmd
Deps []*Dep Deps []*Dep
Label string Label string
@@ -36,27 +36,33 @@ type Task struct {
Interactive bool Interactive bool
Internal bool Internal bool
Method string Method string
Prefix string Prefix string `hash:"ignore"`
IgnoreError bool IgnoreError bool
Run string Run string
Platforms []*Platform Platforms []*Platform
Watch bool Watch bool
Location *Location Location *Location
Failfast bool
// Populated during merging // Populated during merging
Namespace string Namespace string `hash:"ignore"`
IncludeVars *Vars IncludeVars *Vars
IncludedTaskfileVars *Vars IncludedTaskfileVars *Vars
FullName string `hash:"ignore"`
} }
func (t *Task) Name() string { func (t *Task) Name() string {
if t.Label != "" { if t.Label != "" {
return t.Label return t.Label
} }
if t.FullName != "" {
return t.FullName
}
return t.Task return t.Task
} }
func (t *Task) LocalName() string { func (t *Task) LocalName() string {
name := t.Task name := t.FullName
name = strings.TrimPrefix(name, t.Namespace) name = strings.TrimPrefix(name, t.Namespace)
name = strings.TrimPrefix(name, ":") name = strings.TrimPrefix(name, ":")
return name return name
@@ -138,6 +144,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Platforms []*Platform Platforms []*Platform
Requires *Requires Requires *Requires
Watch bool Watch bool
Failfast bool
} }
if err := node.Decode(&task); err != nil { if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node) return errors.NewTaskfileDecodeError(err, node)
@@ -176,6 +183,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Platforms = task.Platforms t.Platforms = task.Platforms
t.Requires = task.Requires t.Requires = task.Requires
t.Watch = task.Watch t.Watch = task.Watch
t.Failfast = task.Failfast
return nil return nil
} }
@@ -220,6 +228,8 @@ func (t *Task) DeepCopy() *Task {
Location: t.Location.DeepCopy(), Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(), Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace, Namespace: t.Namespace,
FullName: t.FullName,
Failfast: t.Failfast,
} }
return c return c
} }

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import (
"sync" "sync"
"github.com/elliotchance/orderedmap/v3" "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/errors"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
@@ -244,8 +244,8 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
} }
func taskNameWithNamespace(taskName string, namespace string) string { func taskNameWithNamespace(taskName string, namespace string) string {
if strings.HasPrefix(taskName, NamespaceSeparator) { if after, ok := strings.CutPrefix(taskName, NamespaceSeparator); ok {
return strings.TrimPrefix(taskName, NamespaceSeparator) return after
} }
return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName) return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName)
} }

View File

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

View File

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

View File

@@ -72,6 +72,16 @@ func NewNode(
return node, err 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) { func getScheme(uri string) (string, error) {
u, err := giturls.Parse(uri) u, err := giturls.Parse(uri)
if u == nil { if u == nil {

View File

@@ -4,8 +4,8 @@ import (
"io" "io"
"os" "os"
"path/filepath" "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/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext" "github.com/go-task/task/v3/internal/fsext"
@@ -18,15 +18,24 @@ type FileNode struct {
} }
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error // Find the entrypoint file
base := NewBaseNode(dir, opts...) resolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles)
entrypoint, base.dir, err = fsext.Search(entrypoint, base.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 { if err != nil {
return nil, err return nil, err
} }
return &FileNode{ return &FileNode{
baseNode: base, baseNode: NewBaseNode(resolvedDir, opts...),
entrypoint: entrypoint, entrypoint: resolvedEntrypoint,
}, nil }, nil
} }
@@ -45,10 +54,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path // If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") { if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
return entrypoint, nil return entrypoint, nil
} }

View File

@@ -98,6 +98,11 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
} }
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, 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) 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 != "" { if node.ref != "" {

View File

@@ -21,6 +21,17 @@ func TestGitNode_ssh(t *testing.T) {
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint) 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) { func TestGitNode_sshWithDir(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -53,7 +53,7 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := http.NewRequest("GET", url.String(), nil) req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
if err != nil { if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.Location()} return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
} }

View File

@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
@@ -43,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) { func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path // If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") { if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil return entrypoint, nil
} }

View File

@@ -3,13 +3,14 @@ package taskfile
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/dominikbraun/graph" "github.com/dominikbraun/graph"
"go.yaml.in/yaml/v4"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env" "github.com/go-task/task/v3/internal/env"
@@ -43,6 +44,7 @@ type (
insecure bool insecure bool
download bool download bool
offline bool offline bool
trustedHosts []string
tempDir string tempDir string
cacheExpiryDuration time.Duration cacheExpiryDuration time.Duration
debugFunc DebugFunc debugFunc DebugFunc
@@ -59,6 +61,7 @@ func NewReader(opts ...ReaderOption) *Reader {
insecure: false, insecure: false,
download: false, download: false,
offline: false, offline: false,
trustedHosts: nil,
tempDir: os.TempDir(), tempDir: os.TempDir(),
cacheExpiryDuration: 0, cacheExpiryDuration: 0,
debugFunc: nil, debugFunc: nil,
@@ -119,6 +122,20 @@ func (o *offlineOption) ApplyToReader(r *Reader) {
r.offline = o.offline r.offline = o.offline
} }
// WithTrustedHosts configures the [Reader] with a list of trusted hosts for remote
// Taskfiles. Hosts in this list will not prompt for user confirmation.
func WithTrustedHosts(trustedHosts []string) ReaderOption {
return &trustedHostsOption{trustedHosts: trustedHosts}
}
type trustedHostsOption struct {
trustedHosts []string
}
func (o *trustedHostsOption) ApplyToReader(r *Reader) {
r.trustedHosts = o.trustedHosts
}
// WithTempDir sets the temporary directory that will be used by the [Reader]. // WithTempDir sets the temporary directory that will be used by the [Reader].
// By default, the reader uses [os.TempDir]. // By default, the reader uses [os.TempDir].
func WithTempDir(tempDir string) ReaderOption { func WithTempDir(tempDir string) ReaderOption {
@@ -206,6 +223,28 @@ func (r *Reader) promptf(format string, a ...any) error {
return nil return nil
} }
// isTrusted checks if a URI's host matches any of the trusted hosts patterns.
func (r *Reader) isTrusted(uri string) bool {
if len(r.trustedHosts) == 0 {
return false
}
// Parse the URI to extract the host
parsedURL, err := url.Parse(uri)
if err != nil {
return false
}
host := parsedURL.Host
// Check against each trusted pattern (exact match including port if provided)
for _, pattern := range r.trustedHosts {
if host == pattern {
return true
}
}
return false
}
func (r *Reader) include(ctx context.Context, node Node) error { func (r *Reader) include(ctx context.Context, node Node) error {
// Create a new vertex for the Taskfile // Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{ vertex := &ast.TaskfileVertex{
@@ -459,9 +498,9 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
// If there is no manual checksum pin, run the automatic checks // If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" { if node.Checksum() == "" {
// Prompt the user if required // Prompt the user if required (unless host is trusted)
prompt := cache.ChecksumPrompt(checksum) prompt := cache.ChecksumPrompt(checksum)
if prompt != "" { if prompt != "" && !r.isTrusted(node.Location()) {
if err := func() error { if err := func() error {
r.promptMutex.Lock() r.promptMutex.Lock()
defer r.promptMutex.Unlock() defer r.promptMutex.Unlock()

View File

@@ -12,7 +12,8 @@ import (
) )
var ( var (
defaultTaskfiles = []string{ // DefaultTaskfiles is the list of Taskfile file names supported by default.
DefaultTaskfiles = []string{
"Taskfile.yml", "Taskfile.yml",
"taskfile.yml", "taskfile.yml",
"Taskfile.yaml", "Taskfile.yaml",
@@ -28,6 +29,7 @@ var (
"text/x-yaml", "text/x-yaml",
"application/yaml", "application/yaml",
"application/x-yaml", "application/x-yaml",
"application/octet-stream",
} }
) )
@@ -66,7 +68,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
// If the request was not successful, append the default Taskfile names to // If the request was not successful, append the default Taskfile names to
// the URL and return the URL of the first successful request // the URL and return the URL of the first successful request
for _, taskfile := range defaultTaskfiles { for _, taskfile := range DefaultTaskfiles {
// Fixes a bug with JoinPath where a leading slash is not added to the // Fixes a bug with JoinPath where a leading slash is not added to the
// path if it is empty // path if it is empty
if u.Path == "" { if u.Path == "" {

View File

@@ -1,12 +1,13 @@
# https://taskfile.dev # yaml-language-server: $schema=https://taskfile.dev/schema.json
version: '3' version: '3'
vars: vars:
GREETING: Hello, World! GREETING: Hello, world!
tasks: tasks:
default: default:
desc: Print a greeting message
cmds: cmds:
- echo "{{.GREETING}}" - echo "{{.GREETING}}"
silent: true silent: true

View File

@@ -1,8 +1,60 @@
package ast package ast
import "github.com/Masterminds/semver/v3" import (
"cmp"
"maps"
"slices"
"time"
"github.com/Masterminds/semver/v3"
)
type TaskRC struct { type TaskRC struct {
Version *semver.Version `yaml:"version"` Version *semver.Version `yaml:"version"`
Experiments map[string]int `yaml:"experiments"` Verbose *bool `yaml:"verbose"`
DisableFuzzy *bool `yaml:"disable-fuzzy"`
Concurrency *int `yaml:"concurrency"`
Remote Remote `yaml:"remote"`
Failfast bool `yaml:"failfast"`
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"`
TrustedHosts []string `yaml:"trusted-hosts"`
}
// 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)
if len(other.Remote.TrustedHosts) > 0 {
merged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts)
slices.Sort(merged)
t.Remote.TrustedHosts = slices.Compact(merged)
}
t.Verbose = cmp.Or(other.Verbose, t.Verbose)
t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
} }

View File

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

View File

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

View File

@@ -1,6 +1,92 @@
package taskrc package taskrc
var defaultTaskRCs = []string{ import (
".taskrc.yml", "os"
".taskrc.yaml", "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
} }

309
taskrc/taskrc_test.go Normal file
View File

@@ -0,0 +1,309 @@
package taskrc
import (
"os"
"path/filepath"
"testing"
"time"
"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)
}
func TestGetConfig_RemoteTrustedHosts(t *testing.T) { //nolint:paralleltest // cannot run in parallel
_, _, localDir := setupDirs(t)
// Test with single host
configYAML := `
remote:
trusted-hosts:
- github.com
`
writeFile(t, localDir, ".taskrc.yml", configYAML)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, []string{"github.com"}, cfg.Remote.TrustedHosts)
// Test with multiple hosts
configYAML = `
remote:
trusted-hosts:
- github.com
- gitlab.com
- example.com:8080
`
writeFile(t, localDir, ".taskrc.yml", configYAML)
cfg, err = GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
assert.Equal(t, []string{"github.com", "gitlab.com", "example.com:8080"}, cfg.Remote.TrustedHosts)
}
func TestGetConfig_RemoteTrustedHostsMerge(t *testing.T) { //nolint:paralleltest // cannot run in parallel
t.Run("file-based merge precedence", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
xdgConfigDir, homeDir, localDir := setupDirs(t)
// XDG config has github.com and gitlab.com
xdgConfig := `
remote:
trusted-hosts:
- github.com
- gitlab.com
timeout: "30s"
`
writeFile(t, xdgConfigDir, "taskrc.yml", xdgConfig)
// Home config has example.com (should be combined with XDG)
homeConfig := `
remote:
trusted-hosts:
- example.com
`
writeFile(t, homeDir, ".taskrc.yml", homeConfig)
cfg, err := GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
// Home config entries come first, then XDG
assert.Equal(t, []string{"example.com", "github.com", "gitlab.com"}, cfg.Remote.TrustedHosts)
// Test with local config too
localConfig := `
remote:
trusted-hosts:
- local.dev
`
writeFile(t, localDir, ".taskrc.yml", localConfig)
cfg, err = GetConfig(localDir)
assert.NoError(t, err)
assert.NotNil(t, cfg)
// Local config entries come first
assert.Equal(t, []string{"example.com", "github.com", "gitlab.com", "local.dev"}, cfg.Remote.TrustedHosts)
})
t.Run("merge edge cases", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
tests := []struct {
name string
base *ast.TaskRC
other *ast.TaskRC
expected []string
}{
{
name: "merge hosts into empty",
base: &ast.TaskRC{},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"github.com"},
},
},
expected: []string{"github.com"},
},
{
name: "merge combines lists",
base: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"base.com"},
},
},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"other.com"},
},
},
expected: []string{"base.com", "other.com"},
},
{
name: "merge empty list does not override",
base: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"base.com"},
},
},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{},
},
},
expected: []string{"base.com"},
},
{
name: "merge nil does not override",
base: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: []string{"base.com"},
},
},
other: &ast.TaskRC{
Remote: ast.Remote{
TrustedHosts: nil,
},
},
expected: []string{"base.com"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
tt.base.Merge(tt.other)
assert.Equal(t, tt.expected, tt.base.Remote.TrustedHosts)
})
}
})
t.Run("all remote fields merge", func(t *testing.T) { //nolint:paralleltest // parent test cannot run in parallel
insecureTrue := true
offlineTrue := true
timeout := 30 * time.Second
cacheExpiry := 1 * time.Hour
base := &ast.TaskRC{}
other := &ast.TaskRC{
Remote: ast.Remote{
Insecure: &insecureTrue,
Offline: &offlineTrue,
Timeout: &timeout,
CacheExpiry: &cacheExpiry,
TrustedHosts: []string{"github.com", "gitlab.com"},
},
}
base.Merge(other)
assert.Equal(t, &insecureTrue, base.Remote.Insecure)
assert.Equal(t, &offlineTrue, base.Remote.Offline)
assert.Equal(t, &timeout, base.Remote.Timeout)
assert.Equal(t, &cacheExpiry, base.Remote.CacheExpiry)
assert.Equal(t, []string{"github.com", "gitlab.com"}, base.Remote.TrustedHosts)
})
}

View File

@@ -12,6 +12,14 @@ tasks:
generates: generates:
- ./generated.txt - ./generated.txt
method: checksum 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: build-with-status:
cmds: cmds:

View File

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

14
testdata/failfast/default/Taskfile.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: '3'
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1,3 @@
dep1
dep2
dep3

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1 @@

15
testdata/failfast/task/Taskfile.yaml vendored Normal file
View File

@@ -0,0 +1,15 @@
version: '3'
tasks:
default:
deps:
- dep1
- dep2
- dep3
- dep4
failfast: true
dep1: sleep 0.1 && echo 'dep1'
dep2: sleep 0.2 && echo 'dep2'
dep3: sleep 0.3 && echo 'dep3'
dep4: exit 1

View File

@@ -0,0 +1 @@
task: Failed to run task "default": task: Failed to run task "dep4": exit status 1

View File

@@ -0,0 +1 @@

7
testdata/label_error/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: '3'
tasks:
foo:
label: "foobar"
cmds:
- "false"

View File

@@ -0,0 +1 @@
task: Failed to run task "foobar": exit status 1

View File

@@ -0,0 +1 @@
task: [foobar] false

View File

@@ -1 +1 @@
task: precondition not met task: Failed to run task "impossible": task: precondition not met

View File

@@ -1 +1 @@
task: Failed to run task "executes_failing_task_as_cmd": task: precondition not met task: Failed to run task "executes_failing_task_as_cmd": task: Failed to run task "impossible": task: precondition not met

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