Compare commits

..

296 Commits

Author SHA1 Message Date
renovate[bot]
b27a6653f6 chore(deps): update all non-major dependencies (#2816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-17 12:40:51 +02:00
renovate[bot]
76269b03c4 chore(deps): update pnpm to v11 (#2844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-05-17 12:20:33 +02:00
renovate[bot]
ed492ac862 chore(deps): update pnpm/action-setup digest to 0e279bb (#2843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 11:59:56 +02:00
dependabot[bot]
1d385ad5b4 chore(deps): bump vite from 5.4.19 to 5.4.21 in /website (#2841)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-17 11:59:42 +02:00
Andrey Nering
2eff3545d9 ci(goreleaser): add missing env to workflow 2026-05-16 18:09:51 -03:00
Andrey Nering
bd79c29a39 v3.51.1 2026-05-16 17:51:05 -03:00
Andrey Nering
136e0dae89 ci: fix goreleaser 2026-05-16 17:50:34 -03:00
Andrey Nering
41a2137044 v3.51.0 2026-05-16 17:27:25 -03:00
imgbot[bot]
aea7f7a713 docs: optimize images (#2690) 2026-05-16 17:23:46 -03:00
renovate[bot]
c3e16b0a68 chore(deps): update mvdan.cc/sh/v3 digest to f5c6e27 (#2834) 2026-05-16 17:20:20 -03:00
renovate[bot]
19699a1f0e chore(deps): update pnpm/action-setup digest to 739bfe4 (#2835)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 16:27:48 +02:00
Timothy Rule
9e3ff27ba1 fix: avoid panic after calling WordsSeq (#2810) 2026-05-09 21:42:00 +00:00
renovate[bot]
de05ebc503 chore(deps): update goreleaser/goreleaser-action digest to 1a80836 (#2815) 2026-05-09 21:34:39 +00:00
Andrey Nering
6e154f2b71 docs(changelog): add entry for #2832 2026-05-09 18:28:46 -03:00
Andrey Nering
9ba1e566a9 fix: update mvdan.cc/sh to fix regression
* Closes #2812
* Closes #2818
* Ref https://github.com/mvdan/sh/pull/1330
2026-05-09 18:28:46 -03:00
Andrey Nering
1b53d425ee ci(govulncheck): update golang.org/x/net 2026-05-09 11:45:53 -03:00
Andrey Nering
7b3559ce4c ci: fix build and lint builds 2026-05-09 11:45:53 -03:00
Andrey Nering
43dbeb4260 docs(changelog): add entry for #2406 #2408 2026-05-09 11:25:49 -03:00
Jerry Wiltse
d9e0e04725 feat: add joinEnv and joinUrl string functions and 2 new system vars (#2408) 2026-05-09 14:19:33 +00:00
Andrey Nering
629a10813f chore: update minimal go version to go 1.25.10 2026-05-09 11:19:02 -03:00
Andrey Nering
dc64864643 docs: add security > threat model page (#2830) 2026-05-09 13:59:36 +00:00
renovate[bot]
5f78da2d0a chore(deps): update dependency netlify-cli to v26 (#2823)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 03:03:32 +02:00
renovate[bot]
7915b78ac2 chore(deps): update pnpm/action-setup action to v6 (#2824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 03:03:13 +02:00
Andrey Nering
3441bf522e docs(changelog): add entry for #2820 2026-05-01 10:03:53 -03:00
Roman Dahm
46f5db22c8 perf(templater): skip template engine for strings without {{ (#2820) 2026-05-01 09:57:16 -03:00
Ludovic Fernandez
ecffcc720f chore(golangci-lint): improve and clean configuration (#2808) 2026-04-23 21:13:15 -03:00
Valentin Maerten
be35b3af75 feat(website): highlight big orgs / projects that use Task (#2803) 2026-04-22 10:02:06 +02:00
renovate[bot]
3aaedc790d chore(deps): update goreleaser/goreleaser-action digest to e24998b (#2804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 22:49:24 +02:00
renovate[bot]
0b19d973ac chore(deps): update all non-major dependencies (#2805)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 22:08:05 +02:00
Andreas **Felix** Häberle
70b6cd8ee0 docs: add call internal task within a task example (#2789)
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-04-20 21:58:42 +02:00
Valentin Maerten
1eb5720e7e chore: changelog for #2788 2026-04-20 21:53:50 +02:00
Mateen Anjum
1b06da16f6 feat(templater): add absPath template function (#2788)
Signed-off-by: Mateen Anjum <mateenali66@gmail.com>
Co-authored-by: Valentin Maerten <maerten.valentin@gmail.com>
2026-04-20 21:50:03 +02:00
Andrey Nering
6e37e3d7a7 chore(website): remove controls to copy page content
This is part of the LLM plugin. It's distracting and not really useful.

We're keeping the markdown version of the pages, tho. Just append `.md`
to any page to see the markdown version.
2026-04-15 16:39:20 -03:00
Pete Davison
4bea638b05 feat: add security docs to website and update contributing (#2799) 2026-04-15 20:34:38 +01:00
Pete Davison
8f2d17a387 feat: use GH_PAT for goreleaser (#2797) 2026-04-15 13:33:57 +00:00
Andrey Nering
f7d17fffad chore(website): update my bluesky handle 2026-04-15 10:16:02 -03:00
Pete Davison
697ef35303 feat: add permissions to actions (#2796) 2026-04-15 13:27:23 +01:00
Andrey Nering
8fe3d048fa docs: document and add blog post about go tool task (#2791) 2026-04-14 22:47:45 -03:00
Andrey Nering
d61d92dfdf v3.50.0 2026-04-13 17:54:26 -03:00
renovate[bot]
114ac1eedc chore(deps): update actions/github-script action to v9 (#2787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 10:49:13 -03:00
renovate[bot]
a016b7b72b chore(deps): update all non-major dependencies (#2786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-13 10:48:30 -03:00
Andrey Nering
219bf3e5a5 chore: add nolint annotations for gosec linter
Add //nolint:gosec annotations for intentional code patterns
that are safe in context (path traversal in release tool,
uintptr conversion for terminals, weak rand in tests,
TLS skip verify for user-configured insecure mode).

Assisted-by: Kimi-K2.5 via Crush <crush@charm.land>
2026-04-13 10:46:19 -03:00
Andrey Nering
b36fcfd8bb chore(golangci-lint): exclude gosec G306 for file permissions
This allows using 0644 file permissions without linter warnings.

Assisted-by: Kimi-K2.5 via Crush <crush@charm.land>
2026-04-13 10:46:19 -03:00
Andrey Nering
ef8fb84c8f chore(golangci-lint): enable gosec linter 2026-04-13 10:46:19 -03:00
Andrey Nering
ddecd51715 ci: add security action with govulncheck 2026-04-13 10:46:19 -03:00
Andrey Nering
88fc6d4f24 ci: pin action by commit hash 2026-04-13 10:46:19 -03:00
Andrey Nering
07fbd9887e docs(changelog): add entry for #2716 2026-04-11 21:45:48 -03:00
Doug Richardson
f2e32beabd fix: re-run task when generated files are missing with method: timestamp (#2716) 2026-04-11 21:42:50 -03:00
dependabot[bot]
8fa9dc04ac chore(deps): bump github.com/aws/aws-sdk-go-v2/service/s3 from 1.97.1 to 1.97.3 (#2778)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:24:22 +00:00
renovate[bot]
0c98f1ad13 chore(deps): update all non-major dependencies (#2775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-09 06:16:22 +00:00
Valentin Maerten
a12cc6e843 chore: changelog for #2764 2026-04-09 08:16:03 +02:00
Mateen Anjum
44e1350d0c fix: handle SIGHUP in watcher (#2764) 2026-04-09 08:15:37 +02:00
dependabot[bot]
2973dd75f9 chore(deps): bump go.opentelemetry.io/otel/sdk from 1.40.0 to 1.43.0 (#2779)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 08:07:58 +02:00
dependabot[bot]
a10f1d2ee7 chore(deps): bump google.golang.org/grpc from 1.78.0 to 1.79.3 (#2750)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 15:02:49 +00:00
dependabot[bot]
f727b55fbc chore(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 (#2771)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 11:58:01 -03:00
renovate[bot]
363153cbf3 chore(deps): update pnpm/action-setup action to v5 (#2766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-02 18:59:00 +02:00
Valentin Maerten
6b436eda48 fix: Windows CI test failures and path normalization (#2670) 2026-03-28 10:39:13 +01:00
Valentin Maerten
2ddebca4e1 docs: add AI usage disclosure policy (#2755) 2026-03-28 10:30:54 +01:00
renovate[bot]
b6ab6153a2 chore(deps): update all non-major dependencies (#2728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 10:28:52 +01:00
Rohan Santhosh Kumar
bca99520bf docs: use dependent in gentle force docs (#2756)
Co-authored-by: rohan436 <rohan.santhoshkumar@googlemail.com>
2026-03-28 10:24:36 +01:00
Pete Davison
1312ee8a81 fix: typo in workflow 2026-03-23 20:06:17 +00:00
Pete Davison
b7cb204f10 feat: test adjustments to issue-experiment workflow 2026-03-23 20:01:06 +00:00
renovate[bot]
921f84157a chore(deps): update go-task/setup-task action to v2 (#2759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-23 08:39:36 +01:00
Valentin Maerten
470ef30f8f chore: changelog for #2730 2026-03-21 11:41:47 +01:00
Sergio
87b12e663e fix(fish): honor GO_TASK_PROGNAME for experiments cache (#2730) 2026-03-21 11:37:51 +01:00
Valentin Maerten
e61700f36d chore: changelog for #2678 2026-03-21 11:37:12 +01:00
Valentin Maerten
8b6aca5722 feat(requires): support variable references in enum constraints (#2678) 2026-03-21 11:32:02 +01:00
Sergio
19d8fae5f9 docs: correct stdin Taskfile command example (#2738) 2026-03-18 20:48:57 +00:00
Valentin Maerten
c387048f8f feat(website): add APK (Alpine Linux) to official package managers
Add Alpine Linux APK installation instructions via Cloudsmith and move
the Cloudsmith hosting info block above the package manager sections.
2026-03-18 21:42:17 +01:00
Valentin Maerten
f2b3ba1263 feat(website): add community sponsors section with Cloudsmith (#2748) 2026-03-17 22:00:48 +01:00
renovate[bot]
2139e32426 chore(deps): update dependency netlify-cli to v24 (#2729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 21:51:27 +01:00
renovate[bot]
d4e168128b chore(deps): update pnpm/action-setup digest to fc06bc1 (#2744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 21:51:01 +01:00
Andrey Nering
54bdcba369 ci: update to go 1.26 (#2724) 2026-03-08 17:25:37 -03:00
Andrey Nering
c55c969474 docs: update changelog 2026-03-08 17:11:49 -03:00
Andrey Nering
73f9879421 docs: bring back line commented out by mistake 2026-03-08 17:05:32 -03:00
Andrey Nering
aa83651da2 v3.49.1 2026-03-08 17:01:25 -03:00
Andrey Nering
4ddad9f9f7 Revert "fix: Call ReplaceVars() to resolve Ref's for imported global vars." (#2723) 2026-03-08 20:00:12 +00:00
Jannis
080ee8869f docs: schema: add tasks.task.method (#2718) 2026-03-08 11:45:27 +01:00
Andrey Nering
af943b064b ci: fix netlify prod deploy after release 2026-03-07 20:02:53 -03:00
Andrey Nering
962eada344 docs: update releasing guide
We have now more package managers being released automatically by
GoReleaser. Only Snapcraft still require manual steps.
2026-03-07 19:42:03 -03:00
Andrey Nering
a0d9750edf docs(changelog): fix case: git -> Git 2026-03-07 19:26:22 -03:00
Andrey Nering
a1b8985df0 v3.49.0 2026-03-07 19:20:58 -03:00
dependabot[bot]
21daf6160a chore(deps): bump go.opentelemetry.io/otel/sdk from 1.39.0 to 1.40.0 (#2712)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 19:49:38 -03:00
renovate[bot]
c70d28f7b8 chore(deps): update actions/upload-artifact action to v7 (#2714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 19:48:51 -03:00
Andrey Nering
90e6ef88dc security: pin github actions by commit (#2719) 2026-03-06 19:20:25 -03:00
renovate[bot]
a788034148 chore(deps): update all non-major dependencies (#2713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-06 22:32:30 +01:00
Timothy Rule
90bcbe9ba5 fix: address "taskfile not found" in some environments like docker (#2709) 2026-02-27 09:47:59 -03:00
Andrey Nering
60a808ca23 docs(readme): update | to 2026-02-23 17:26:14 -03:00
renovate[bot]
edc80aed81 chore(deps): update goreleaser/goreleaser-action action to v7 (#2706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 15:35:02 -03:00
renovate[bot]
68bea7f273 chore(deps): update pnpm to v10.30.1 (#2705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 15:34:23 -03:00
Valentin Maerten
c62f9c7147 fix(build): exclude unsupported windows/arm target 2026-02-21 15:18:04 +01:00
Valentin Maerten
c4ecff753d chore: changelog for #2607 2026-02-18 19:00:15 +01:00
Valentin Maerten
2ed77716be feat(config): add environment variable support for all taskrc options (#2607) 2026-02-18 18:58:13 +01:00
Valentin Maerten
a2f8e144ca chore: changelog for #2632 2026-02-18 18:57:53 +01:00
Timothy Rule
2cdd7d3e43 fix: Call ReplaceVars() to resolve Ref's for imported global vars. (#2632) 2026-02-18 18:55:20 +01:00
Andrey Nering
56b316a124 ci: fix lint 2026-02-17 15:43:13 -03:00
renovate[bot]
39ce6a21ac chore(deps): update all non-major dependencies 2026-02-17 15:43:13 -03:00
Andrey Nering
fc5f6fa3aa fix: pin yaml package to v3 for now (#2693) 2026-02-17 15:29:51 -03:00
Valentin Maerten
44a2f2e5f5 docs: add Cloudsmith attribution for deb/rpm package hosting 2026-02-15 17:22:01 +01:00
Valentin Maerten
d356c649aa chore: changelog for #2682 2026-02-15 17:01:02 +01:00
Valentin Maerten
ca24d32f37 chore: changelog for #2669 2026-02-15 14:56:52 +01:00
Timothy Rule
f63a63fa6c fix: improve error message when searching for Taskfile. (#2682) 2026-02-15 14:56:35 +01:00
renovate[bot]
c0ff7105e7 chore(deps): update peter-evans/find-comment action to v4 (#2684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 14:49:49 +01:00
Valentin Maerten
8b063d6b92 fix(git): check cache before context timeout in getOrCloneRepo (#2669) 2026-02-15 14:46:55 +01:00
Valentin Maerten
dc8ac5e79f chore: changelog for #2686 2026-02-15 14:45:47 +01:00
Timothy Rule
df7810ab63 fix: copy watch when merging tasks during import (#2686) 2026-02-15 14:42:59 +01:00
Pete Davison
82783417ea docs: update integration doc with details of extension config namespace change (#2428)
* docs: update integration doc with details of extension config namespace change

* docs: add descriptions of sorting modes
2026-02-14 20:47:06 +00:00
renovate[bot]
d5bed6b716 chore(deps): update peter-evans/create-or-update-comment action to v5 (#2673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 09:42:48 +01:00
renovate[bot]
c8f722c0d5 chore(deps): update actions/upload-artifact action to v6 (#2672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 09:35:52 +01:00
Andrey Nering
b7743eda88 chore(goreleaser): update descriptions 2026-01-31 16:20:31 -03:00
Valentin Maerten
c0796e9701 chore: changelog for #2656 2026-01-31 18:53:52 +01:00
Trim21
cf54be3266 fix(node_git): always use unix path style (#2656) 2026-01-31 18:49:22 +01:00
Jens Erat
e129ae2fac docs: fix dir headline level (#2665)
The other commands in this section are on headline level 4, which probably is also the expected one for `dir`.
2026-01-28 18:21:50 +00:00
Andrey Nering
ed69256512 chore: update readme title and description to match website 2026-01-27 22:28:02 -03:00
Andrey Nering
40ad9719d4 chore(website): improve home page title, including on opengraph 2026-01-27 22:18:49 -03:00
Andrey Nering
48f75f0913 docs(cli): mention --list with --silent 2026-01-27 21:53:52 -03:00
Andrey Nering
f000ea2b22 chore(website): have a good opengraph image 2026-01-27 21:53:52 -03:00
Andrey Nering
e8be687a40 chore(website): add "edit this page on github" links 2026-01-27 21:53:52 -03:00
renovate[bot]
788605a3a9 chore(deps): update actions/setup-go action to v6 (#2662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 21:42:04 +01:00
Andrey Nering
697cf442a2 docs(blog): adjust post title 2026-01-26 09:51:43 -03:00
Andrey Nering
e957edf783 chore(website): add goodx sponsor 2026-01-26 09:39:06 -03:00
Andrey Nering
09e7247d05 v3.48.0 2026-01-26 09:26:23 -03:00
Andrey Nering
502f24a2ad docs(changelog): add entry for #2658 and #2660 2026-01-26 09:24:26 -03:00
Valentin Maerten
f09f31c6d5 fix: skip prompting for vars when task if condition fails
Move the prompt for required variables AFTER the if condition check.
This avoids asking the user for input when the task won't run anyway.

The order in RunTask() is now:
1. FastCompiledTask
2. Check required vars early (non-interactive mode only)
3. CompiledTask (resolve dynamic vars)
4. Check if condition → exit early if false
5. Prompt for missing vars (only if task will run)
6. Validate required vars
2026-01-26 09:21:09 -03:00
Valentin Maerten
5a78808caa fix: evaluate task-level if condition after resolving dynamic variables 2026-01-26 09:21:09 -03:00
Valentin Maerten
026c899d90 feat: support self-signed certificates for remote taskfiles (#2537) 2026-01-25 18:51:30 +01:00
Timothy Rule
f6720760b4 fix(includes): propagate silent mode from included Taskfiles to tasks (#2640) 2026-01-25 16:33:52 +01:00
Valentin Maerten
065236f076 chore: changelog for #2635 2026-01-25 16:08:11 +01:00
Timothy Rule
1bd5aa6bd5 fix: correct the value of ROOT_TASKFILE when no entrypoint (#2635) 2026-01-25 16:06:13 +01:00
Valentin Maerten
c3fd3c4b5e chore: add website/.netlify to gitignore 2026-01-25 14:26:53 +01:00
Valentin Maerten
299232ee7d fix(website): improve SEO with favicons, structured data and robots.txt (#2657) 2026-01-25 14:16:23 +01:00
Andrey Nering
12a26fa15e v3.47.0 2026-01-24 20:49:48 -03:00
Andrey Nering
4ab5dec8ae fix(website): have the actual page title on open graph and twitter tags 2026-01-24 20:48:51 -03:00
Andrey Nering
af311229fe docs: new blog post about if: and required variables prompt 2026-01-24 20:48:51 -03:00
Andrey Nering
1443e2d989 chore(deps): update mvdan/sh once again
Closes #2650
2026-01-24 20:48:51 -03:00
Andrey Nering
5bf4e4a29b chore(deps): revert mvdan/sh to latest stable version (#2651)
There is an important regression on interactive commands here. See #2650
and mvdan/sh#1242.

Once mvdan/sh#1244 is merged we'll upgrade again.
2026-01-24 22:21:30 +00:00
Andrey Nering
f9052c9fdf chore(taskfile): add go.mod as a dependency of install 2026-01-24 18:49:38 -03:00
Valentin Maerten
0a82e2e053 chore: changelog for #2579 2026-01-22 21:22:47 +01:00
Valentin Maerten
6dedcafd7d feat(vars): add interactive prompting for required variables (#2579) 2026-01-22 21:20:45 +01:00
Valentin Maerten
c84cfa41f7 chore: changelog for #2564 2026-01-21 23:06:59 +01:00
Valentin Maerten
9bc1efbc47 feat: add conditional execution for tasks and commands (#2564) 2026-01-21 23:05:40 +01:00
Andrey Nering
da7eb0c855 fix(shell): fix deprecation warning 2026-01-21 14:05:52 -03:00
Andrey Nering
edb491a4d0 chore(deps): update shell interpreter
Closes #2446
Ref mvdan/sh#1182
Ref mvdan/sh#1241
2026-01-21 14:05:52 -03:00
renovate[bot]
2ad3d26f4a chore(deps): update all non-major dependencies (#2637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 21:09:40 +01:00
renovate[bot]
cdfcd08213 chore(deps): update actions/checkout action to v6 (#2638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 21:08:59 +01:00
Valentin Maerten
382c37bc2a chore: changelog for #2234 2026-01-18 19:07:13 +01:00
Valentin Maerten
618cd8956f feat: wildcard match aliases (#2234) 2026-01-18 19:05:29 +01:00
Andrey Nering
b53e5da41a docs(changelog): add entry for #2584 2026-01-18 09:20:00 -03:00
Timothy Rule
b9c1ab8eae fix: ensure no ansi sequences are printed for --color=false (#2584) 2026-01-18 09:18:52 -03:00
Andrey Nering
e47f55783e docs(changelog): add entry for #1566, #2633 2026-01-18 08:44:50 -03:00
Timothy Rule
d5f071c096 fix: print prefix when task is up-to-date and output-style is prefixed (#2633) 2026-01-18 08:42:18 -03:00
Andrey Nering
fb784f4e3d chore(taskfile): create tag as annotated and add message automatically 2026-01-18 08:35:23 -03:00
renovate[bot]
91f9299c98 chore(deps): update all non-major dependencies (#2611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-14 21:07:58 +01:00
Valentin Maerten
145412a75c chore: changelog for #2602 2026-01-14 19:51:00 +01:00
Valentin Maerten
ba38344ca6 fix: remote git taskfile cloning and directory includes (#2602) 2026-01-14 19:43:39 +01:00
Valentin Maerten
4e963f8714 feat(ci): add on-demand PR build workflow (#2578) 2026-01-14 19:41:50 +01:00
Bouke Versteegh
3d4d189bcd docs: clarify dotenv file precedence when multiple files are specified (#2628) 2026-01-14 19:37:41 +01:00
Andrey Nering
179bde1f37 v3.46.4 2025-12-24 18:57:17 -03:00
Andrey Nering
e4de687aee docs(changelog): add entry for #2592 2025-12-24 18:55:21 -03:00
WinkelCode
06538860a8 fix(completion): use posix whitespace in fish sed regex (#2592) 2025-12-24 18:48:54 -03:00
renovate[bot]
8a37bf5c1f chore(deps): update all non-major dependencies (#2598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 20:36:40 +01:00
Valentin Maerten
ca99266aea feat(schema): add cache-dir property to taskrc schema (#2603) 2025-12-23 20:35:57 +01:00
Andrey Nering
8dfafe507f v3.46.3 2025-12-19 15:50:51 -03:00
Andrey Nering
678fdec7d2 docs(changelog): add entry for #2593 and #2594 2025-12-19 15:50:33 -03:00
Valentin Maerten
3626b271a7 fix(completion): correct zsh pattern matching for global flag (#2594) 2025-12-19 15:48:59 -03:00
Andrey Nering
fc378cfb92 v3.46.2 2025-12-18 17:55:19 -03:00
Andrey Nering
9488a2a744 docs(changelog): add entry for #2588 and #2589 2025-12-18 17:54:38 -03:00
Andrey Nering
6ece2445ae docs(changelog): change tag to match release on github 2025-12-18 17:51:33 -03:00
Valentin Maerten
9b95e758f4 fix: cli variables should take priority over aaskfile defaults (#2589)
When using `task FOO=bar` with a Taskfile containing
`FOO: '{{.FOO | default "foo"}}'`, the CLI value was being
overwritten by the Taskfile default.

Split the variable merging into two steps:
1. Merge CLI variables (FOO=bar) first so they override Taskfile vars
2. ReverseMerge special variables (CLI_ARGS, CLI_FORCE, etc.) so
   they're available for templating in Taskfile vars

Fixes regression introduced in #2403.

Co-authored-by: Timothy Rule <timothy.rule@gmail.com>
2025-12-18 17:50:00 -03:00
Valentin Maerten
28fee2c356 v3.46.1 2025-12-18 17:42:03 +01:00
Valentin Maerten
763e77467b fix: cloudsmith alpine upload 2025-12-18 17:41:54 +01:00
Valentin Maerten
f2385e625d v3.46.0 2025-12-18 17:29:29 +01:00
Valentin Maerten
e929cccd73 docs: better changelog (#2586) 2025-12-18 16:42:55 +01:00
Valentin Maerten
cb183349b7 refactor: migrate from go-git to go-getter (#2512) 2025-12-18 12:21:30 +01:00
Valentin Maerten
2ebbb99f58 improve website's SEO 2025-12-18 08:52:43 +01:00
Valentin Maerten
6660afc8d2 feat: auto-detect color output in CI environments (#2569) 2025-12-18 08:40:37 +01:00
Valentin Maerten
b710259bfa feat(completion): add zstyle verbose option for zsh completion (#2571) 2025-12-18 08:35:56 +01:00
Valentin Maerten
4ec6c453bd feat: add remote.cache-dir taskrc option (#2572) 2025-12-18 08:32:11 +01:00
Valentin Maerten
28408ef3f4 fix(schema): workaround IntelliJ JSON Schema validation bug (#2576) 2025-12-15 23:03:13 +01:00
Valentin Maerten
a2d34ffc4c chore: changelog for #2577 2025-12-15 22:45:52 +01:00
Semih Buyukgungor
1a190a118f feat: expose .CLI_ASSUME_YES templating variable (#2577) 2025-12-15 22:42:27 +01:00
renovate[bot]
18efa3982f chore(deps): update all non-major dependencies (#2580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 14:33:40 -03:00
Valentin Maerten
655e83454e fix(completion): handle colons in task descriptions for fish (#2573) 2025-12-14 15:56:18 +01:00
Valentin Maerten
3ad4604c36 fix(completion): support --global flag in zsh completion (#2574) 2025-12-14 15:51:15 +01:00
Valentin Maerten
5a27d04655 chore: changelog for #2552 2025-12-12 22:30:18 +01:00
Timothy Rule
ea933bcc55 fix: support error_ignore for a task call (#2552) 2025-12-12 22:28:20 +01:00
Valentin Maerten
e0d6b71971 chore: changelog for #2568 2025-12-12 21:25:59 +01:00
Valentin Maerten
d7ee855e49 feat: emit error annotations in GitHub Actions (#2568) 2025-12-12 21:23:37 +01:00
Valentin Maerten
511f35a456 chore: changelog for #2403 2025-12-12 21:23:17 +01:00
Valentin Maerten
5889ff6b65 fix: globals vars are available in vars at root level (#2403) 2025-12-12 21:20:27 +01:00
Valentin Maerten
85a98b5f90 chore: changelog for #2566 2025-12-12 21:11:30 +01:00
Timothy Rule
89b6140166 fix: always run a watch task regardless of run setting (#2566) 2025-12-12 21:09:35 +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
228 changed files with 11777 additions and 3412 deletions

3
.gitattributes vendored
View File

@@ -1,2 +1,5 @@
* text=auto
*.mdx -linguist-detectable
# Keep LF line endings in testdata for consistent checksums across platforms
testdata/** text eol=lf

View File

@@ -7,3 +7,5 @@ Please understand that it may take some time to be reviewed.
Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
-->
- [ ] I have read and followed the [Contribution Guide](https://taskfile.dev/contributing/)

11
.github/renovate.json vendored
View File

@@ -8,6 +8,17 @@
],
"mode": "full",
"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": [
{
"matchManagers": ["github-actions"],

View File

@@ -4,13 +4,16 @@ on:
issue_comment:
types: [created]
permissions:
issues: write
jobs:
issue-awaiting-response:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,

View File

@@ -4,13 +4,16 @@ on:
issues:
types: [closed]
permissions:
issues: write
jobs:
issue-closed:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const labels = await github.paginate(
github.rest.issues.listLabelsOnIssue, {

View File

@@ -2,16 +2,19 @@ name: issue experiment
on:
issues:
types: [labeled]
types: [field_added]
permissions:
issues: write
jobs:
issue-experiment-proposed:
if: github.event.label.name == format('status{0} proposed', ':')
issue-experiment-proposal:
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'proposal'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
@@ -20,12 +23,12 @@ jobs:
body: 'This issue has been marked as an experiment proposal! :test_tube: It will now enter a period of consultation during which we encourage the community to provide feedback on the proposed design. Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-draft:
if: github.event.label.name == format('status{0} draft', ':')
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'draft'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
@@ -34,12 +37,12 @@ jobs:
body: 'This experiment has been marked as a draft! :sparkles: This means that an initial implementation has been added to the latest release of Task! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-candidate:
if: github.event.label.name == format('status{0} candidate', ':')
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'candidate'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
@@ -48,12 +51,12 @@ jobs:
body: 'This experiment has been marked as a candidate! :fire: This means that the implementation is nearing completion and we are entering a period for final comments and feedback! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-stable:
if: github.event.label.name == format('status{0} stable', ':')
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'stable'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
@@ -62,12 +65,12 @@ jobs:
body: 'This experiment has been marked as stable! :metal: This means that the implementation is now final and ready to be released. No more changes will be made and the experiment is safe to use in production! You can find information about this experiment and how to enable it in our [experiments documentation](https://taskfile.dev/experiments). Please see the [experiment workflow documentation](https://taskfile.dev/experiments#workflow) for more information on how we release experiments.'
})
issue-experiment-released:
if: github.event.label.name == format('status{0} released', ':')
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'released'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
@@ -82,12 +85,12 @@ jobs:
state: 'closed'
})
issue-experiment-abandoned:
if: github.event.label.name == format('status{0} abandoned', ':')
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'abandoned'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
@@ -102,12 +105,12 @@ jobs:
state: 'closed'
})
issue-experiment-superseded:
if: github.event.label.name == format('status{0} superseded', ':')
if: github.event.issue_field.id == '6591' && github.event.issue_field_value.option.name == 'superseded'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,

View File

@@ -4,13 +4,16 @@ on:
issues:
types: [opened]
permissions:
issues: write
jobs:
issue-needs-triage:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{secrets.GH_PAT}}
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const labels = await github.paginate(
github.rest.issues.listLabelsOnIssue, {

View File

@@ -8,33 +8,36 @@ on:
branches:
- main
permissions:
contents: read
jobs:
lint:
name: Lint
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
go-version: [1.25.10, 1.26.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.1.0
version: v2.12.2
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
python-version: 3.14
- uses: actions/checkout@v5
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'

69
.github/workflows/pr-build.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: PR Build
on:
pull_request_target:
types: [labeled, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
build:
if: contains(github.event.pull_request.labels.*.name, 'needs-build')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.x"
cache: true
- uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
with:
version: "~> v2"
args: release --snapshot --clean --config .goreleaser-pr.yml
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_linux_amd64
path: dist/task_linux_amd64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_linux_arm64
path: dist/task_linux_arm64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_darwin_amd64
path: dist/task_darwin_amd64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_darwin_arm64
path: dist/task_darwin_arm64.tar.gz
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: task_windows_amd64
path: dist/task_windows_amd64.zip
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: checksums
path: dist/task_checksums.txt
- uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
token: ${{secrets.GITHUB_TOKEN}}
issue-number: ${{ github.event.pull_request.number }}
body-includes: "📦 Build artifacts ready!"
- uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
token: ${{secrets.GITHUB_TOKEN}}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## 📦 Build artifacts ready!
Download binaries from [this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).
Available platforms: Linux, macOS, Windows (amd64, arm64)
edit-mode: replace

View File

@@ -4,27 +4,31 @@ on:
workflow_dispatch:
schedule:
- cron: 0 0 * * *
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.25.x
go-version: 1.26.x
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
with:
distribution: goreleaser-pro
version: latest
args: release --clean --nightly -f .goreleaser-nightly.yml
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}

View File

@@ -3,45 +3,52 @@ name: goreleaser
on:
push:
tags:
- 'v*'
- "v*"
permissions:
id-token: write # Required for OIDC
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.25.x
go-version: 1.26.x
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Update npm
run: npm install -g npm@latest
- name: npm-login
run: |
npm config set '//registry.npmjs.org/:_authToken'=${{ secrets.NPM_TOKEN }}
- name: Install Task
uses: go-task/setup-task@v1
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
node-version: '22.x'
cache: 'pnpm'
package_json_file: "website/package.json"
run_install: "true"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
with:
distribution: goreleaser-pro
version: latest
args: release --clean --draft
env:
GITHUB_TOKEN: ${{secrets.GH_PAT}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GH_GORELEASER_TOKEN: ${{secrets.GH_GORELEASER_TOKEN}}
GORELEASER_KEY: ${{secrets.GORELEASER_KEY}}
CLOUDSMITH_TOKEN: ${{secrets.CLOUDSMITH_TOKEN}}
@@ -49,3 +56,5 @@ jobs:
shell: bash
run: |
task website:deploy:prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

19
.github/workflows/security.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Security
on:
pull_request:
push:
tags:
- v*
branches:
- main
permissions:
contents: read
jobs:
govulncheck:
name: govulncheck
runs-on: ubuntu-latest
steps:
- uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4

View File

@@ -8,31 +8,26 @@ on:
branches:
- main
permissions:
contents: read
jobs:
test:
name: Test
strategy:
fail-fast: false
matrix:
go-version: [1.24.x, 1.25.x]
go-version: [1.25.10, 1.26.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{matrix.platform}}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v5
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{matrix.go-version}}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v5
- 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::'
run: go run ./cmd/task test

1
.gitignore vendored
View File

@@ -35,3 +35,4 @@ tags
/testdata/vars/v1
/tmp
node_modules
website/.netlify/

View File

@@ -2,45 +2,33 @@ version: "2"
formatters:
enable:
- gofmt
- gofumpt
- goimports
- gci
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
gofumpt:
module-path: github.com/go-task/task/v3
goimports:
local-prefixes:
- github.com/go-task
gci:
sections:
- standard
- default
- prefix(github.com/go-task)
- localmodule
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
linters:
enable:
- depguard
- gosec
- mirror
- misspell
- modernize
- noctx
- paralleltest
- thelper
- tparallel
- usetesting
settings:
gosec:
excludes:
- G306
depguard:
rules:
main:
@@ -52,13 +40,8 @@ linters:
- pkg: errors
desc: Use github.com/go-task/task/v3/errors instead
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$

31
.goreleaser-pr.yml Normal file
View File

@@ -0,0 +1,31 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
builds:
- binary: task
main: ./cmd/task
goos: [windows, darwin, linux]
goarch: [amd64, arm64]
env:
- CGO_ENABLED=0
mod_timestamp: '{{ .CommitTimestamp }}'
flags:
- -trimpath
ldflags:
- "-s -w"
archives:
- name_template: '{{.Binary}}_{{.Os}}_{{.Arch}}'
files:
- README.md
- LICENSE
- completion/**/*
format_overrides:
- goos: windows
formats: [zip]
snapshot:
version_template: 'pr-{{ .ShortCommit }}'
checksum:
name_template: 'task_checksums.txt'

View File

@@ -22,6 +22,8 @@ builds:
goarch: '386'
- goos: darwin
goarch: riscv64
- goos: windows
goarch: arm
- goos: windows
goarch: riscv64
env:
@@ -60,7 +62,7 @@ nfpms:
- vendor: Task
homepage: https://taskfile.dev
maintainer: The Task authors <task@taskfile.dev>
description: Simple task runner written in Go
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
section: golang
license: MIT
conflicts:
@@ -69,7 +71,7 @@ nfpms:
- deb
- rpm
- apk
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
file_name_template: '{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}'
contents:
- src: completion/bash/task.bash
dst: /etc/bash_completion.d/task
@@ -80,13 +82,14 @@ nfpms:
brews:
- name: go-task
description: Task runner / simpler Make alternative written in Go
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
license: MIT
homepage: https://taskfile.dev
directory: Formula
repository:
owner: go-task
name: homebrew-tap
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
test: system "#{bin}/task", "--help"
install: |-
bin.install "task"
@@ -100,8 +103,8 @@ brews:
winget:
- name: Task
publisher: Task
short_description: A task runner / simpler Make alternative written in Go
description: Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.
short_description: The modern task runner.
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
license: MIT
homepage: https://taskfile.dev/
publisher_url: https://taskfile.dev/
@@ -127,7 +130,8 @@ winget:
repository:
owner: go-task
name: winget-pkgs
branch: 'chore/task-{{.Version}}'
branch: 'task-{{.Version}}'
token: "{{.Env.GH_GORELEASER_TOKEN}}" # So that it runs as the task-bot user
pull_request:
enabled: true
draft: false
@@ -136,13 +140,14 @@ winget:
owner: microsoft
name: winget-pkgs
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
description: A fast, cross-platform build tool inspired by Make, designed for modern workflows.
homepage: https://taskfile.dev
license: MIT
author: "The Task authors"
@@ -153,7 +158,6 @@ npms:
- "build-tool"
- "task-runner"
cloudsmiths:
- organization: "task"
repository: "{{if not .IsNightly}}task{{end}}"

View File

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

View File

@@ -1,6 +1,206 @@
# Changelog
## v3.45.1 - 2025-09-15
## v3.51.1 - 2026-05-16
- A significant performance boost was achieved for large Taskfiles (monorepos)
by skipping templating altogether when the string is static (#2820 by @romnn).
- Added `absPath` template function that resolves a path to its absolute form,
cleaning `..` and `.` components (#2681, #2788 by @mateenanjum).
- Added `joinEnv` function to join paths based on your oprating system: `;` for
Windows and `:` elsewhere, and `joinUrl` to join URL paths. Also, added two
new special variables: `FILE_PATH_SEPARATOR` which returns `\` on Windows
and `/` elsewhere, and `PATH_LIST_SEPARATOR` which returns `;` on Windows and
`:` elsewhere (#2406, #2408 by @solvingj).
- Update the shell interpreter with a regression fix (#2812, #2832 by
@andreynering).
- Fix potential panic with the shell interpreter (#2810 by @trulede).
## v3.50.0 - 2026-04-13
- Added `enum.ref` support in `requires`: enum constraints can now reference
variables or template pipelines (e.g., `ref: .ALLOWED_ENVS`) instead of
duplicating static lists. Combined with `sh:` variables, this enables fully
dynamic enum validation (#2678 by @vmaerten).
- Fixed Fish completion using hardcoded `task` binary name instead of
`$GO_TASK_PROGNAME` for experiments cache (#2730, #2727 by @SergioChan).
- Fixed watch mode ignoring SIGHUP signal, causing the watcher to exit instead
of restarting (#2764, #2642).
- Fixed a long time bug where the task wouldn't re-run as it should when using
`method: timestamp` and the files listed on `generates:` were deleted.
This makes `method: timestamp` behaves the same as `method: checksum`
(#1230, #2716 by @drichardson).
## v3.49.1 - 2026-03-08
* Reverted #2632 for now, which caused some regressions. That change will be
reworked (#2720, #2722, #2723).
## v3.49.0 - 2026-03-07
- Fixed included Taskfiles with `watch: true` not triggering watch mode when
called from the root Taskfile (#2686, #1763 by @trulede).
- Fixed Remote Git Taskfiles failing on Windows due to backslashes in URL paths
(#2656 by @Trim21).
- Fixed remote Git Taskfiles timing out when resolving includes after accepting
the trust prompt (#2669, #2668 by @vmaerten).
- Fixed unclear error message when Taskfile search stops at a directory
ownership boundary (#2682, #1683 by @trulede).
- Fixed global variables from imported Taskfiles not resolving `ref:` values
correctly (#2632 by @trulede).
- Every `.taskrc.yml` option can now be overridden with a `TASK_`-prefixed
environment variable, making CI and container configuration easier (#2607,
#1066 by @vmaerten).
## v3.48.0 - 2026-01-26
- Fixed `if:` conditions when using to check dynamic variables. Also, skip
variable prompt if task would be skipped by `if:` (#2658, #2660 by @vmaerten).
- Fixed `ROOT_TASKFILE` variable pointing to directory instead of the actual
Taskfile path when no explicit `-t` flag is provided (#2635, #1706 by
@trulede).
- Included Taskfiles with `silent: true` now properly propagate silence to their
tasks, while still allowing individual tasks to override with `silent: false`
(#2640, #1319 by @trulede).
- Added TLS certificate options for Remote Taskfiles: use `--cacert` for
self-signed certificates and `--cert`/`--cert-key` for mTLS authentication
(#2537, #2242 by @vmaerten).
## v3.47.0 - 2026-01-24
- Fixed remote git Taskfiles: cloning now works without explicit ref, and
directory includes are properly resolved (#2602 by @vmaerten).
- For `output: prefixed`, print `prefix:` if set instead of task name (#1566,
#2633 by @trulede).
- Ensure no ANSI sequences are printed for `--color=false` (#2560, #2584 by
@trulede).
- Task aliases can now contain wildcards and will match accordingly (e.g., `s-*`
as alias for `start-*`) (#1900, #2234 by @vmaerten).
- Added conditional execution with the `if` field: skip tasks, commands, or task
calls based on shell exit codes or template expressions like
`{{ eq .ENV "prod" }}` (#2564, #608 by @vmaerten).
- Task can now interactively prompt for missing required variables when running
in a TTY, with support for enum selection menus. Enable with `--interactive`
flag or `interactive: true` in `.taskrc.yml` (#2579, #2079 by @vmaerten).
## v3.46.4 - 2025-12-24
- Fixed regressions in completion script for Fish (#2591, #2604, #2592 by
@WinkelCode).
## v3.46.3 - 2025-12-19
- Fixed regression in completion script for zsh (#2593, #2594 by @vmaerten).
## v3.46.2 - 2025-12-18
- Fixed a regression on previous release that affected variables passed via
command line (#2588, #2589 by @vmaerten).
## v3.46.1 - 2025-12-18
### ✨ Features
- 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).
- 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 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).
- 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).
- When running in GitHub Actions, Task now automatically emits error annotations
on failure, improving visibility in workflow summaries (#2568 by @vmaerten).
- The `--yes` flag is now accessible in templates via the new `CLI_ASSUME_YES`
variable (#2577, #2479 by @semihbkgr).
- Improved shell completion scripts (Zsh, Fish, PowerShell) by adding missing
flags and dynamic experimental feature detection (#2532 by @vmaerten).
- Remote Taskfiles now accept `application/octet-stream` Content-Type (#2536,
#1944 by @vmaerten).
- 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).
- Added `--remote-cache-dir` flag and `remote.cache-dir` taskrc option to
customize the cache directory for Remote Taskfiles (#2572 by @vmaerten).
- Zsh completion now supports zstyle verbose option to show or hide task
descriptions (#2571 by @vmaerten).
- Task now automatically enables colored output in CI environments (GitHub
Actions, GitLab CI, etc.) without requiring FORCE_COLOR=1 (#2569 by
@vmaerten).
- Added color taskrc option to explicitly enable or disable colored output
globally (#2569 by @vmaerten).
- Improved Git Remote Taskfiles by switching to go-getter: SSH authentication
now works out of the box and `applyOf` is properly supported (#2512 by
@vmaerten).
### 🐛 Fixes
- 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).
- Fixed Zsh and Fish completions to stop suggesting task names after `--`
separator, allowing proper CLI_ARGS completion (#1843, #1844 by
@boiledfroginthewell).
- Watch mode (`--watch`) now always runs the task, regardless of `run: once` or
`run: when_changed` settings (#2566, #1388 by @trulede).
- Fixed global variables (CLI_ARGS, CLI_FORCE, etc.) not being accessible in
root-level vars section (#2403, #2397 by @trulede, @vmaerten).
- Fixed a bug where `ignore_error` was ignored when using `task:` to call
another task (#2552, #363 by @trulede).
- Fixed Zsh completion not suggesting global tasks when using `-g`/`--global`
flag (#1574, #2574 by @vmaerten).
- Fixed Fish completion failing to parse task descriptions containing colons
(e.g., URLs or namespaced functions) (#2101, #2573 by @vmaerten).
- Fixed false positive "property 'for' is not allowed" warnings in IntelliJ when
using `for` loops in Taskfiles (#2576 by @vmaerten).
## 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
@@ -49,7 +249,7 @@ more timely. We have already merged a couple of longstanding PRs in our
@pd93, @shrink, @trim21 and all the previous contributors to
[arduino/setup-task](https://github.com/arduino/setup-task/)).
## v3.45.0 - 2025-09-15
## v3.45.0-v3.45.2 - 2025-09-15
Failed due to an issue with our release process.

View File

@@ -3,14 +3,14 @@
<img src="website/src/public/img/logo.svg" width="200px" height="200px" />
</a>
<h1>Task</h1>
<h1>Task: The Modern Task Runner</h1>
<p>
Task is a task runner / build tool that aims to be simpler and easier to use than, for example, <a href="https://www.gnu.org/software/make/">GNU Make<a>.
A fast, cross-platform build tool inspired by Make, designed for modern workflows.
</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> &bullet; <a href="https://taskfile.dev/docs/getting-started">Getting Started</a> &bullet; <a href="https://taskfile.dev/docs/guide">Docs</a> &bullet; <a href="https://twitter.com/taskfiledev">Twitter</a> &bullet; <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> &bullet; <a href="https://fosstodon.org/@task">Mastodon</a> &bullet; <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p>
<h1>Gold Sponsors</h1>
@@ -19,7 +19,29 @@
<tr>
<td align="center" valign="middle">
<a target="_blank" href="https://devowl.io">
<img src="https://devowl.io/wp-content/uploads/meta/favicon.webp" 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://goodx.international/">
<img src="website/src/public/img/goodx.svg" height="80px" width="200px" title="GoodX" />
</a>
</td>
<td align="center" valign="middle">
<a target="_blank" href="https://magic.dev/">
<img src="website/src/public/img/magic.png" height="100px" width="200px" title="Magic" />
</a>
</td>
</tr>
</table>
<h2>Community Sponsors</h2>
<table>
<tr>
<td align="center" valign="middle">
<a target="_blank" href="https://cloudsmith.com/">
<img src="website/src/public/img/cloudsmith.svg" height="100px" width="200px" title="Cloudsmith" />
</a>
</td>
</tr>

View File

@@ -29,6 +29,7 @@ tasks:
aliases: [i]
sources:
- './**/*.go'
- go.mod
cmds:
- go install -v ./cmd/task
@@ -226,7 +227,7 @@ tasks:
- "git add --all"
- "git commit -m v{{.VERSION}}"
- "git push"
- "git tag v{{.VERSION}}"
- "git tag -a v{{.VERSION}} -m v{{.VERSION}}"
- "git push origin tag v{{.VERSION}}"
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
silent: true

View File

@@ -117,15 +117,19 @@ func changelog(version *semver.Version) error {
changelog = changelogReleaseRegex.ReplaceAllString(changelog, fmt.Sprintf("## v%s - %s", version, date))
// Write the changelog to the source file
if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil {
if err := os.WriteFile(changelogSource, []byte(changelog), 0o644); err != nil { //nolint:gosec
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
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
return os.WriteFile(changelogTarget, []byte(changelog), 0o644)
return os.WriteFile(changelogTarget, []byte(changelogWithFrontmatter), 0o644) //nolint:gosec
}
func setVersionFile(fileName string, version *semver.Version) error {

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/spf13/pflag"
@@ -28,19 +29,34 @@ func main() {
Color: flags.Color,
}
if err, ok := err.(*errors.TaskRunError); ok && flags.ExitCode {
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err)
os.Exit(err.TaskExitCode())
}
if err, ok := err.(errors.TaskError); ok {
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err)
os.Exit(err.Code())
}
emitCIErrorAnnotation(err)
l.Errf(logger.Red, "%v\n", err)
os.Exit(errors.CodeUnknown)
}
os.Exit(errors.CodeOk)
}
// emitCIErrorAnnotation emits an error annotation for supported CI providers.
func emitCIErrorAnnotation(err error) {
if isGA, _ := strconv.ParseBool(os.Getenv("GITHUB_ACTIONS")); !isGA {
return
}
if e, ok := err.(*errors.TaskRunError); ok {
fmt.Fprintf(os.Stdout, "::error title=Task '%s' failed::%v\n", e.TaskName, e.Err)
return
}
fmt.Fprintf(os.Stdout, "::error title=Task failed::%v\n", err)
}
func run() error {
log := &logger.Logger{
Stdout: os.Stdout,
@@ -156,18 +172,23 @@ func run() error {
calls = append(calls, &task.Call{Task: "default"})
}
// Merge CLI variables first (e.g. FOO=bar) so they take priority over Taskfile defaults
e.Taskfile.Vars.Merge(globals, nil)
// Then ReverseMerge special variables so they're available for templating
cliArgsPostDashQuoted, err := args.ToQuotedString(cliArgsPostDash)
if err != nil {
return err
}
globals.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
globals.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
e.Taskfile.Vars.Merge(globals, nil)
specialVars := ast.NewVars()
specialVars.Set("CLI_ARGS", ast.Var{Value: cliArgsPostDashQuoted})
specialVars.Set("CLI_ARGS_LIST", ast.Var{Value: cliArgsPostDash})
specialVars.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
specialVars.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
specialVars.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
specialVars.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
specialVars.Set("CLI_ASSUME_YES", ast.Var{Value: flags.AssumeYes})
e.Taskfile.Vars.ReverseMerge(specialVars, nil)
if !flags.Watch {
e.InterceptInterruptSignals()
}

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)
// 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
// Preserve the Sh field so it can be displayed in summary
if !evaluateShVars && newVar.Value == nil {
result.Set(k, ast.Var{Value: ""})
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
return nil
}
// If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars {
result.Set(k, ast.Var{Value: newVar.Value})
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
return nil
}
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
@@ -197,18 +198,23 @@ func (c *Compiler) ResetCache() {
}
func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, error) {
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
// across platforms. This prevents issues with backslashes being interpreted
// as escape sequences when paths are used in shell commands on Windows.
allVars := map[string]string{
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
"ROOT_DIR": c.Dir,
"USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(),
"TASK_EXE": filepath.ToSlash(os.Args[0]),
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
"ROOT_DIR": filepath.ToSlash(c.Dir),
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
"TASK_VERSION": version.GetVersion(),
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
"FILE_PATH_SEPARATOR": string(os.PathSeparator),
}
if t != nil {
allVars["TASK"] = t.Task
allVars["TASK_DIR"] = filepathext.SmartJoin(c.Dir, t.Dir)
allVars["TASKFILE"] = t.Location.Taskfile
allVars["TASKFILE_DIR"] = filepath.Dir(t.Location.Taskfile)
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
} else {
allVars["TASK"] = ""
allVars["TASK_DIR"] = ""

View File

@@ -1,6 +1,7 @@
# vim: set tabstop=2 shiftwidth=2 expandtab:
_GO_TASK_COMPLETION_LIST_OPTION='--list-all'
TASK_CMD="${TASK_EXE:-task}"
function _task()
{
@@ -21,10 +22,14 @@ function _task()
# Handle special arguments of options.
case "$prev" in
-d|--dir)
-d|--dir|--remote-cache-dir)
_filedir -d
return $?
;;
--cacert|--cert|--cert-key)
_filedir
return $?
;;
-t|--taskfile)
_filedir yaml || return $?
_filedir yml
@@ -52,4 +57,4 @@ function _task()
__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 --inherit-variable GO_TASK_PROGNAME
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 ($GO_TASK_PROGNAME --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
# Check if the global task is requested
@@ -27,28 +54,67 @@ function __task_get_tasks --description "Prints all available tasks with their d
end
# Grab names and descriptions (if any) of the tasks
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):[[:space:]]\{2,\}\(.*\)[[:space:]]\{2,\}(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):[[:space:]]\{2,\}\(.*\)/\1\t\2/'| string split0)
if test $output
echo $output
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
specified.' -xa "(__task_get_tasks)"
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 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)'
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'sets directory of execution'
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 f -l force -d 'forces execution even when the task is up-to-date'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'shows Task usage'
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 l -l list -d 'lists tasks with description of current Taskfile'
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 p -l parallel -d 'executes tasks provided on command line in parallel'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disables echoing'
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 -l summary -d 'show summary about a task'
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 v -l verbose -d 'enables verbose mode'
complete -c $GO_TASK_PROGNAME -l version -d 'show Task version'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'enables watch of the given task'
# Standard flags
complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks'
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks'
complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell"
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution'
complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names'
complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing'
complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command'
complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments'
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 -s f -l force -d 'force execution even when up-to-date'
complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help'
complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile'
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 -l interactive -d 'prompt for missing required variables'
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'
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
# 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,86 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
if ($commandName.StartsWith('-')) {
$completions = @(
[CompletionResult]::new('--list-all ', '--list-all ', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('--color ', '--color', [CompletionResultType]::ParameterName, '--color'),
[CompletionResult]::new('--concurrency=', '--concurrency=', [CompletionResultType]::ParameterName, 'concurrency'),
[CompletionResult]::new('--interval=', '--interval=', [CompletionResultType]::ParameterName, 'interval'),
[CompletionResult]::new('--output=interleaved ', '--output=interleaved', [CompletionResultType]::ParameterName, '--output='),
[CompletionResult]::new('--output=group ', '--output=group', [CompletionResultType]::ParameterName, '--output='),
[CompletionResult]::new('--output=prefixed ', '--output=prefixed', [CompletionResultType]::ParameterName, '--output='),
[CompletionResult]::new('--dry ', '--dry', [CompletionResultType]::ParameterName, '--dry'),
[CompletionResult]::new('--force ', '--force', [CompletionResultType]::ParameterName, '--force'),
[CompletionResult]::new('--parallel ', '--parallel', [CompletionResultType]::ParameterName, '--parallel'),
[CompletionResult]::new('--silent ', '--silent', [CompletionResultType]::ParameterName, '--silent'),
[CompletionResult]::new('--status ', '--status', [CompletionResultType]::ParameterName, '--status'),
[CompletionResult]::new('--verbose ', '--verbose', [CompletionResultType]::ParameterName, '--verbose'),
[CompletionResult]::new('--watch ', '--watch', [CompletionResultType]::ParameterName, '--watch')
# Standard flags (alphabetical order)
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),
[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),
[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),
[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),
[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('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
[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')
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
# 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) }
}

View File

@@ -1,67 +1,151 @@
#compdef task
compdef _task task
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}"
# 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
function __task_list() {
local -a scripts cmd
local -i enabled=0
local taskfile item task desc
cmd=(task)
cmd=($TASK_CMD)
taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
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
enabled=1
cmd+=(--taskfile "$taskfile")
else
for taskfile in {T,t}askfile{,.dist}.{yaml,yml}; do
if [[ -f "$taskfile" ]]; then
enabled=1
break
fi
done
fi
# Check if global flag is set
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
cmd+=(--global)
fi
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
enabled=1
fi
(( enabled )) || return 0
scripts=()
for item in "${(@)${(f)$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION)}[2,-1]#\* }"; do
# Read zstyle verbose option (default = true via -T)
local show_desc
zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false
for item in "${(@)${(f)output}[2,-1]#\* }"; do
task="${item%%:[[:space:]]*}"
desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
if [[ "$show_desc" == "true" ]]; then
local desc="${item##[^[:space:]]##[[:space:]]##}"
scripts+=( "${task//:/\\:}:$desc" )
else
scripts+=( "$task" )
fi
done
_describe 'Task to run' scripts
if [[ "$show_desc" == "true" ]]; then
_describe 'Task to run' scripts
else
compadd -Q -a scripts
fi
}
_task() {
_arguments \
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
'(-c --color)'{-c,--color}'[colored output]' \
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
'(-s --silent)'{-s,--silent}'[disable echoing]' \
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
+ '(operation)' \
{-l,--list}'[list describable tasks]' \
{-a,--list-all}'[list all tasks]' \
{-i,--init}'[create new Taskfile.yml]' \
'(-*)'{-h,--help}'[show help]' \
'(-*)--version[show version and exit]' \
'*: :__task_list'
local -a standard_args operation_args
standard_args=(
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'
'(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'
'(-f --force)'{-f,--force}'[run even if task is up-to-date]'
'(-c --color)'{-c,--color}'[colored output]'
'(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'
'(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'
'(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'
'(--dry)--dry[dry-run mode, compile and print tasks only]'
'(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'
'(--experiments)--experiments[list available experiments]'
'(-g --global)'{-g,--global}'[run global Taskfile from home directory]'
'(--insecure)--insecure[allow insecure Taskfile downloads]'
'(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '
'(-j --json)'{-j,--json}'[format task list as JSON]'
'(--nested)--nested[nest namespaces when listing as JSON]'
'(--no-status)--no-status[ignore status when listing as JSON]'
'(--interactive)--interactive[prompt for missing required variables]'
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
'(--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: '
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
'(--cert)--cert[client certificate for mTLS]:file:_files'
'(--cert-key)--cert-key[client certificate private key]:file:_files'
)
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

View File

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

View File

@@ -54,6 +54,10 @@ func (err *TaskRunError) TaskExitCode() int {
return err.Code()
}
func (err *TaskRunError) Unwrap() error {
return err.Err
}
// TaskInternalError when the user attempts to invoke a task that is internal.
type TaskInternalError struct {
TaskName string
@@ -191,9 +195,9 @@ type TaskNotAllowedVarsError struct {
func (err *TaskNotAllowedVarsError) Error() string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName)) //nolint:staticcheck
for _, s := range err.NotAllowedVars {
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum)) //nolint:staticcheck
}
return builder.String()

View File

@@ -3,6 +3,7 @@ package errors
import (
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/Masterminds/semver/v3"
@@ -11,16 +12,23 @@ import (
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when
// searching the filesystem.
type TaskfileNotFoundError struct {
URI string
Walk bool
URI string
Walk bool
AskInit bool
OwnerChange bool
}
func (err TaskfileNotFoundError) Error() string {
var walkText string
if err.Walk {
walkText = " (or any of the parent directories)"
if err.OwnerChange {
walkText = " (or any of the parent directories until ownership changed)."
} else if err.Walk {
walkText = " (or any of the parent directories)."
}
return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
if err.AskInit {
walkText += " Run `task --init` to create a new Taskfile."
}
return fmt.Sprintf(`task: No Taskfile found at %q%s`, filepath.ToSlash(err.URI), walkText)
}
func (err TaskfileNotFoundError) Code() int {
@@ -47,7 +55,7 @@ type TaskfileInvalidError struct {
}
func (err TaskfileInvalidError) Error() string {
return fmt.Sprintf("task: Failed to parse %s:\n%v", err.URI, err.Err)
return fmt.Sprintf("task: Failed to parse %s:\n%v", filepath.ToSlash(err.URI), err.Err)
}
func (err TaskfileInvalidError) Code() int {
@@ -66,7 +74,7 @@ func (err TaskfileFetchFailedError) Error() string {
if err.HTTPStatusCode != 0 {
statusText = fmt.Sprintf(" with status code %d (%s)", err.HTTPStatusCode, http.StatusText(err.HTTPStatusCode))
}
return fmt.Sprintf(`task: Download of %q failed%s`, err.URI, statusText)
return fmt.Sprintf(`task: Download of %q failed%s`, filepath.ToSlash(err.URI), statusText)
}
func (err TaskfileFetchFailedError) Code() int {
@@ -82,7 +90,7 @@ type TaskfileNotTrustedError struct {
func (err *TaskfileNotTrustedError) Error() string {
return fmt.Sprintf(
`task: Taskfile %q not trusted by user`,
err.URI,
filepath.ToSlash(err.URI),
)
}
@@ -99,7 +107,7 @@ type TaskfileNotSecureError struct {
func (err *TaskfileNotSecureError) Error() string {
return fmt.Sprintf(
`task: Taskfile %q cannot be downloaded over an insecure connection. You can override this by using the --insecure flag`,
err.URI,
filepath.ToSlash(err.URI),
)
}
@@ -116,7 +124,7 @@ type TaskfileCacheNotFoundError struct {
func (err *TaskfileCacheNotFoundError) Error() string {
return fmt.Sprintf(
`task: Taskfile %q was not found in the cache. Remove the --offline flag to use a remote copy or download it using the --download flag`,
err.URI,
filepath.ToSlash(err.URI),
)
}
@@ -137,12 +145,12 @@ func (err *TaskfileVersionCheckError) Error() string {
if err.SchemaVersion == nil {
return fmt.Sprintf(
`task: Missing schema version in Taskfile %q`,
err.URI,
filepath.ToSlash(err.URI),
)
}
return fmt.Sprintf(
"task: Invalid schema version in Taskfile %q:\nSchema version (%s) %s",
err.URI,
filepath.ToSlash(err.URI),
err.SchemaVersion.String(),
err.Message,
)
@@ -162,7 +170,7 @@ type TaskfileNetworkTimeoutError struct {
func (err *TaskfileNetworkTimeoutError) Error() string {
return fmt.Sprintf(
`task: Network connection timed out after %s while attempting to download Taskfile %q`,
err.Timeout, err.URI,
err.Timeout, filepath.ToSlash(err.URI),
)
}
@@ -179,8 +187,8 @@ type TaskfileCycleError struct {
func (err TaskfileCycleError) Error() string {
return fmt.Sprintf("task: include cycle detected between %s <--> %s",
err.Source,
err.Destination,
filepath.ToSlash(err.Source),
filepath.ToSlash(err.Destination),
)
}
@@ -199,7 +207,7 @@ type TaskfileDoesNotMatchChecksum struct {
func (err *TaskfileDoesNotMatchChecksum) Error() string {
return fmt.Sprintf(
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
err.URI,
filepath.ToSlash(err.URI),
err.ActualChecksum,
err.ExpectedChecksum,
)

View File

@@ -7,7 +7,7 @@ import (
"sync"
"time"
"github.com/puzpuzpuz/xsync/v3"
"github.com/puzpuzpuz/xsync/v4"
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/internal/logger"
@@ -34,19 +34,27 @@ type (
Insecure bool
Download bool
Offline bool
TrustedHosts []string
Timeout time.Duration
CacheExpiryDuration time.Duration
RemoteCacheDir string
CACert string
Cert string
CertKey string
Watch bool
Verbose bool
Silent bool
DisableFuzzy bool
AssumeYes bool
AssumeTerm bool // Used for testing
Interactive bool
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
Failfast bool
// I/O
Stdin io.Reader
@@ -63,14 +71,16 @@ type (
UserWorkingDir string
EnableVersionCheck bool
fuzzyModel *fuzzy.Model
fuzzyModel *fuzzy.Model
fuzzyModelOnce sync.Once
promptedVars *ast.Vars // vars collected via interactive prompts
concurrencySemaphore chan struct{}
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
executionHashes map[string]context.Context
executionHashesMutex sync.Mutex
watchedDirs *xsync.MapOf[string, bool]
watchedDirs *xsync.Map[string, bool]
}
TempDir struct {
Remote string
@@ -225,6 +235,20 @@ func (o *offlineOption) ApplyToExecutor(e *Executor) {
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
// default, the timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ExecutorOption {
@@ -240,7 +264,7 @@ func (o *timeoutOption) ApplyToExecutor(e *Executor) {
}
// WithCacheExpiryDuration sets the duration after which the cache is considered
// expired. By default, the cache is considered expired after 24 hours.
// expired. By default, the cache is 0 (disabled).
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration}
}
@@ -253,6 +277,58 @@ func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
r.CacheExpiryDuration = o.duration
}
// WithRemoteCacheDir sets the directory where remote taskfiles are cached.
func WithRemoteCacheDir(dir string) ExecutorOption {
return &remoteCacheDirOption{dir: dir}
}
type remoteCacheDirOption struct {
dir string
}
func (o *remoteCacheDirOption) ApplyToExecutor(e *Executor) {
e.RemoteCacheDir = o.dir
}
// WithCACert sets the path to a custom CA certificate for TLS connections.
func WithCACert(caCert string) ExecutorOption {
return &caCertOption{caCert: caCert}
}
type caCertOption struct {
caCert string
}
func (o *caCertOption) ApplyToExecutor(e *Executor) {
e.CACert = o.caCert
}
// WithCert sets the path to a client certificate for TLS connections.
func WithCert(cert string) ExecutorOption {
return &certOption{cert: cert}
}
type certOption struct {
cert string
}
func (o *certOption) ApplyToExecutor(e *Executor) {
e.Cert = o.cert
}
// WithCertKey sets the path to a client certificate key for TLS connections.
func WithCertKey(certKey string) ExecutorOption {
return &certKeyOption{certKey: certKey}
}
type certKeyOption struct {
certKey string
}
func (o *certKeyOption) ApplyToExecutor(e *Executor) {
e.CertKey = o.certKey
}
// WithWatch tells the [Executor] to keep running in the background and watch
// for changes to the fingerprint of the tasks that are run. When changes are
// detected, a new task run is triggered.
@@ -296,6 +372,19 @@ func (o *silentOption) ApplyToExecutor(e *Executor) {
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.
func WithAssumeYes(assumeYes bool) ExecutorOption {
return &assumeYesOption{assumeYes}
@@ -322,6 +411,19 @@ func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
e.AssumeTerm = o.assumeTerm
}
// WithInteractive tells the [Executor] to prompt for missing required variables.
func WithInteractive(interactive bool) ExecutorOption {
return &interactiveOption{interactive}
}
type interactiveOption struct {
interactive bool
}
func (o *interactiveOption) ApplyToExecutor(e *Executor) {
e.Interactive = o.interactive
}
// WithDry tells the [Executor] to output the commands that would be run without
// actually running them.
func WithDry(dry bool) ExecutorOption {
@@ -502,3 +604,16 @@ type versionCheckOption struct {
func (o *versionCheckOption) ApplyToExecutor(e *Executor) {
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

@@ -143,12 +143,12 @@ func (tt *ExecutorTest) run(t *testing.T) {
t.Helper()
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
var buffer SyncBuffer
opts := append(
tt.executorOpts,
task.WithStdout(&buf),
task.WithStderr(&buf),
task.WithStdout(&buffer),
task.WithStderr(&buffer),
)
// If the test has input, create a reader for it and add it to the
@@ -165,13 +165,14 @@ func (tt *ExecutorTest) run(t *testing.T) {
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
goldie.WithEqualFn(NormalizedEqual),
)
// Call setup and check for errors
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
return
} else {
require.NoError(t, err)
@@ -192,7 +193,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
if err := e.Run(ctx, call); tt.wantRunError {
require.Error(t, err)
tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
return
} else {
require.NoError(t, err)
@@ -205,7 +206,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)
@@ -263,6 +264,23 @@ func TestVars(t *testing.T) {
task.WithSilent(true),
),
)
NewExecutorTest(t,
WithName("cli-var-priority-default"),
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
WithTask("cli-var-priority"),
)
NewExecutorTest(t,
WithName("cli-var-priority-override"),
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
WithTask("cli-var-priority"),
WithVar("CLI_VAR", "from_cli"),
)
}
func TestRequires(t *testing.T) {
@@ -334,6 +352,41 @@ func TestRequires(t *testing.T) {
),
WithTask("var-defined-in-task"),
)
NewExecutorTest(t,
WithName("enum ref - passes validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var-ref"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("enum ref - fails validation"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var-ref"),
WithVar("ENV", "invalid"),
WithRunError(),
)
NewExecutorTest(t,
WithName("enum ref - ref to non-list"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var-ref-invalid"),
WithVar("VALUE", "test"),
WithRunError(),
)
NewExecutorTest(t,
WithName("enum ref - ref to nonexistent var"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var-ref-nonexistent"),
WithVar("ENV", "dev"),
WithRunError(),
)
}
// TODO: mock fs
@@ -347,6 +400,7 @@ func TestSpecialVars(t *testing.T) {
// Root
"print-task",
"print-root-dir",
"print-root-taskfile",
"print-taskfile",
"print-taskfile-dir",
"print-task-dir",
@@ -621,6 +675,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) {
t.Parallel()
@@ -665,6 +743,36 @@ func TestLabel(t *testing.T) {
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("label in error"),
WithExecutorOptions(
task.WithDir("testdata/label_error"),
),
WithTask("foo"),
WithRunError(),
)
}
func TestPrefix(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("up to date"),
WithExecutorOptions(
task.WithDir("testdata/prefix_uptodate"),
task.WithOutputStyle(ast.Output{Name: "prefixed"}),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("up to dat with no output style"),
WithExecutorOptions(
task.WithDir("testdata/prefix_uptodate"),
),
WithTask("foo"),
)
}
func TestPromptInSummary(t *testing.T) {
@@ -987,3 +1095,128 @@ func TestIncludeChecksum(t *testing.T) {
WithFixtureTemplating(),
)
}
func TestIncludeSilent(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("include-taskfile-silent"),
WithExecutorOptions(
task.WithDir("testdata/includes_silent"),
),
WithTask("default"),
)
}
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(),
)
})
}
func TestIf(t *testing.T) {
t.Parallel()
tests := []struct {
name string
task string
vars map[string]any
verbose bool
}{
// Basic command-level if
{name: "cmd-if-true", task: "cmd-if-true"},
{name: "cmd-if-false", task: "cmd-if-false"},
// Task-level if
{name: "task-if-true", task: "task-if-true"},
{name: "task-if-false", task: "task-if-false", verbose: true},
// Task call with if
{name: "task-call-if-true", task: "task-call-if-true"},
{name: "task-call-if-false", task: "task-call-if-false", verbose: true},
// Go template conditions
{name: "template-eq-true", task: "template-eq-true"},
{name: "template-eq-false", task: "template-eq-false", verbose: true},
{name: "template-ne", task: "template-ne"},
{name: "template-bool-true", task: "template-bool-true"},
{name: "template-bool-false", task: "template-bool-false"},
{name: "template-direct-true", task: "template-direct-true"},
{name: "template-direct-false", task: "template-direct-false"},
{name: "template-and", task: "template-and"},
{name: "template-or", task: "template-or"},
// CLI variable override
{name: "template-cli-var", task: "template-cli-var", vars: map[string]any{"MY_VAR": "yes"}},
// Task-level if with template
{name: "task-level-template", task: "task-level-template"},
{name: "task-level-template-false", task: "task-level-template-false", verbose: true},
// For loop with if
{name: "if-in-for-loop", task: "if-in-for-loop", verbose: true},
// Task-level if with dynamic variable
{name: "task-if-dynamic-true", task: "task-if-dynamic-true"},
{name: "task-if-dynamic-false", task: "task-if-dynamic-false", verbose: true},
}
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithExecutorOptions(
task.WithDir("testdata/if"),
task.WithSilent(true),
task.WithVerbose(test.verbose),
),
WithTask(test.task),
}
if test.vars != nil {
for k, v := range test.vars {
opts = append(opts, WithVar(k, v))
}
}
NewExecutorTest(t, opts...)
}
}

View File

@@ -33,14 +33,12 @@ var xList []Experiment
func Parse(dir string) {
config, _ := taskrc.GetConfig(dir)
ParseWithConfig(dir, config)
}
func ParseWithConfig(dir string, config *ast.TaskRC) {
// Read any .env files
readDotEnv(dir)
// Initialize the experiments
GentleForce = New("GENTLE_FORCE", config, 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)

View File

@@ -127,6 +127,7 @@ func (tt *FormatterTest) run(t *testing.T) {
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
goldie.WithEqualFn(NormalizedEqual),
)
// Call setup and check for errors

148
go.mod
View File

@@ -1,66 +1,136 @@
module github.com/go-task/task/v3
go 1.24.0
go 1.25.10
require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.6
charm.land/lipgloss/v2 v2.0.3
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/Masterminds/semver/v3 v3.5.0
github.com/alecthomas/chroma/v2 v2.24.1
github.com/chainguard-dev/git-urls v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v3 v3.1.0
github.com/fatih/color v1.18.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.2
github.com/fatih/color v1.19.0
github.com/fsnotify/fsnotify v1.10.1
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/go-task/template v0.2.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-getter v1.8.6
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/puzpuzpuz/xsync/v4 v4.5.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.10
github.com/stretchr/testify v1.11.1
github.com/zeebo/xxh3 v1.0.2
golang.org/x/sync v0.17.0
golang.org/x/term v0.35.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/moreinterp v0.0.0-20250807215248-5a1a658912aa
mvdan.cc/sh/v3 v3.12.0
github.com/zeebo/xxh3 v1.1.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.20.0
golang.org/x/term v0.43.0
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/storage v1.61.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
github.com/charmbracelet/x/ansi v0.11.7 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 // indirect
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.36.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

356
go.sum
View File

@@ -1,85 +1,196 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.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-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE=
github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.8.6 h1:9sQboWULaydVphxc4S64oAI4YqpuCk7nPmvbk131ebY=
github.com/hashicorp/go-getter v1.8.6/go.mod h1:nVH12eOV2P58dIiL3rsU6Fh3wLeJEKBOJzhMmzlSWoo=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
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/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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=
@@ -89,107 +200,130 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
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/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync/v4 v4.5.0 h1:vOSWu6b57/emh+L/Cw0BeQfvxa/cogFywXHeGUxQxAg=
github.com/puzpuzpuz/xsync/v4 v4.5.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 h1:ax+jBy7xFhh+Ka0IGLmH5mft+YDuqvzEjSgWuAP0nsM=
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7/go.mod h1:/0Qr7qJeDwWxoKku2xKQ4Szc+SwBE3g9VE8jNiamsmc=
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 h1:cq+DjLAjz3ZPwh0+G571O/jMH0c0DzReDPLjQGL2/BA=
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8/go.mod h1:JNauIV2zopCBv/6o+umxcT3bKe8YUqYJaTZQYSYpKss=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/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.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
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.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0=
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/moreinterp v0.0.0-20250807215248-5a1a658912aa h1:sRmA9AmA5+9CbK6a7N52q9W9jAeoBy1EJ7cncm+SLxw=
mvdan.cc/sh/moreinterp v0.0.0-20250807215248-5a1a658912aa/go.mod h1:Of9PCedbLDYT8b3EyiYG64rNnx5nOp27OLCVdDrjJyo=
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/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE=
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997/go.mod h1:Qy/zdaMDxq9sT72Gi43K3gsV+TtTohyDO3f1cyBVwuo=
mvdan.cc/sh/v3 v3.13.2-0.20260503214111-9e7dd28c81cf h1:3mGRe/xSr7fd9m+c5FSab/CSCtADsbdMcyhYGdVR6DY=
mvdan.cc/sh/v3 v3.13.2-0.20260503214111-9e7dd28c81cf/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117 h1:BfzdGSjcnJBb8sPNLudpzTml8zFUxS1N0N/v9IIS6tQ=
mvdan.cc/sh/v3 v3.13.2-0.20260510185049-f5c6e2779117/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=

25
init.go
View File

@@ -6,9 +6,10 @@ import (
"github.com/go-task/task/v3/errors"
"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
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.
func InitTaskfile(path string) (string, error) {
fi, err := os.Stat(path)
if err == nil && !fi.IsDir() {
info, err := os.Stat(path)
if err == nil && !info.IsDir() {
return path, errors.TaskfileAlreadyExistsError{}
}
if fi != nil && fi.IsDir() {
path = filepathext.SmartJoin(path, defaultTaskFilename)
// path was a directory, so check if Taskfile.yml exists in it
if _, err := os.Stat(path); err == nil {
if info != nil && info.IsDir() {
// path was a directory, check if there is a Taskfile already
if hasDefaultTaskfile(path) {
return path, errors.TaskfileAlreadyExistsError{}
}
path = filepathext.SmartJoin(path, defaultFilename)
}
if err := os.WriteFile(path, []byte(DefaultTaskfile), 0o644); err != nil {
return path, err
}
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

@@ -10,6 +10,15 @@ type Copier[T any] interface {
DeepCopy() T
}
func Scalar[T any](orig *T) *T {
if orig == nil {
return nil
} else {
v := *orig
return &v
}
}
func Slice[T any](orig []T) []T {
if orig == nil {
return nil
@@ -72,7 +81,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
traverseFunc = func(copy, v reflect.Value) error {
switch v.Kind() {
case reflect.Ptr:
case reflect.Pointer:
// Unwrap the pointer
originalValue := v.Elem()
// If the pointer is nil, do nothing

62
internal/env/env.go vendored
View File

@@ -3,7 +3,9 @@ package env
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile/ast"
@@ -61,3 +63,63 @@ func isTypeAllowed(v any) bool {
func GetTaskEnv(key string) string {
return os.Getenv(taskVarPrefix + key)
}
// GetTaskEnvBool returns the boolean value of a TASK_ prefixed env var.
// Returns the value and true if set and valid, or false and false if not set or invalid.
func GetTaskEnvBool(key string) (bool, bool) {
v := GetTaskEnv(key)
if v == "" {
return false, false
}
b, err := strconv.ParseBool(v)
return b, err == nil
}
// GetTaskEnvInt returns the integer value of a TASK_ prefixed env var.
// Returns the value and true if set and valid, or 0 and false if not set or invalid.
func GetTaskEnvInt(key string) (int, bool) {
v := GetTaskEnv(key)
if v == "" {
return 0, false
}
i, err := strconv.Atoi(v)
return i, err == nil
}
// GetTaskEnvDuration returns the duration value of a TASK_ prefixed env var.
// Returns the value and true if set and valid, or 0 and false if not set or invalid.
func GetTaskEnvDuration(key string) (time.Duration, bool) {
v := GetTaskEnv(key)
if v == "" {
return 0, false
}
d, err := time.ParseDuration(v)
return d, err == nil
}
// GetTaskEnvString returns the string value of a TASK_ prefixed env var.
// Returns the value and true if set (non-empty), or empty string and false if not set.
func GetTaskEnvString(key string) (string, bool) {
v := GetTaskEnv(key)
return v, v != ""
}
// GetTaskEnvStringSlice returns a comma-separated list from a TASK_ prefixed env var.
// Returns the slice and true if set (non-empty), or nil and false if not set.
func GetTaskEnvStringSlice(key string) ([]string, bool) {
v := GetTaskEnv(key)
if v == "" {
return nil, false
}
parts := strings.Split(v, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
result = append(result, trimmed)
}
}
if len(result) == 0 {
return nil, false
}
return result, true
}

View File

@@ -127,12 +127,11 @@ func ExpandFields(s string) ([]string, error) {
s = escape(s)
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
for w, err := range p.WordsSeq(strings.NewReader(s)) {
if err != nil {
return nil, err
}
words = append(words, w)
return true
})
if err != nil {
return nil, err
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
@@ -147,7 +146,7 @@ func execHandlers() (handlers []func(next interp.ExecHandlerFunc) interp.ExecHan
if useGoCoreUtils {
handlers = append(handlers, coreutils.ExecHandler)
}
return
return handlers
}
func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {

View File

@@ -2,6 +2,7 @@ package fingerprint
import (
"os"
"path/filepath"
"sort"
"github.com/go-task/task/v3/internal/execext"
@@ -50,7 +51,8 @@ func collectKeys(m map[string]bool) []string {
keys := make([]string, 0, len(m))
for k, v := range m {
if v {
keys = append(keys, k)
// Normalize path separators for consistent sorting across platforms
keys = append(keys, filepath.ToSlash(k))
}
}
sort.Strings(keys)

View File

@@ -53,6 +53,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
if len(t.Generates) > 0 {
// For each specified 'generates' field, check whether the files actually exist
for _, g := range t.Generates {
// Exclusion patterns don't represent output files; skip them.
if g.Negate {
continue
}

View File

@@ -32,6 +32,28 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
if err != nil {
return false, nil
}
// If generates are declared, ensure they all exist. A missing generated
// file means the task must run regardless of timestamps.
if len(t.Generates) > 0 {
for _, g := range t.Generates {
// Exclusion patterns don't represent output files; skip them.
if g.Negate {
continue
}
files, err := glob(t.Dir, g.Glob)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
if len(files) == 0 {
return false, nil
}
}
}
generates, err := Globs(t.Dir, t.Generates)
if err != nil {
return false, nil

View File

@@ -5,13 +5,16 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"time"
"github.com/fatih/color"
"github.com/spf13/pflag"
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/sort"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/task/v3/taskrc"
@@ -58,6 +61,7 @@ var (
Watch bool
Verbose bool
Silent bool
DisableFuzzy bool
AssumeYes bool
Dry bool
Summary bool
@@ -69,13 +73,20 @@ var (
Output ast.Output
Color bool
Interval time.Duration
Failfast bool
Global bool
Experiments bool
Download bool
Offline bool
TrustedHosts []string
ClearCache bool
Timeout time.Duration
CacheExpiryDuration time.Duration
RemoteCacheDir string
CACert string
Cert string
CertKey string
Interactive bool
)
func init() {
@@ -119,13 +130,15 @@ func init() {
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(&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.BoolVar(&Insecure, "insecure", getConfig(config, "REMOTE_INSECURE", func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.")
pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.")
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.")
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, "VERBOSE", func() *bool { return config.Verbose }, false), "Enables verbose mode.")
pflag.BoolVarP(&Silent, "silent", "s", getConfig(config, "SILENT", func() *bool { return config.Silent }, false), "Disables echoing.")
pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, "DISABLE_FUZZY", func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
pflag.BoolVarP(&AssumeYes, "yes", "y", getConfig(config, "ASSUME_YES", func() *bool { return nil }, false), "Assume \"yes\" as answer to all prompts.")
pflag.BoolVar(&Interactive, "interactive", getConfig(config, "INTERACTIVE", func() *bool { return config.Interactive }, false), "Prompt for missing required variables.")
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", getConfig(config, "DRY", func() *bool { return nil }, false), "Compiles and prints tasks in the order that they would be run, without executing them.")
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
pflag.BoolVarP(&ExitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
pflag.StringVarP(&Dir, "dir", "d", "", "Sets the directory in which Task will execute and look for a Taskfile.")
@@ -134,9 +147,10 @@ func init() {
pflag.StringVar(&Output.Group.Begin, "output-group-begin", "", "Message template to print before 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.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", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
pflag.BoolVarP(&Color, "color", "c", getConfig(config, "COLOR", func() *bool { return config.Color }, true), "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, "CONCURRENCY", func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Failfast, "failfast", "F", getConfig(config, "FAILFAST", 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.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -151,12 +165,40 @@ func init() {
// Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&Offline, "offline", getConfig(config, "REMOTE_OFFLINE", func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, "REMOTE_TRUSTED_HOSTS", func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, "REMOTE_TIMEOUT", func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, 0), "Expiry duration for cached remote Taskfiles.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, "REMOTE_CACHE_EXPIRY", func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, "REMOTE_CACHE_DIR", func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
pflag.StringVar(&CACert, "cacert", getConfig(config, "REMOTE_CACERT", func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
}
pflag.Parse()
// Auto-detect color based on environment when not explicitly configured
// Priority: CLI flag > TASK_COLOR env > taskrc config > NO_COLOR > FORCE_COLOR/CI > default
colorExplicitlySet := pflag.Lookup("color").Changed || env.GetTaskEnv("COLOR") != "" || (config != nil && config.Color != nil)
if !colorExplicitlySet {
if os.Getenv("NO_COLOR") != "" {
Color = false
color.NoColor = true
} else if os.Getenv("FORCE_COLOR") != "" || isCI() {
Color = true
color.NoColor = false // Force colors even without TTY
}
// Otherwise, let fatih/color auto-detect TTY
} else {
// Explicit config: sync with fatih/color
color.NoColor = !Color
}
}
// isCI returns true if running in a CI environment
func isCI() bool {
ci, _ := strconv.ParseBool(os.Getenv("CI"))
return ci
}
func Validate() error {
@@ -200,6 +242,11 @@ func Validate() error {
return errors.New("task: --nested only applies to --json with --list or --list-all")
}
// Validate certificate flags
if (Cert != "" && CertKey == "") || (Cert == "" && CertKey != "") {
return errors.New("task: --cert and --cert-key must be provided together")
}
return nil
}
@@ -238,12 +285,19 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithInsecure(Insecure),
task.WithDownload(Download),
task.WithOffline(Offline),
task.WithTrustedHosts(TrustedHosts),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithRemoteCacheDir(RemoteCacheDir),
task.WithCACert(CACert),
task.WithCert(Cert),
task.WithCertKey(CertKey),
task.WithWatch(Watch),
task.WithVerbose(Verbose),
task.WithSilent(Silent),
task.WithDisableFuzzy(DisableFuzzy),
task.WithAssumeYes(AssumeYes),
task.WithInteractive(Interactive),
task.WithDry(Dry || Status),
task.WithSummary(Summary),
task.WithParallel(Parallel),
@@ -253,18 +307,49 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithOutputStyle(Output),
task.WithTaskSorter(sorter),
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
// getConfig extracts a config value with priority: env var > taskrc config > fallback
func getConfig[T any](config *taskrcast.TaskRC, envKey string, fieldFunc func() *T, fallback T) T {
if envKey != "" {
if val, ok := getEnvAs[T](envKey); ok {
return val
}
}
field := fieldFunc()
if field != nil {
return *field
if config != nil {
if field := fieldFunc(); field != nil {
return *field
}
}
return fallback
}
// getEnvAs parses a TASK_ prefixed env var as type T
func getEnvAs[T any](envKey string) (T, bool) {
var zero T
switch any(zero).(type) {
case bool:
if val, ok := env.GetTaskEnvBool(envKey); ok {
return any(val).(T), true
}
case int:
if val, ok := env.GetTaskEnvInt(envKey); ok {
return any(val).(T), true
}
case time.Duration:
if val, ok := env.GetTaskEnvDuration(envKey); ok {
return any(val).(T), true
}
case string:
if val, ok := env.GetTaskEnvString(envKey); ok {
return any(val).(T), true
}
case []string:
if val, ok := env.GetTaskEnvStringSlice(envKey); ok {
return any(val).(T), true
}
}
return zero, false
}

View File

@@ -108,10 +108,10 @@ func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, er
}
}
paths, err := SearchNPathRecursively(dir, possibleFilenames, -1)
if err != nil {
return nil, err
}
return append(entrypoints, paths...), nil
// The call to SearchNPathRecursively is ambiguous and may return
// os.ErrPermission if its search ends, however it may have still
// returned valid paths. Caller may choose to ignore that error.
return append(entrypoints, paths...), err
}
// SearchPath will check if a file at the given path exists or not. If it does,
@@ -153,13 +153,16 @@ func SearchPath(path string, possibleFilenames []string) (string, error) {
// also check if the user ID of the directory changes and abort if it does.
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
paths, err := SearchNPathRecursively(path, possibleFilenames, 1)
if err != nil {
return "", err
if len(paths) > 0 {
// Regardless of the error, return the first possible filename.
return paths[0], nil
} else {
if err != nil {
return "", err
} else {
return "", os.ErrNotExist
}
}
if len(paths) == 0 {
return "", os.ErrNotExist
}
return paths[0], nil
}
// SearchNPathRecursively walks up the directory tree starting at the given
@@ -188,10 +191,13 @@ func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]s
return nil, err
}
// Error if we reached the root directory and still haven't found a file
// OR if the user id of the directory changes
if path == parentPath || (parentOwner != owner) {
// If the user id of the directory changes indicate a permission error, otherwise
// the calling code will infer an error condition based on the accumulated
// contents of paths.
if path == parentPath {
return paths, nil
} else if parentOwner != owner {
return paths, os.ErrPermission
}
owner = parentOwner

View File

@@ -20,5 +20,5 @@ func Name(t *ast.Task) (string, error) {
func Hash(t *ast.Task) (string, error) {
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
}

211
internal/input/input.go Normal file
View File

@@ -0,0 +1,211 @@
package input
import (
"fmt"
"io"
"strings"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/go-task/task/v3/errors"
)
var ErrCancelled = errors.New("prompt cancelled")
var (
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true) // green bold
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray
)
// Prompter handles interactive variable prompting
type Prompter struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
// Text prompts the user for a text value
func (p *Prompter) Text(varName string) (string, error) {
m := newTextModel(varName)
prog := tea.NewProgram(m,
tea.WithInput(p.Stdin),
tea.WithOutput(p.Stderr),
)
result, err := prog.Run()
if err != nil {
return "", err
}
model := result.(textModel)
if model.cancelled {
return "", ErrCancelled
}
return model.value, nil
}
// Select prompts the user to select from a list of options
func (p *Prompter) Select(varName string, options []string) (string, error) {
if len(options) == 0 {
return "", errors.New("no options provided")
}
m := newSelectModel(varName, options)
prog := tea.NewProgram(m,
tea.WithInput(p.Stdin),
tea.WithOutput(p.Stderr),
)
result, err := prog.Run()
if err != nil {
return "", err
}
model := result.(selectModel)
if model.cancelled {
return "", ErrCancelled
}
return model.options[model.cursor], nil
}
// Prompt prompts for a variable value, using Select if enum is provided, Text otherwise
func (p *Prompter) Prompt(varName string, enum []string) (string, error) {
if len(enum) > 0 {
return p.Select(varName, enum)
}
return p.Text(varName)
}
// textModel is the Bubble Tea model for text input
type textModel struct {
varName string
textInput textinput.Model
value string
cancelled bool
done bool
}
func newTextModel(varName string) textModel {
ti := textinput.New()
ti.Placeholder = ""
ti.CharLimit = 256
ti.SetWidth(40)
ti.Focus()
return textModel{
varName: varName,
textInput: ti,
}
}
func (m textModel) Init() tea.Cmd {
return tea.Batch(m.textInput.Focus(), textinput.Blink)
}
func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.Keystroke() {
case "ctrl+c", "escape":
m.cancelled = true
m.done = true
return m, tea.Quit
case "enter":
m.value = m.textInput.Value()
m.done = true
return m, tea.Quit
}
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
func (m textModel) View() tea.View {
if m.done {
return tea.NewView("")
}
prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", m.varName))
return tea.NewView(prompt + m.textInput.View() + "\n")
}
// selectModel is the Bubble Tea model for selection
type selectModel struct {
varName string
options []string
cursor int
cancelled bool
done bool
}
func newSelectModel(varName string, options []string) selectModel {
return selectModel{
varName: varName,
options: options,
cursor: 0,
}
}
func (m selectModel) Init() tea.Cmd {
return nil
}
func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.Keystroke() {
case "ctrl+c", "escape":
m.cancelled = true
m.done = true
return m, tea.Quit
case "up", "shift+tab", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "tab", "j":
if m.cursor < len(m.options)-1 {
m.cursor++
}
case "enter":
m.done = true
return m, tea.Quit
}
}
return m, nil
}
func (m selectModel) View() tea.View {
if m.done {
return tea.NewView("")
}
var b strings.Builder
b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", m.varName)))
b.WriteString("\n")
for i, opt := range m.options {
if i == m.cursor {
b.WriteString(cursorStyle.Render(" "))
b.WriteString(selectedStyle.Render(opt))
} else {
b.WriteString(" " + opt)
}
b.WriteString("\n")
}
b.WriteString(dimStyle.Render(" (↑/↓ to move, enter to select, esc to cancel)"))
return tea.NewView(b.String())
}

View File

@@ -3,7 +3,6 @@ package logger
import (
"bufio"
"io"
"os"
"slices"
"strconv"
"strings"
@@ -43,6 +42,12 @@ type (
PrintFunc func(io.Writer, string, ...any)
)
func None() PrintFunc {
c := color.New()
c.DisableColor()
return c.FprintfFunc()
}
func Default() PrintFunc {
return color.New(attrsReset...).FprintfFunc()
}
@@ -96,10 +101,6 @@ func BrightRed() PrintFunc {
}
func envColor(name string, defaultColor color.Attribute) []color.Attribute {
if os.Getenv("FORCE_COLOR") != "" {
color.NoColor = false
}
// Fetch the environment variable
override := env.GetTaskEnv(name)
@@ -149,7 +150,7 @@ func (l *Logger) FOutf(w io.Writer, color Color, s string, args ...any) {
s, args = "%s", []any{s}
}
if !l.Color {
color = Default
color = None
}
print := color()
print(w, s, args...)
@@ -168,7 +169,7 @@ func (l *Logger) Errf(color Color, s string, args ...any) {
s, args = "%s", []any{s}
}
if !l.Color {
color = Default
color = None
}
print := color()
print(l.Stderr, s, args...)

View File

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

View File

@@ -1,6 +1,8 @@
package summary
import (
"fmt"
"os"
"strings"
"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) {
printTaskName(l, t)
printTaskDescribingText(t, l)
printTaskVars(l, t)
printTaskEnv(l, t)
printTaskRequires(l, t)
printTaskDependencies(l, t)
printTaskAliases(l, t)
printTaskCommands(l, t)
@@ -118,3 +123,172 @@ 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)) //nolint:staticcheck
}
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 v.Enum != nil && len(v.Enum.Value) > 0 {
l.Outf(logger.Yellow, " - %s:\n", v.Name)
l.Outf(logger.Yellow, " enum:\n")
for _, enumValue := range v.Enum.Value {
l.Outf(logger.Yellow, " - %s\n", enumValue)
}
} else if v.Enum != nil && v.Enum.Ref != "" {
l.Outf(logger.Yellow, " - %s:\n", v.Name)
l.Outf(logger.Yellow, " enum:\n")
l.Outf(logger.Yellow, " ref: %s\n", v.Enum.Ref)
} else {
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" ||
key == "PATH_LIST_SEPARATOR" ||
key == "FILE_PATH_SEPARATOR" {
return true
}
return envVars[key]
}

View File

@@ -3,13 +3,15 @@ package templater
import (
"maps"
"math/rand/v2"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"mvdan.cc/sh/v3/shell"
"mvdan.cc/sh/v3/syntax"
@@ -21,8 +23,8 @@ var templateFuncs template.FuncMap
func init() {
taskFuncs := template.FuncMap{
"OS": os,
"ARCH": arch,
"OS": goos,
"ARCH": goarch,
"numCPU": runtime.NumCPU,
"catLines": catLines,
"splitLines": splitLines,
@@ -33,7 +35,10 @@ func init() {
"splitArgs": splitArgs,
"IsSH": IsSH, // Deprecated
"joinPath": filepath.Join,
"joinEnv": joinEnv,
"joinUrl": joinUrl,
"relPath": filepath.Rel,
"absPath": filepath.Abs,
"merge": merge,
"spew": spew.Sdump,
"fromYaml": fromYaml,
@@ -56,11 +61,11 @@ func init() {
maps.Copy(templateFuncs, taskFuncs)
}
func os() string {
func goos() string {
return runtime.GOOS
}
func arch() string {
func goarch() string {
return runtime.GOARCH
}
@@ -94,6 +99,14 @@ func IsSH() bool {
return true
}
func joinEnv(elem ...string) string {
return strings.Join(elem, string(os.PathListSeparator))
}
func joinUrl(elem ...string) string {
return path.Join(elem...)
}
func merge(base map[string]any, v ...map[string]any) map[string]any {
cap := len(v)
for _, m := range v {

View File

@@ -68,6 +68,13 @@ func ReplaceWithExtra[T any](v T, cache *Cache, extra map[string]any) T {
return v
}
// Optimization: skip if string is not a template
if s, ok := any(v).(string); ok {
if !strings.Contains(s, "{{") {
return v
}
}
// Initialize the cache map if it's not already initialized
if cache.cacheMap == nil {
cache.cacheMap = cache.Vars.ToCacheMap()
@@ -82,6 +89,10 @@ func ReplaceWithExtra[T any](v T, cache *Cache, extra map[string]any) T {
// Traverse the value and parse any template variables
copy, err := deepcopy.TraverseStringsFunc(v, func(v string) (string, error) {
// Optimization: skip if string is not a template
if !strings.Contains(v, "{{") {
return v, nil
}
tpl, err := template.New("").Funcs(templateFuncs).Parse(v)
if err != nil {
return v, err

View File

@@ -7,5 +7,5 @@ import (
)
func IsTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) //nolint:gosec
}

View File

@@ -1 +1 @@
3.45.1
3.51.1

View File

@@ -4,35 +4,180 @@ import (
"slices"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/input"
"github.com/go-task/task/v3/internal/term"
"github.com/go-task/task/v3/taskfile/ast"
)
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
func (e *Executor) canPrompt() bool {
return e.Interactive && (e.AssumeTerm || term.IsTerminal())
}
func (e *Executor) newPrompter() *input.Prompter {
return &input.Prompter{
Stdin: e.Stdin,
Stdout: e.Stdout,
Stderr: e.Stderr,
}
}
// promptDepsVars traverses the dependency tree, collects all missing required
// variables, and prompts for them upfront. This is used for deps which execute
// in parallel, so all prompts must happen before execution to avoid interleaving.
// Prompted values are stored in e.promptedVars for injection into task calls.
func (e *Executor) promptDepsVars(calls []*Call) error {
if !e.canPrompt() {
return nil
}
var missingVars []errors.MissingVar
for _, requiredVar := range t.Requires.Vars {
_, ok := t.Vars.Get(requiredVar.Name)
if !ok {
missingVars = append(missingVars, errors.MissingVar{
Name: requiredVar.Name,
AllowedValues: requiredVar.Enum,
})
// Collect all missing vars from the dependency tree
visited := make(map[string]bool)
varsMap := make(map[string]*ast.VarsWithValidation)
var collect func(call *Call) error
collect = func(call *Call) error {
compiledTask, err := e.FastCompiledTask(call)
if err != nil {
return err
}
for _, v := range getMissingRequiredVars(compiledTask) {
if _, exists := varsMap[v.Name]; !exists {
varsMap[v.Name] = v
}
}
// Check visited AFTER collecting vars to handle duplicate task calls with different vars
if visited[call.Task] {
return nil
}
visited[call.Task] = true
for _, dep := range compiledTask.Deps {
depCall := &Call{
Task: dep.Task,
Vars: dep.Vars,
Silent: dep.Silent,
}
if err := collect(depCall); err != nil {
return err
}
}
return nil
}
for _, call := range calls {
if err := collect(call); err != nil {
return err
}
}
if len(missingVars) > 0 {
return &errors.TaskMissingRequiredVarsError{
TaskName: t.Name(),
MissingVars: missingVars,
if len(varsMap) == 0 {
return nil
}
prompter := e.newPrompter()
e.promptedVars = ast.NewVars()
for _, v := range varsMap {
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
if err != nil {
if errors.Is(err, input.ErrCancelled) {
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
}
return err
}
e.promptedVars.Set(v.Name, ast.Var{Value: value})
}
return nil
}
// promptTaskVars prompts for any missing required vars from a single task.
// Used for sequential task calls (cmds) where we can prompt just-in-time.
// Returns true if any vars were prompted (caller should recompile the task).
func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
if !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 {
return false, nil
}
// Find missing vars, excluding already prompted ones
var missing []*ast.VarsWithValidation
for _, v := range getMissingRequiredVars(t) {
if e.promptedVars != nil {
if _, ok := e.promptedVars.Get(v.Name); ok {
continue
}
}
missing = append(missing, v)
}
if len(missing) == 0 {
return false, nil
}
prompter := e.newPrompter()
for _, v := range missing {
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
if err != nil {
if errors.Is(err, input.ErrCancelled) {
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
}
return false, err
}
// Add to call.Vars for recompilation
if call.Vars == nil {
call.Vars = ast.NewVars()
}
call.Vars.Set(v.Name, ast.Var{Value: value})
// Cache for reuse by other tasks
if e.promptedVars == nil {
e.promptedVars = ast.NewVars()
}
e.promptedVars.Set(v.Name, ast.Var{Value: value})
}
return true, nil
}
// getMissingRequiredVars returns required vars that are not set in the task's vars.
func getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation {
if t.Requires == nil {
return nil
}
var missing []*ast.VarsWithValidation
for _, v := range t.Requires.Vars {
if _, ok := t.Vars.Get(v.Name); !ok {
missing = append(missing, v)
}
}
return missing
}
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
missing := getMissingRequiredVars(t)
if len(missing) == 0 {
return nil
}
missingVars := make([]errors.MissingVar, len(missing))
for i, v := range missing {
missingVars[i] = errors.MissingVar{
Name: v.Name,
AllowedValues: getEnumValues(v.Enum),
}
}
return &errors.TaskMissingRequiredVarsError{
TaskName: t.Name(),
MissingVars: missingVars,
}
}
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
if t.Requires == nil || len(t.Requires.Vars) == 0 {
return nil
@@ -42,15 +187,15 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
for _, requiredVar := range t.Requires.Vars {
varValue, _ := t.Vars.Get(requiredVar.Name)
enumValues := getEnumValues(requiredVar.Enum)
value, isString := varValue.Value.(string)
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
if isString && len(enumValues) > 0 && !slices.Contains(enumValues, value) {
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
Value: value,
Enum: requiredVar.Enum,
Enum: enumValues,
Name: requiredVar.Name,
})
}
}
if len(notAllowedValuesVars) > 0 {
@@ -62,3 +207,10 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
return nil
}
func getEnumValues(e *ast.Enum) []string {
if e == nil {
return nil
}
return e.Value
}

View File

@@ -35,7 +35,6 @@ func (e *Executor) Setup() error {
if err := e.readTaskfile(node); err != nil {
return err
}
e.setupFuzzyModel()
e.setupStdFiles()
if err := e.setupOutput(); err != nil {
return err
@@ -55,11 +54,21 @@ func (e *Executor) Setup() 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,
taskfile.WithCACert(e.CACert),
taskfile.WithCert(e.Cert),
taskfile.WithCertKey(e.CertKey),
)
var taskNotFoundError errors.TaskfileNotFoundError
if errors.As(err, &taskNotFoundError) {
taskNotFoundError.AskInit = true
return nil, taskNotFoundError
}
if err != nil {
return nil, err
}
e.Dir = node.Dir()
e.Entrypoint = node.Location()
return node, err
}
@@ -76,8 +85,12 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
taskfile.WithTrustedHosts(e.TrustedHosts),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithReaderCACert(e.CACert),
taskfile.WithReaderCert(e.Cert),
taskfile.WithReaderCertKey(e.CertKey),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
@@ -145,16 +158,16 @@ func (e *Executor) setupTempDir() error {
}
}
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir != "" {
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
// RemoteCacheDir from taskrc/env can override the remote cache directory
if e.RemoteCacheDir != "" {
if filepath.IsAbs(e.RemoteCacheDir) || strings.HasPrefix(e.RemoteCacheDir, "~") {
remoteCacheDir, err := execext.ExpandLiteral(e.RemoteCacheDir)
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
e.TempDir.Remote = remoteCacheDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, e.RemoteCacheDir)
}
}

View File

@@ -15,7 +15,7 @@ const maxInterruptSignals = 3
// time to do cleanup work.
func (e *Executor) InterceptInterruptSignals() {
ch := make(chan os.Signal, maxInterruptSignals)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
go func() {
for i := range maxInterruptSignals {

196
task.go
View File

@@ -6,6 +6,7 @@ import (
"os"
"runtime"
"slices"
"strings"
"sync/atomic"
"golang.org/x/sync/errgroup"
@@ -73,14 +74,21 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
return nil
}
// Prompt for all required vars from deps upfront (parallel execution)
if err := e.promptDepsVars(calls); err != nil {
return err
}
regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
if err != nil {
return err
}
g, ctx := errgroup.WithContext(ctx)
g := &errgroup.Group{}
if e.Failfast {
g, ctx = errgroup.WithContext(ctx)
}
for _, c := range regularCalls {
c := c
if e.Parallel {
g.Go(func() error { return e.RunTask(ctx, c) })
} else {
@@ -113,11 +121,24 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
regularCalls = append(regularCalls, c)
}
}
return
return regularCalls, watchCalls, err
}
// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call *Call) error {
// Inject prompted vars into call if available
if e.promptedVars != nil {
if call.Vars == nil {
call.Vars = ast.NewVars()
}
for name, v := range e.promptedVars.All() {
// Only inject if not already set in call
if _, ok := call.Vars.Get(name); !ok {
call.Vars.Set(name, v)
}
}
}
t, err := e.FastCompiledTask(call)
if err != nil {
return err
@@ -127,8 +148,12 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return nil
}
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
// Check required vars early (before template compilation) if we can't prompt.
// This gives a clear "missing required variables" error instead of a template error.
if !e.canPrompt() {
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
}
t, err = e.CompiledTask(call)
@@ -136,6 +161,35 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return err
}
// Check if condition after CompiledTask so dynamic variables are resolved
if strings.TrimSpace(t.If) != "" {
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: t.If,
Dir: t.Dir,
Env: env.Get(t),
}); err != nil {
e.Logger.VerboseOutf(logger.Yellow, "task: if condition not met - skipped: %q\n", call.Task)
return nil
}
}
// Prompt for missing required vars after if check (avoid prompting if task won't run)
prompted, err := e.promptTaskVars(t, call)
if err != nil {
return err
}
if prompted {
// Recompile with the new vars
t, err = e.FastCompiledTask(call)
if err != nil {
return err
}
}
if err := e.areTaskRequiredVarsSet(t); err != nil {
return err
}
if err := e.areTaskRequiredVarsAllowedValuesSet(t); err != nil {
return err
}
@@ -150,7 +204,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
release := e.acquireConcurrencyLimit()
defer release()
return e.startExecution(ctx, t, func(ctx context.Context) error {
if err = e.startExecution(ctx, t, func(ctx context.Context) error {
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
if err := e.runDeps(ctx, t); err != nil {
return err
@@ -183,8 +237,12 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
}
if upToDate && preCondMet {
if e.Verbose || (!call.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", t.Name())
if e.Verbose || (!call.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
name := t.Name()
if e.OutputStyle.Name == "prefixed" {
name = t.Prefix
}
e.Logger.Errf(logger.Magenta, "task: Task %q is up to date\n", name)
}
return nil
}
@@ -210,7 +268,7 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
for i := range t.Cmds {
if t.Cmds[i].Defer {
defer e.runDeferred(t, call, i, &deferredExitCode)
defer e.runDeferred(t, call, i, t.Vars, &deferredExitCode)
continue
}
@@ -228,16 +286,16 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
deferredExitCode = uint8(exitCode)
}
if call.Indirect {
return err
}
return &errors.TaskRunError{TaskName: t.Task, Err: err}
return err
}
}
e.Logger.VerboseErrf(logger.Magenta, "task: %q finished\n", call.Task)
return nil
})
}); err != nil {
return &errors.TaskRunError{TaskName: t.Name(), Err: err}
}
return nil
}
func (e *Executor) mkdir(t *ast.Task) error {
@@ -258,13 +316,15 @@ func (e *Executor) mkdir(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()
defer reacquire()
for _, d := range t.Deps {
d := d
g.Go(func() error {
err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true})
if err != nil {
@@ -277,17 +337,11 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
return g.Wait()
}
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode *uint8) {
func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, deferredExitCode *uint8) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
origTask, err := e.GetTask(call)
if err != nil {
return
}
cmd := t.Cmds[i]
vars, _ := e.Compiler.GetVariables(origTask, call)
cache := &templater.Cache{Vars: vars}
extra := map[string]any{}
@@ -297,6 +351,7 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.If = templater.ReplaceWithExtra(cmd.If, cache, extra)
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
if err := e.runCommand(ctx, t, call, i); err != nil {
@@ -307,23 +362,37 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, deferredExitCode
func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i int) error {
cmd := t.Cmds[i]
// Check if condition for any command type
if strings.TrimSpace(cmd.If) != "" {
if err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.If,
Dir: t.Dir,
Env: env.Get(t),
}); err != nil {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] if condition not met - skipped\n", t.Name())
return nil
}
}
switch {
case cmd.Task != "":
reacquire := e.releaseConcurrencyLimit()
defer reacquire()
err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true})
if err != nil {
return err
var exitCode interp.ExitStatus
if errors.As(err, &exitCode) && cmd.IgnoreError {
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] task error ignored: %v\n", t.Name(), err)
return nil
}
return nil
return err
case cmd.Cmd != "":
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
e.Logger.VerboseOutf(logger.Yellow, "task: [%s] %s not for current platform - ignored\n", t.Name(), cmd.Cmd)
return nil
}
if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
if e.Verbose || (!call.Silent && !cmd.Silent && !t.IsSilent() && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
}
@@ -372,7 +441,7 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
return err
}
if h == "" {
if h == "" || t.Watch {
return execute(ctx)
}
@@ -400,19 +469,40 @@ func (e *Executor) startExecution(ctx context.Context, t *ast.Task, execute func
}
// FindMatchingTasks returns a list of tasks that match the given call. A task
// matches a call if its name is equal to the call's task name or if it matches
// matches a call if its name is equal to the call's task name, or one of aliases, or if it matches
// a wildcard pattern. The function returns a list of MatchingTask structs, each
// containing a task and a list of wildcards that were matched.
func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
// If multiple tasks match due to aliases, a TaskNameConflictError is returned.
func (e *Executor) FindMatchingTasks(call *Call) ([]*MatchingTask, error) {
if call == nil {
return nil
return nil, nil
}
var matchingTasks []*MatchingTask
// If there is a direct match, return it
if task, ok := e.Taskfile.Tasks.Get(call.Task); ok {
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
return matchingTasks
return matchingTasks, nil
}
var aliasedTasks []string
for task := range e.Taskfile.Tasks.Values(nil) {
if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task)
matchingTasks = append(matchingTasks, &MatchingTask{Task: task, Wildcards: nil})
}
}
if len(aliasedTasks) == 1 {
return matchingTasks, nil
}
// If we found multiple tasks
if len(aliasedTasks) > 1 {
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: aliasedTasks,
}
}
// Attempt a wildcard match
for _, value := range e.Taskfile.Tasks.All(nil) {
if match, wildcards := value.WildcardMatch(call.Task); match {
@@ -422,7 +512,7 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
})
}
}
return matchingTasks
return matchingTasks, nil
}
// GetTask will return the task with the name matching the given call from the taskfile.
@@ -430,7 +520,11 @@ func (e *Executor) FindMatchingTasks(call *Call) []*MatchingTask {
// If multiple tasks contain the same alias or no matches are found an error is returned.
func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
// Search for a matching task
matchingTasks := e.FindMatchingTasks(call)
matchingTasks, err := e.FindMatchingTasks(call)
if err != nil {
return nil, err
}
if len(matchingTasks) > 0 {
if call.Vars == nil {
call.Vars = ast.NewVars()
@@ -439,34 +533,18 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
return matchingTasks[0].Task, nil
}
// If didn't find one, search for a task with a matching alias
var matchingTask *ast.Task
var aliasedTasks []string
for task := range e.Taskfile.Tasks.Values(nil) {
if slices.Contains(task.Aliases, call.Task) {
aliasedTasks = append(aliasedTasks, task.Task)
matchingTask = task
}
}
// If we found multiple tasks
if len(aliasedTasks) > 1 {
return nil, &errors.TaskNameConflictError{
Call: call.Task,
TaskNames: aliasedTasks,
}
}
// If we found no tasks
if len(aliasedTasks) == 0 {
didYouMean := ""
didYouMean := ""
if !e.DisableFuzzy {
e.fuzzyModelOnce.Do(e.setupFuzzyModel)
if e.fuzzyModel != nil {
didYouMean = e.fuzzyModel.SpellCheck(call.Task)
}
return nil, &errors.TaskNotFoundError{
TaskName: call.Task,
DidYouMean: didYouMean,
}
}
return matchingTask, nil
return nil, &errors.TaskNotFoundError{
TaskName: call.Task,
DidYouMean: didYouMean,
}
}
type FilterFunc func(task *ast.Task) bool
@@ -498,7 +576,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
// Compile the list of tasks
for i := range tasks {
g.Go(func() error {
compiledTask, err := e.FastCompiledTask(&Call{Task: tasks[i].Task})
compiledTask, err := e.CompiledTaskForTaskList(&Call{Task: tasks[i].Task})
if err != nil {
return err
}

View File

@@ -9,6 +9,7 @@ import (
rand "math/rand/v2"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
@@ -87,13 +88,14 @@ func (tt *TaskTest) writeFixture(
if tt.fixtureTemplatingEnabled {
fixtureTemplateData := map[string]any{
"TEST_NAME": t.Name(),
"TEST_DIR": wd,
"TEST_DIR": filepath.ToSlash(wd),
}
// If the test has additional template data, copy it into the map
if tt.fixtureTemplateData != nil {
maps.Copy(fixtureTemplateData, tt.fixtureTemplateData)
}
g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, b)
// Normalize output before comparison (CRLF→LF, backslash→forward slash)
g.AssertWithTemplate(t, goldenFileName, fixtureTemplateData, normalizeOutput(b))
} else {
g.Assert(t, goldenFileName, b)
}
@@ -307,6 +309,73 @@ func PPSortedLines(t *testing.T, b []byte) []byte {
return []byte(strings.Join(lines, "\n") + "\n")
}
// normalizeOutput normalizes cross-platform differences for byte slice comparison:
// - Converts CRLF and CR to LF (line endings)
// - Converts backslashes to forward slashes (Windows paths)
// - Handles escaped backslashes in JSON (\\) by converting to single forward slash
func normalizeOutput(b []byte) []byte {
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
b = bytes.ReplaceAll(b, []byte("\r"), []byte("\n"))
// First replace escaped backslashes (common in JSON), then single backslashes
b = bytes.ReplaceAll(b, []byte("\\\\"), []byte("/"))
b = bytes.ReplaceAll(b, []byte("\\"), []byte("/"))
return b
}
// normalizePathSeparators converts backslashes to forward slashes for cross-platform path comparison.
func normalizePathSeparators(s string) string {
return strings.ReplaceAll(s, "\\", "/")
}
// NormalizedEqual compares two byte slices after normalizing output.
// This is used as a custom goldie.EqualFn for cross-platform golden file tests.
func NormalizedEqual(actual, expected []byte) bool {
return bytes.Equal(normalizeOutput(actual), normalizeOutput(expected))
}
func TestNormalizeOutput(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input []byte
expected []byte
}{
{"CRLF to LF", []byte("line1\r\nline2\r\n"), []byte("line1\nline2\n")},
{"CR to LF", []byte("line1\rline2\r"), []byte("line1\nline2\n")},
{"Windows path", []byte(`D:\a\task\task`), []byte(`D:/a/task/task`)},
{"JSON escaped backslash", []byte(`{"path":"D:\\a\\task"}`), []byte(`{"path":"D:/a/task"}`)},
{"Mixed", []byte("D:\\a\\task\r\n"), []byte("D:/a/task\n")},
{"Unix path unchanged", []byte("/home/user/task\n"), []byte("/home/user/task\n")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := normalizeOutput(tt.input)
assert.Equal(t, tt.expected, got)
})
}
}
func TestNormalizePathSeparators(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{"Windows path", `D:\a\task\task`, `D:/a/task/task`},
{"Unix path unchanged", `/home/user/task`, `/home/user/task`},
{"Mixed separators", `C:\Users/name\file`, `C:/Users/name/file`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := normalizePathSeparators(tt.input)
assert.Equal(t, tt.expected, got)
})
}
}
// SyncBuffer is a threadsafe buffer for testing.
// Some times replace stdout/stderr with a buffer to capture output.
// stdout and stderr are threadsafe, but a regular bytes.Buffer is not.
@@ -486,6 +555,104 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
}
}
// TestStatusTimestamp is a regression test for https://github.com/go-task/task/issues/1230.
// When using method: timestamp, deleting a generated file should cause the task to re-run,
// not be skipped because the timestamp file is still present.
func TestStatusTimestamp(t *testing.T) { // nolint:paralleltest // cannot run in parallel
const dir = "testdata/timestamp"
generatedFile := filepathext.SmartJoin(dir, "generated.txt")
tempDir := task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}
// Clean up any state from previous runs.
_ = os.Remove(generatedFile)
_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithTempDir(tempDir),
)
require.NoError(t, e.Setup())
// First run: task should execute and create generated.txt.
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
_, err := os.Stat(generatedFile)
require.NoError(t, err, "generated.txt should exist after first run")
buff.Reset()
// Second run: task should be up to date.
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
buff.Reset()
// Delete the generated file (simulate a clean), but leave the timestamp file.
require.NoError(t, os.Remove(generatedFile))
_, err = os.Stat(generatedFile)
require.Error(t, err, "generated.txt should be gone")
// Third run: task MUST re-run because generated.txt is missing.
// This is the regression: previously the task was incorrectly skipped.
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing")
_, err = os.Stat(generatedFile)
require.NoError(t, err, "generated.txt should be recreated after third run")
}
// TestStatusChecksumMissingGenerated is a regression test for https://github.com/go-task/task/issues/1230.
// When using method: checksum, deleting a generated file should cause the task to re-run,
// not be skipped because the checksum file still matches.
func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest // cannot run in parallel
const dir = "testdata/checksum"
generatedFile := filepathext.SmartJoin(dir, "generated.txt")
tempDir := task.TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}
// Clean up any state from previous runs.
_ = os.Remove(generatedFile)
_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithTempDir(tempDir),
)
require.NoError(t, e.Setup())
// First run: task should execute and create generated.txt.
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
_, err := os.Stat(generatedFile)
require.NoError(t, err, "generated.txt should exist after first run")
buff.Reset()
// Second run: task should be up to date.
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
buff.Reset()
// Delete the generated file (simulate a clean), but leave the checksum file.
require.NoError(t, os.Remove(generatedFile))
_, err = os.Stat(generatedFile)
require.Error(t, err, "generated.txt should be gone")
// Third run: task MUST re-run because generated.txt is missing.
// This is the regression: previously the task was incorrectly skipped.
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing")
_, err = os.Stat(generatedFile)
require.NoError(t, err, "generated.txt should be recreated after third run")
}
func TestStatusVariables(t *testing.T) {
t.Parallel()
@@ -569,7 +736,9 @@ func TestCyclicDep(t *testing.T) {
task.WithStderr(io.Discard),
)
require.NoError(t, e.Setup())
assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(t.Context(), &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) {
@@ -784,6 +953,11 @@ func TestIncludesRemote(t *testing.T) {
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 {
name string
executor *task.Executor
@@ -823,6 +997,23 @@ func TestIncludesRemote(t *testing.T) {
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 {
@@ -831,7 +1022,7 @@ func TestIncludesRemote(t *testing.T) {
for k, taskCall := range taskCalls {
t.Run(taskCall.Task, func(t *testing.T) {
expectedContent := fmt.Sprint(rand.Int64())
expectedContent := fmt.Sprint(rand.Int64()) //nolint:gosec
t.Setenv("CONTENT", expectedContent)
outputFile := fmt.Sprintf("%d.%d.txt", i, k)
@@ -1052,8 +1243,8 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_implicit_false"
wd, _ := os.Getwd()
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
expected := fmt.Sprintf(message, wd, dir)
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
expected := fmt.Sprintf(message, filepath.ToSlash(wd), dir)
e := task.NewExecutor(
task.WithDir(dir),
@@ -1072,8 +1263,8 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_explicit_false"
wd, _ := os.Getwd()
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
expected := fmt.Sprintf(message, wd, dir)
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
expected := fmt.Sprintf(message, filepath.ToSlash(wd), dir)
e := task.NewExecutor(
task.WithDir(dir),
@@ -1121,11 +1312,11 @@ func TestIncludesRelativePath(t *testing.T) {
require.NoError(t, e.Setup())
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, filepath.ToSlash(buff.String()), "testdata/includes_rel_path/common")
buff.Reset()
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, filepath.ToSlash(buff.String()), "testdata/includes_rel_path/common")
}
func TestIncludesInternal(t *testing.T) {
@@ -1303,7 +1494,7 @@ func TestIncludedTaskfileVarMerging(t *testing.T) {
err := e.Run(t.Context(), &task.Call{Task: test.task})
require.NoError(t, err)
assert.Contains(t, buff.String(), test.expectedOutput)
assert.Contains(t, filepath.ToSlash(buff.String()), test.expectedOutput)
})
}
}
@@ -1450,7 +1641,9 @@ func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"}))
// got should be the "dir" part of "testdata/dir"
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
normalized := normalizePathSeparators(out.String())
got := strings.TrimSuffix(filepath.Base(normalized), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
}
@@ -1469,7 +1662,9 @@ func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "whereami"}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
normalized := normalizePathSeparators(out.String())
got := strings.TrimSuffix(filepath.Base(normalized), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
}
@@ -1495,7 +1690,9 @@ func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
normalized := normalizePathSeparators(out.String())
got := strings.TrimSuffix(filepath.Base(normalized), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
// Clean-up after ourselves only if no error.
@@ -1524,7 +1721,11 @@ func TestDynamicVariablesRunOnTheNewCreatedDir(t *testing.T) {
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
// Take only the first line as Windows may output additional debug info
normalized := normalizePathSeparators(out.String())
firstLine := strings.Split(normalized, "\n")[0]
got := filepath.Base(firstLine)
assert.Equal(t, expected, got, "Mismatch in the working directory")
// Clean-up after ourselves only if no error.
@@ -1849,6 +2050,29 @@ func TestRunOnceSharedDeps(t *testing.T) {
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) {
t.Parallel()
@@ -2220,7 +2444,8 @@ func TestUserWorkingDirectory(t *testing.T) {
require.NoError(t, err)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "default"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
// Use filepath.ToSlash because USER_WORKING_DIR uses forward slashes on all platforms
assert.Equal(t, fmt.Sprintf("%s\n", filepath.ToSlash(wd)), buff.String())
}
func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
@@ -2229,7 +2454,7 @@ func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
wd, err := os.Getwd()
require.NoError(t, err)
wd = filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir")
wd = filepath.ToSlash(filepathext.SmartJoin(wd, "testdata/user_working_dir_with_includes/somedir"))
var buff bytes.Buffer
e := task.NewExecutor(
@@ -2242,7 +2467,8 @@ func TestUserWorkingDirectoryWithIncluded(t *testing.T) {
require.NoError(t, err)
require.NoError(t, e.Setup())
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "included:echo"}))
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
assert.Equal(t, fmt.Sprintf("%s\n", wd), normalizePathSeparators(buff.String()))
}
func TestPlatforms(t *testing.T) {
@@ -2375,6 +2601,27 @@ func TestSplitArgs(t *testing.T) {
assert.Equal(t, "3\n", buff.String())
}
func TestAbsPath(t *testing.T) {
t.Parallel()
var buff bytes.Buffer
e := task.NewExecutor(
task.WithDir("testdata/abs_path"),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithSilent(true),
)
require.NoError(t, e.Setup())
err := e.Run(t.Context(), &task.Call{Task: "default"})
require.NoError(t, err)
cwd, err := os.Getwd()
require.NoError(t, err)
expected := filepath.Join(cwd, "bar") + "\n"
assert.Equal(t, expected, buff.String())
}
func TestSingleCmdDep(t *testing.T) {
t.Parallel()
@@ -2564,6 +2811,11 @@ func TestWildcard(t *testing.T) {
call: "start-foo",
expectedOutput: "Starting foo\n",
},
{
name: "alias",
call: "s-foo",
expectedOutput: "Starting foo\n",
},
{
name: "matches exactly",
call: "matches-exactly-*",

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -12,6 +12,7 @@ type Cmd struct {
Cmd string
Task string
For *For
If string
Silent bool
Set []string
Shopt []string
@@ -29,6 +30,7 @@ func (c *Cmd) DeepCopy() *Cmd {
Cmd: c.Cmd,
Task: c.Task,
For: c.For.DeepCopy(),
If: c.If,
Silent: c.Silent,
Set: deepcopy.Slice(c.Set),
Shopt: deepcopy.Slice(c.Shopt),
@@ -55,6 +57,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
Cmd string
Task string
For *For
If string
Silent bool
Set []string
Shopt []string
@@ -92,7 +95,9 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
c.Task = cmdStruct.Task
c.Vars = cmdStruct.Vars
c.For = cmdStruct.For
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent
c.IgnoreError = cmdStruct.IgnoreError
return nil
}
@@ -100,6 +105,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
if cmdStruct.Cmd != "" {
c.Cmd = cmdStruct.Cmd
c.For = cmdStruct.For
c.If = cmdStruct.If
c.Silent = cmdStruct.Silent
c.Set = cmdStruct.Set
c.Shopt = cmdStruct.Shopt

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
package ast
import (
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -22,9 +22,56 @@ func (r *Requires) DeepCopy() *Requires {
}
}
// Enum represents an enum constraint for a required variable.
// It can either be a static list of values or a reference to another variable.
type Enum struct {
Ref string
Value []string
}
func (e *Enum) DeepCopy() *Enum {
if e == nil {
return nil
}
return &Enum{
Ref: e.Ref,
Value: deepcopy.Slice(e.Value),
}
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (e *Enum) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.SequenceNode:
// Static list of values: enum: ["a", "b"]
var values []string
if err := node.Decode(&values); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
e.Value = values
return nil
case yaml.MappingNode:
// Reference to another variable: enum: { ref: .VAR }
var refStruct struct {
Ref string
}
if err := node.Decode(&refStruct); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
if refStruct.Ref == "" {
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("enum")
}
e.Ref = refStruct.Ref
return nil
}
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("enum")
}
type VarsWithValidation struct {
Name string
Enum []string
Enum *Enum
}
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
@@ -33,7 +80,7 @@ func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
}
return &VarsWithValidation{
Name: v.Name,
Enum: v.Enum,
Enum: v.Enum.DeepCopy(),
}
}
@@ -53,7 +100,7 @@ func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode:
var vv struct {
Name string
Enum []string
Enum *Enum
}
if err := node.Decode(&vv); err != nil {
return errors.NewTaskfileDecodeError(err, node)

View File

@@ -5,7 +5,7 @@ import (
"regexp"
"strings"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -13,7 +13,7 @@ import (
// Task represents a task
type Task struct {
Task string
Task string `hash:"ignore"`
Cmds []*Cmd
Deps []*Dep
Label string
@@ -32,22 +32,24 @@ type Task struct {
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Silent *bool
Interactive bool
Internal bool
Method string
Prefix string
Prefix string `hash:"ignore"`
IgnoreError bool
Run string
Platforms []*Platform
If string
Watch bool
Location *Location
Failfast bool
// Populated during merging
Namespace string
Namespace string `hash:"ignore"`
IncludeVars *Vars
IncludedTaskfileVars *Vars
FullName string
FullName string `hash:"ignore"`
}
func (t *Task) Name() string {
@@ -67,28 +69,37 @@ func (t *Task) LocalName() string {
return name
}
// IsSilent returns true if the task has silent mode explicitly enabled.
// Returns false if Silent is nil (not set) or explicitly set to false.
func (t *Task) IsSilent() bool {
return t.Silent != nil && *t.Silent
}
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
func (t *Task) WildcardMatch(name string) (bool, []string) {
// Convert the name into a regex string
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(t.Task, "*", "(.*)"))
regex := regexp.MustCompile(regexStr)
wildcards := regex.FindStringSubmatch(name)
wildcardCount := strings.Count(t.Task, "*")
names := append([]string{t.Task}, t.Aliases...)
// If there are no wildcards, return false
if len(wildcards) == 0 {
return false, nil
for _, taskName := range names {
regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)"))
regex := regexp.MustCompile(regexStr)
wildcards := regex.FindStringSubmatch(name)
if len(wildcards) == 0 {
continue
}
// Remove the first match, which is the full string
wildcards = wildcards[1:]
wildcardCount := strings.Count(taskName, "*")
if len(wildcards) != wildcardCount {
continue
}
return true, wildcards
}
// Remove the first match, which is the full string
wildcards = wildcards[1:]
// If there are more/less wildcards than matches, return false
if len(wildcards) != wildcardCount {
return false, wildcards
}
return true, wildcards
return false, nil
}
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
@@ -133,7 +144,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Vars *Vars
Env *Vars
Dotenv []string
Silent bool
Silent *bool `yaml:"silent,omitempty"`
Interactive bool
Internal bool
Method string
@@ -141,8 +152,10 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
IgnoreError bool `yaml:"ignore_error"`
Run string
Platforms []*Platform
If string
Requires *Requires
Watch bool
Failfast bool
}
if err := node.Decode(&task); err != nil {
return errors.NewTaskfileDecodeError(err, node)
@@ -171,7 +184,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Vars = task.Vars
t.Env = task.Env
t.Dotenv = task.Dotenv
t.Silent = task.Silent
t.Silent = deepcopy.Scalar(task.Silent)
t.Interactive = task.Interactive
t.Internal = task.Internal
t.Method = task.Method
@@ -179,8 +192,10 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.IgnoreError = task.IgnoreError
t.Run = task.Run
t.Platforms = task.Platforms
t.If = task.If
t.Requires = task.Requires
t.Watch = task.Watch
t.Failfast = task.Failfast
return nil
}
@@ -212,7 +227,7 @@ func (t *Task) DeepCopy() *Task {
Vars: t.Vars.DeepCopy(),
Env: t.Env.DeepCopy(),
Dotenv: deepcopy.Slice(t.Dotenv),
Silent: t.Silent,
Silent: deepcopy.Scalar(t.Silent),
Interactive: t.Interactive,
Internal: t.Internal,
Method: t.Method,
@@ -222,10 +237,13 @@ func (t *Task) DeepCopy() *Task {
IncludeVars: t.IncludeVars.DeepCopy(),
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
Platforms: deepcopy.Slice(t.Platforms),
If: t.If,
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
FullName: t.FullName,
Watch: t.Watch,
Failfast: t.Failfast,
}
return c
}

View File

@@ -5,7 +5,7 @@ import (
"time"
"github.com/Masterminds/semver/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
)
@@ -59,6 +59,14 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Tasks == nil {
t1.Tasks = NewTasks()
}
if t2.Silent {
for _, t := range t2.Tasks.All(nil) {
if t.Silent == nil {
v := true
t.Silent = &v
}
}
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)

View File

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

View File

@@ -8,7 +8,7 @@ import (
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
"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 {
if strings.HasPrefix(taskName, NamespaceSeparator) {
return strings.TrimPrefix(taskName, NamespaceSeparator)
if after, ok := strings.CutPrefix(taskName, NamespaceSeparator); ok {
return after
}
return fmt.Sprintf("%s%s%s", namespace, NamespaceSeparator, taskName)
}

View File

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

View File

@@ -5,7 +5,7 @@ import (
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -113,12 +113,12 @@ func (vars *Vars) ToCacheMap() (m map[string]any) {
m[k] = v.Value
}
}
return
return m
}
// Merge loops over other and merges it values with the variables in vars. If
// the include parameter is not nil and its it is an advanced import, the
// directory is set set to the value of the include parameter.
// directory is set to the value of the include parameter.
func (vars *Vars) Merge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil {
return
@@ -133,6 +133,35 @@ func (vars *Vars) Merge(other *Vars, include *Include) {
}
}
// ReverseMerge merges other variables with the existing variables in vars, but
// keeps the other variables first in order. If the include parameter is not
// nil and it is an advanced import, the directory is set to the value of the
// include parameter.
func (vars *Vars) ReverseMerge(other *Vars, include *Include) {
if vars == nil || vars.om == nil || other == nil || other.om == nil {
return
}
newOM := orderedmap.NewOrderedMap[string, Var]()
other.mutex.RLock()
for pair := other.om.Front(); pair != nil; pair = pair.Next() {
val := pair.Value
if include != nil && include.AdvancedImport {
val.Dir = include.Dir
}
newOM.Set(pair.Key, val)
}
other.mutex.RUnlock()
vars.mutex.Lock()
for pair := vars.om.Front(); pair != nil; pair = pair.Next() {
newOM.Set(pair.Key, pair.Value)
}
vars.om = newOM
vars.mutex.Unlock()
}
func (vs *Vars) DeepCopy() *Vars {
if vs == nil {
return nil

View File

@@ -34,13 +34,14 @@ func NewRootNode(
dir string,
insecure bool,
timeout time.Duration,
opts ...NodeOption,
) (Node, error) {
dir = fsext.DefaultDir(entrypoint, dir)
// If the entrypoint is "-", we read from stdin
if entrypoint == "-" {
return NewStdinNode(dir)
}
return NewNode(entrypoint, dir, insecure)
return NewNode(entrypoint, dir, insecure, opts...)
}
func NewNode(
@@ -72,6 +73,16 @@ func NewNode(
return node, err
}
func isRemoteEntrypoint(entrypoint string) bool {
scheme, _ := getScheme(entrypoint)
switch scheme {
case "git", "http", "https":
return true
default:
return false
}
}
func getScheme(uri string) (string, error) {
u, err := giturls.Parse(uri)
if u == nil {
@@ -82,8 +93,8 @@ func getScheme(uri string) (string, error) {
return "git", nil
}
if i := strings.Index(uri, "://"); i != -1 {
return uri[:i], nil
if before, _, ok := strings.Cut(uri, "://"); ok {
return before, nil
}
return "", nil

View File

@@ -10,6 +10,9 @@ type (
parent Node
dir string
checksum string
caCert string
cert string
certKey string
}
)
@@ -54,3 +57,21 @@ func (node *baseNode) Checksum() string {
func (node *baseNode) Verify(checksum string) bool {
return node.checksum == "" || node.checksum == checksum
}
func WithCACert(caCert string) NodeOption {
return func(node *baseNode) {
node.caCert = caCert
}
}
func WithCert(cert string) NodeOption {
return func(node *baseNode) {
node.cert = cert
}
}
func WithCertKey(certKey string) NodeOption {
return func(node *baseNode) {
node.certKey = certKey
}
}

View File

@@ -4,8 +4,8 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
@@ -19,8 +19,17 @@ type FileNode struct {
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
// Find the entrypoint file
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, defaultTaskfiles)
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, DefaultTaskfiles)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
if entrypoint == "" {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true}
} else {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
}
} else if errors.Is(err, os.ErrPermission) {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: true, OwnerChange: true}
}
return nil, err
}
@@ -51,10 +60,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -3,20 +3,20 @@ package taskfile
import (
"context"
"fmt"
"io"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"sync"
giturls "github.com/chainguard-dev/git-urls"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/hashicorp/go-getter"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
)
// An GitNode is a node that reads a Taskfile from a remote location via Git.
@@ -28,6 +28,36 @@ type GitNode struct {
path string
}
type gitRepoCache struct {
mu sync.Mutex // Protects the locks map
locks map[string]*sync.Mutex // One mutex per repo cache key
}
func (c *gitRepoCache) getLockForRepo(cacheKey string) *sync.Mutex {
c.mu.Lock()
defer c.mu.Unlock()
if _, exists := c.locks[cacheKey]; !exists {
c.locks[cacheKey] = &sync.Mutex{}
}
return c.locks[cacheKey]
}
var globalGitRepoCache = &gitRepoCache{
locks: make(map[string]*sync.Mutex),
}
func CleanGitCache() error {
// Clear the in-memory locks map to prevent memory leak
globalGitRepoCache.mu.Lock()
globalGitRepoCache.locks = make(map[string]*sync.Mutex)
globalGitRepoCache.mu.Unlock()
cacheDir := filepath.Join(os.TempDir(), "task-git-repos")
return os.RemoveAll(cacheDir)
}
func NewGitNode(
entrypoint string,
dir string,
@@ -72,24 +102,83 @@ func (node *GitNode) Read() ([]byte, error) {
return node.ReadContext(context.Background())
}
func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.url.String(),
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
})
func (node *GitNode) buildURL() string {
// Get the base URL
baseURL := node.url.String()
// Always use git:: prefix for git URLs (following Terraform's pattern)
// This forces go-getter to use git protocol
if node.ref != "" {
return fmt.Sprintf("git::%s?ref=%s&depth=1", baseURL, node.ref)
}
// When no ref is specified, omit it entirely to let git clone the default branch
return fmt.Sprintf("git::%s?depth=1", baseURL)
}
// getOrCloneRepo returns the path to a cached git repository.
// If the repository is not cached, it clones it first.
// This function is thread-safe: multiple goroutines cloning the same repo+ref
// will synchronize, and only one clone operation will occur.
//
// The cache directory is /tmp/task-git-repos/{cache_key}/
func (node *GitNode) getOrCloneRepo(ctx context.Context) (string, error) {
cacheKey := node.repoCacheKey()
repoMutex := globalGitRepoCache.getLockForRepo(cacheKey)
repoMutex.Lock()
defer repoMutex.Unlock()
cacheDir := filepath.Join(os.TempDir(), "task-git-repos", cacheKey)
// Check cache FIRST - if already cloned, no network needed, timeout irrelevant
gitDir := filepath.Join(cacheDir, ".git")
if _, err := os.Stat(gitDir); err == nil {
return cacheDir, nil
}
// Only check context if we need to clone (requires network)
if err := ctx.Err(); err != nil {
return "", fmt.Errorf("context cancelled while waiting for repository lock: %w", err)
}
getterURL := node.buildURL()
client := &getter.Client{
Ctx: ctx,
Src: getterURL,
Dst: cacheDir,
Mode: getter.ClientModeDir,
}
if err := client.Get(); err != nil {
_ = os.RemoveAll(cacheDir)
return "", fmt.Errorf("failed to clone repository: %w", err)
}
return cacheDir, nil
}
func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
// Get or clone the repository into cache
repoDir, err := node.getOrCloneRepo(ctx)
if err != nil {
return nil, err
}
file, err := fs.Open(node.path)
// Build path to Taskfile in the cached repo
// If node.path is empty, search in repo root; otherwise search in the specified path
// fsext.SearchPath handles both files and directories (searching for DefaultTaskfiles)
searchPath := repoDir
if node.path != "" {
searchPath = filepath.Join(repoDir, node.path)
}
filePath, err := fsext.SearchPath(searchPath, DefaultTaskfiles)
if err != nil {
return nil, err
}
// Read the entire response body
b, err := io.ReadAll(file)
// Read file from cached repo
b, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
@@ -98,8 +187,13 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
}
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}
dir, _ := path.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, path.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
@@ -133,6 +227,22 @@ func (node *GitNode) CacheKey() string {
return fmt.Sprintf("git.%s.%s.%s", node.url.Host, prefix, checksum)
}
// repoCacheKey generates a unique cache key for the repository+ref combination.
// Unlike CacheKey() which includes the file path, this identifies the repository itself.
// Two GitNodes with the same repo+ref but different file paths will share the same cache.
//
// Returns a path like: github.com/user/repo.git/main
func (node *GitNode) repoCacheKey() string {
repoPath := strings.Trim(node.url.Path, "/")
ref := node.ref
if ref == "" {
ref = "_default_" // Placeholder for the remote's default branch
}
return filepath.Join(node.url.Host, repoPath, ref)
}
func splitURLOnDoubleSlash(u *url.URL) (string, string) {
x := strings.Split(u.Path, "//")
switch len(x) {

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)
}
func TestGitNode_sshWithAltRepo(t *testing.T) {
t.Parallel()
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
entrypoint, err := node.ResolveEntrypoint("git@github.com:foo/other.git//Taskfile.yml?ref=dev")
assert.NoError(t, err)
assert.Equal(t, "git@github.com:foo/other.git//Taskfile.yml?ref=dev", entrypoint)
}
func TestGitNode_sshWithDir(t *testing.T) {
t.Parallel()
@@ -91,3 +102,146 @@ func TestGitNode_CacheKey(t *testing.T) {
assert.Equal(t, tt.expectedKey, key)
}
}
func TestGitNode_buildURL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
entrypoint string
expectedURL string
}{
{
name: "HTTPS with ref",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
expectedURL: "git::https://github.com/foo/bar.git?ref=main&depth=1",
},
{
name: "SSH with ref",
entrypoint: "git@github.com:foo/bar.git//Taskfile.yml?ref=main",
expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=main&depth=1",
},
{
name: "HTTPS with tag ref",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=v1.0.0",
expectedURL: "git::https://github.com/foo/bar.git?ref=v1.0.0&depth=1",
},
{
name: "HTTPS without ref (uses remote default branch)",
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml",
expectedURL: "git::https://github.com/foo/bar.git?depth=1",
},
{
name: "SSH with directory path",
entrypoint: "git@github.com:foo/bar.git//directory/Taskfile.yml?ref=dev",
expectedURL: "git::ssh://git@github.com/foo/bar.git?ref=dev&depth=1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
node, err := NewGitNode(tt.entrypoint, "", false)
require.NoError(t, err)
gotURL := node.buildURL()
assert.Equal(t, tt.expectedURL, gotURL)
})
}
}
func TestRepoCacheKey_SameRepoSameRef(t *testing.T) {
t.Parallel()
// Same repo, same ref, different files should have SAME cache key
node1, err := NewGitNode("https://github.com/foo/bar.git//file1.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//dir/file2.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.Equal(t, key1, key2, "Same repo+ref should generate same cache key regardless of file path")
}
func TestRepoCacheKey_SameRepoDifferentRef(t *testing.T) {
t.Parallel()
// Same repo, different ref should have DIFFERENT cache keys
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=dev", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.NotEqual(t, key1, key2, "Different refs should generate different cache keys")
}
func TestRepoCacheKey_DifferentRepos(t *testing.T) {
t.Parallel()
// Different repos should have DIFFERENT cache keys
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/other.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.NotEqual(t, key1, key2, "Different repos should generate different cache keys")
}
func TestRepoCacheKey_NoRefVsExplicitRef(t *testing.T) {
t.Parallel()
// No ref (uses default branch) vs explicit ref should have DIFFERENT cache keys
node1, err := NewGitNode("https://github.com/foo/bar.git//file.yml", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.NotEqual(t, key1, key2, "No ref and explicit ref should generate different cache keys")
}
func TestRepoCacheKey_SSHvsHTTPS(t *testing.T) {
t.Parallel()
// SSH vs HTTPS pointing to same repo should have SAME cache key
// They clone the same repo, so we want to share the cache
node1, err := NewGitNode("git@github.com:foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
node2, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node1.repoCacheKey()
key2 := node2.repoCacheKey()
assert.Equal(t, key1, key2, "SSH and HTTPS for same repo should share cache")
}
func TestRepoCacheKey_Consistency(t *testing.T) {
t.Parallel()
// Calling repoCacheKey multiple times on same node should return same key
node, err := NewGitNode("https://github.com/foo/bar.git//file.yml?ref=main", "", false)
require.NoError(t, err)
key1 := node.repoCacheKey()
key2 := node.repoCacheKey()
key3 := node.repoCacheKey()
assert.Equal(t, key1, key2)
assert.Equal(t, key2, key3)
}

View File

@@ -2,10 +2,13 @@ package taskfile
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -17,7 +20,54 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*baseNode
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
client *http.Client // HTTP client with optional TLS configuration
}
// buildHTTPClient creates an HTTP client with optional TLS configuration.
// If no certificate options are provided, it returns http.DefaultClient.
func buildHTTPClient(insecure bool, caCert, cert, certKey string) (*http.Client, error) {
// Validate that cert and certKey are provided together
if (cert != "" && certKey == "") || (cert == "" && certKey != "") {
return nil, fmt.Errorf("both --cert and --cert-key must be provided together")
}
// If no TLS customization is needed, return the default client
if !insecure && caCert == "" && cert == "" {
return http.DefaultClient, nil
}
tlsConfig := &tls.Config{
InsecureSkipVerify: insecure, //nolint:gosec
}
// Load custom CA certificate if provided
if caCert != "" {
caCertData, err := os.ReadFile(caCert)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCertData) {
return nil, fmt.Errorf("failed to parse CA certificate")
}
tlsConfig.RootCAs = caCertPool
}
// Load client certificate and key if provided
if cert != "" && certKey != "" {
clientCert, err := tls.LoadX509KeyPair(cert, certKey)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{clientCert}
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}, nil
}
func NewHTTPNode(
@@ -34,9 +84,16 @@ func NewHTTPNode(
if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}
}
client, err := buildHTTPClient(insecure, base.caCert, base.cert, base.certKey)
if err != nil {
return nil, err
}
return &HTTPNode{
baseNode: base,
url: url,
client: client,
}, nil
}
@@ -49,16 +106,16 @@ func (node *HTTPNode) Read() ([]byte, error) {
}
func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, *node.url)
url, err := RemoteExists(ctx, *node.url, node.client)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url.String(), nil)
req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.Location()}
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
resp, err := node.client.Do(req.WithContext(ctx))
if err != nil {
if ctx.Err() != nil {
return nil, err

View File

@@ -1,7 +1,18 @@
package taskfile
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -47,3 +58,227 @@ func TestHTTPNode_CacheKey(t *testing.T) {
assert.Equal(t, tt.expectedKey, key)
}
}
func TestBuildHTTPClient_Default(t *testing.T) {
t.Parallel()
// When no TLS customization is needed, should return http.DefaultClient
client, err := buildHTTPClient(false, "", "", "")
require.NoError(t, err)
assert.Equal(t, http.DefaultClient, client)
}
func TestBuildHTTPClient_Insecure(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(true, "", "", "")
require.NoError(t, err)
require.NotNil(t, client)
assert.NotEqual(t, http.DefaultClient, client)
// Check that InsecureSkipVerify is set
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
}
func TestBuildHTTPClient_CACert(t *testing.T) {
t.Parallel()
// Create a temporary CA cert file
tempDir := t.TempDir()
caCertPath := filepath.Join(tempDir, "ca.crt")
// Generate a valid CA certificate
caCertPEM := generateTestCACert(t)
err := os.WriteFile(caCertPath, caCertPEM, 0o600)
require.NoError(t, err)
client, err := buildHTTPClient(false, caCertPath, "", "")
require.NoError(t, err)
require.NotNil(t, client)
assert.NotEqual(t, http.DefaultClient, client)
// Check that custom RootCAs is set
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
}
func TestBuildHTTPClient_CACertNotFound(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "/nonexistent/ca.crt", "", "")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "failed to read CA certificate")
}
func TestBuildHTTPClient_CACertInvalid(t *testing.T) {
t.Parallel()
// Create a temporary file with invalid content
tempDir := t.TempDir()
caCertPath := filepath.Join(tempDir, "invalid.crt")
err := os.WriteFile(caCertPath, []byte("not a valid certificate"), 0o600)
require.NoError(t, err)
client, err := buildHTTPClient(false, caCertPath, "", "")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "failed to parse CA certificate")
}
func TestBuildHTTPClient_CertWithoutKey(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "", "/path/to/cert.crt", "")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together")
}
func TestBuildHTTPClient_KeyWithoutCert(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "", "", "/path/to/key.pem")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "both --cert and --cert-key must be provided together")
}
func TestBuildHTTPClient_CertAndKey(t *testing.T) {
t.Parallel()
// Create temporary cert and key files
tempDir := t.TempDir()
certPath := filepath.Join(tempDir, "client.crt")
keyPath := filepath.Join(tempDir, "client.key")
// Generate a self-signed certificate and key for testing
cert, key := generateTestCertAndKey(t)
err := os.WriteFile(certPath, cert, 0o600)
require.NoError(t, err)
err = os.WriteFile(keyPath, key, 0o600)
require.NoError(t, err)
client, err := buildHTTPClient(false, "", certPath, keyPath)
require.NoError(t, err)
require.NotNil(t, client)
assert.NotEqual(t, http.DefaultClient, client)
// Check that client certificate is set
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.Len(t, transport.TLSClientConfig.Certificates, 1)
}
func TestBuildHTTPClient_CertNotFound(t *testing.T) {
t.Parallel()
client, err := buildHTTPClient(false, "", "/nonexistent/cert.crt", "/nonexistent/key.pem")
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "failed to load client certificate")
}
func TestBuildHTTPClient_InsecureWithCACert(t *testing.T) {
t.Parallel()
// Create a temporary CA cert file
tempDir := t.TempDir()
caCertPath := filepath.Join(tempDir, "ca.crt")
// Generate a valid CA certificate
caCertPEM := generateTestCACert(t)
err := os.WriteFile(caCertPath, caCertPEM, 0o600)
require.NoError(t, err)
// Both insecure and CA cert can be set together
client, err := buildHTTPClient(true, caCertPath, "", "")
require.NoError(t, err)
require.NotNil(t, client)
transport, ok := client.Transport.(*http.Transport)
require.True(t, ok)
require.NotNil(t, transport.TLSClientConfig)
assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
assert.NotNil(t, transport.TLSClientConfig.RootCAs)
}
// generateTestCertAndKey generates a self-signed certificate and key for testing
func generateTestCertAndKey(t *testing.T) (certPEM, keyPEM []byte) {
t.Helper()
// Generate a new ECDSA private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// Create a certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Task Org"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// Create the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
// Encode certificate to PEM
certPEM = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
// Encode private key to PEM
keyDER, err := x509.MarshalECPrivateKey(privateKey)
require.NoError(t, err)
keyPEM = pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyDER,
})
return certPEM, keyPEM
}
// generateTestCACert generates a self-signed CA certificate for testing
func generateTestCACert(t *testing.T) []byte {
t.Helper()
// Generate a new ECDSA private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
// Create a CA certificate template
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test CA"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
IsCA: true,
BasicConstraintsValid: true,
}
// Create the certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
// Encode certificate to PEM
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
}

View File

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

View File

@@ -3,13 +3,15 @@ package taskfile
import (
"context"
"fmt"
"net/url"
"os"
"slices"
"sync"
"time"
"github.com/dominikbraun/graph"
"go.yaml.in/yaml/v3"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
@@ -43,8 +45,12 @@ type (
insecure bool
download bool
offline bool
trustedHosts []string
tempDir string
cacheExpiryDuration time.Duration
caCert string
cert string
certKey string
debugFunc DebugFunc
promptFunc PromptFunc
promptMutex sync.Mutex
@@ -59,6 +65,7 @@ func NewReader(opts ...ReaderOption) *Reader {
insecure: false,
download: false,
offline: false,
trustedHosts: nil,
tempDir: os.TempDir(),
cacheExpiryDuration: 0,
debugFunc: nil,
@@ -119,6 +126,20 @@ func (o *offlineOption) ApplyToReader(r *Reader) {
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].
// By default, the reader uses [os.TempDir].
func WithTempDir(tempDir string) ReaderOption {
@@ -182,14 +203,59 @@ func (o *promptFuncOption) ApplyToReader(r *Reader) {
r.promptFunc = o.promptFunc
}
// WithReaderCACert sets the path to a custom CA certificate for TLS connections.
func WithReaderCACert(caCert string) ReaderOption {
return &readerCACertOption{caCert: caCert}
}
type readerCACertOption struct {
caCert string
}
func (o *readerCACertOption) ApplyToReader(r *Reader) {
r.caCert = o.caCert
}
// WithReaderCert sets the path to a client certificate for TLS connections.
func WithReaderCert(cert string) ReaderOption {
return &readerCertOption{cert: cert}
}
type readerCertOption struct {
cert string
}
func (o *readerCertOption) ApplyToReader(r *Reader) {
r.cert = o.cert
}
// WithReaderCertKey sets the path to a client certificate key for TLS connections.
func WithReaderCertKey(certKey string) ReaderOption {
return &readerCertKeyOption{certKey: certKey}
}
type readerCertKeyOption struct {
certKey string
}
func (o *readerCertKeyOption) ApplyToReader(r *Reader) {
r.certKey = o.certKey
}
// Read will read the Taskfile defined by the [Reader]'s [Node] and recurse
// through any [ast.Includes] it finds, reading each included Taskfile and
// building an [ast.TaskfileGraph] as it goes. If any errors occur, they will be
// returned immediately.
func (r *Reader) Read(ctx context.Context, node Node) (*ast.TaskfileGraph, error) {
// Clean up git cache after reading all taskfiles
defer func() {
_ = CleanGitCache()
}()
if err := r.include(ctx, node); err != nil {
return nil, err
}
return r.graph, nil
}
@@ -206,6 +272,23 @@ func (r *Reader) promptf(format string, a ...any) error {
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)
return slices.Contains(r.trustedHosts, host)
}
func (r *Reader) include(ctx context.Context, node Node) error {
// Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{
@@ -269,6 +352,9 @@ func (r *Reader) include(ctx context.Context, node Node) error {
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
WithParent(node),
WithChecksum(include.Checksum),
WithCACert(r.caCert),
WithCert(r.cert),
WithCertKey(r.certKey),
)
if err != nil {
if include.Optional {
@@ -459,9 +545,9 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
// If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" {
// Prompt the user if required
// Prompt the user if required (unless host is trusted)
prompt := cache.ChecksumPrompt(checksum)
if prompt != "" {
if prompt != "" && !r.isTrusted(node.Location()) {
if err := func() error {
r.promptMutex.Lock()
defer r.promptMutex.Unlock()

View File

@@ -12,7 +12,8 @@ import (
)
var (
defaultTaskfiles = []string{
// DefaultTaskfiles is the list of Taskfile file names supported by default.
DefaultTaskfiles = []string{
"Taskfile.yml",
"taskfile.yml",
"Taskfile.yaml",
@@ -28,6 +29,7 @@ var (
"text/x-yaml",
"application/yaml",
"application/x-yaml",
"application/octet-stream",
}
)
@@ -36,7 +38,7 @@ var (
// at the given URL with any of the default Taskfile files names. If any of
// these match a file, the first matching path will be returned. If no files are
// found, an error will be returned.
func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL, error) {
// Create a new HEAD request for the given URL to check if the resource exists
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
if err != nil {
@@ -44,7 +46,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
}
// Request the given URL
resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
@@ -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
// 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
// path if it is empty
if u.Path == "" {
@@ -76,7 +78,7 @@ func RemoteExists(ctx context.Context, u url.URL) (*url.URL, error) {
req.URL = alt
// Try the alternative URL
resp, err = http.DefaultClient.Do(req)
resp, err = client.Do(req)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
}

View File

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

View File

@@ -3,24 +3,35 @@ package ast
import (
"cmp"
"maps"
"slices"
"time"
"github.com/Masterminds/semver/v3"
)
type TaskRC struct {
Version *semver.Version `yaml:"version"`
Verbose *bool `yaml:"verbose"`
Concurrency *int `yaml:"concurrency"`
Remote Remote `yaml:"remote"`
Experiments map[string]int `yaml:"experiments"`
Version *semver.Version `yaml:"version"`
Verbose *bool `yaml:"verbose"`
Silent *bool `yaml:"silent"`
Color *bool `yaml:"color"`
DisableFuzzy *bool `yaml:"disable-fuzzy"`
Concurrency *int `yaml:"concurrency"`
Interactive *bool `yaml:"interactive"`
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"`
Insecure *bool `yaml:"insecure"`
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
CacheDir *string `yaml:"cache-dir"`
TrustedHosts []string `yaml:"trusted-hosts"`
CACert *string `yaml:"cacert"`
Cert *string `yaml:"cert"`
CertKey *string `yaml:"cert-key"`
}
// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
@@ -42,7 +53,21 @@ func (t *TaskRC) Merge(other *TaskRC) {
t.Remote.Offline = cmp.Or(other.Remote.Offline, t.Remote.Offline)
t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)
t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)
t.Remote.CacheDir = cmp.Or(other.Remote.CacheDir, t.Remote.CacheDir)
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.Remote.CACert = cmp.Or(other.Remote.CACert, t.Remote.CACert)
t.Remote.Cert = cmp.Or(other.Remote.Cert, t.Remote.Cert)
t.Remote.CertKey = cmp.Or(other.Remote.CertKey, t.Remote.CertKey)
t.Verbose = cmp.Or(other.Verbose, t.Verbose)
t.Silent = cmp.Or(other.Silent, t.Silent)
t.Color = cmp.Or(other.Color, t.Color)
t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
t.Interactive = cmp.Or(other.Interactive, t.Interactive)
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
}

View File

@@ -3,7 +3,7 @@ package taskrc
import (
"os"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/go-task/task/v3/taskrc/ast"
)
@@ -65,7 +65,7 @@ func (r *Reader) Read(node *Node) (*ast.TaskRC, error) {
}
// Read the file
b, err := os.ReadFile(node.entrypoint)
b, err := os.ReadFile(node.entrypoint) //nolint:gosec
if err != nil {
return nil, err
}

View File

@@ -6,6 +6,7 @@ import (
"slices"
"strings"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/taskrc/ast"
)
@@ -57,9 +58,16 @@ func GetConfig(dir string) (*ast.TaskRC, error) {
}
// Find all the nodes from the given directory up to the users home directory
entrypoints, err := fsext.SearchAll("", dir, defaultTaskRCs)
absDir, err := filepath.Abs(dir)
if err != nil {
return nil, err
return config, err
}
entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs)
if errors.Is(err, os.ErrPermission) {
err = nil
}
if err != nil {
return config, err
}
// Reverse the entrypoints since we want the child files to override parent ones
@@ -84,6 +92,5 @@ func GetConfig(dir string) (*ast.TaskRC, error) {
}
config.Merge(localConfig)
}
return config, nil
}

View File

@@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -135,3 +136,174 @@ func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in pa
},
}, 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)
})
}

6
testdata/abs_path/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
version: '3'
tasks:
default:
cmds:
- cmd: echo '{{absPath "foo/../bar"}}'

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

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