Compare commits

..

159 Commits
tui ... scoped

Author SHA1 Message Date
Valentin Maerten
81fbca3420 refactor(compiler): remove unnecessary closure comments 2026-01-25 20:54:49 +01:00
Valentin Maerten
7323fe8009 fix: resolve lint issues after rebase
- Fix import order in setup.go (gci)
- Fix variable alignment in experiments.go (gofmt)
- Add nolint:paralleltest directive for TestScopedTaskfiles
2026-01-25 20:45:46 +01:00
Valentin Maerten
c8efbc2f4a docs(experiments): reference issue #2035 in scoped taskfiles doc 2026-01-25 19:54:04 +01:00
Valentin Maerten
17257a1c31 chore: add scoped variables planning documents (to be reverted) 2026-01-25 19:54:04 +01:00
Valentin Maerten
2810c267dd feat(scoped): refactor compiler, add nested includes, document flatten
Refactor compiler.go for better maintainability:
- Extract isScopedMode() helper function
- Split getVariables() into getScopedVariables() and getLegacyVariables()
- Fix directory resolution: parent chain env/vars now resolve from their
  own directory instead of the current task's directory

Add nested includes support and tests:
- Add testdata/scoped_taskfiles/inc_a/nested/Taskfile.yml (3 levels deep)
- Add test case for nested include inheritance (root → a → nested)
- Verify nested includes inherit vars from full parent chain

Fix flaky tests:
- Remove VAR from print tasks (defined in both inc_a and inc_b)
- Test only unique variables (UNIQUE_A, UNIQUE_B, ROOT_VAR)

Document flatten: true escape hatch:
- Add migration guide step for using flatten: true
- Add new section explaining flatten bypasses scoping
- Include example and usage recommendations
2026-01-25 19:54:04 +01:00
Valentin Maerten
a57a16efca fix(compiler): add call.Vars support in scoped mode
When calling a task with vars (e.g., `task: name` with `vars:`),
those vars were not being applied in scoped mode. This fix adds
call.Vars to the variable resolution chain.

Variable priority (lowest to highest):
1. Root Taskfile vars
2. Include Taskfile vars
3. Include passthrough vars
4. Task vars
5. Call vars (NEW)
6. CLI vars
2026-01-25 19:54:04 +01:00
Valentin Maerten
5ef7313e95 docs(experiments): add SCOPED_TASKFILES documentation
Document the new experiment with:
- Environment namespace ({{.env.XXX}}) explanation
- Variable scoping between includes
- CLI variables priority
- Migration guide from legacy mode
- Comparison table between legacy and scoped modes
2026-01-25 19:54:04 +01:00
Valentin Maerten
e05c9f7793 fix(compiler): CLI vars have highest priority in scoped mode
In scoped mode, CLI vars (e.g., `task foo VAR=value`) now correctly
override task-level vars. This is achieved by:

1. Adding a `CLIVars` field to the Compiler struct
2. Storing CLI globals in this field after parsing
3. Applying CLI vars last in scoped mode to ensure they override everything

The order of variable resolution in scoped mode is now:
1. OS env → {{.env.XXX}}
2. Root taskfile env → {{.env.XXX}}
3. Root taskfile vars → {{.VAR}}
4. Include taskfile env/vars (if applicable)
5. IncludeVars (vars passed via includes: section)
6. Task-level vars
7. CLI vars (highest priority)

Legacy mode behavior is unchanged.
2026-01-25 19:54:04 +01:00
Valentin Maerten
edee501b6b feat(experiments): rename SCOPED_INCLUDES to SCOPED_TASKFILES and add env namespace
Rename the experiment from SCOPED_INCLUDES to SCOPED_TASKFILES to better
reflect its expanded scope. This experiment now provides:

1. Variable scoping (existing): includes see only their own vars + parent vars
2. Environment namespace (new): env vars accessible via {{.env.XXX}}

With TASK_X_SCOPED_TASKFILES=1:
- {{.VAR}} accesses vars only (scoped per include)
- {{.env.VAR}} accesses env (OS + Taskfile env:, inherited)
- {{.TASK}} and other special vars remain at root level

This is a breaking change for the experimental feature:
- {{.PATH}} no longer works, use {{.env.PATH}} instead
- Env vars are no longer at root level in templates
2026-01-25 19:54:04 +01:00
Valentin Maerten
efaea39503 test(scoped-includes): add tests for variable isolation
Tests verify:
- Legacy mode: vars merged globally (A sees B's VAR, can access UNIQUE_B)
- Scoped mode: vars isolated (A sees own VAR, cannot access UNIQUE_B)
- Inheritance: includes can still access root vars (ROOT_VAR)

Test structure:
- testdata/scoped_includes/ with main Taskfile and two includes
- inc_a and inc_b both define VAR with different values
- Cross-include test shows A trying to access B's UNIQUE_B
2026-01-25 19:53:38 +01:00
Valentin Maerten
04b8b75525 feat(compiler): implement lazy variable resolution for scoped includes
When SCOPED_INCLUDES experiment is enabled:
- Resolve vars from DAG instead of merged vars
- Apply root Taskfile vars first (inheritance)
- Then apply task's source Taskfile vars from DAG
- Apply IncludeVars passed via includes: section
- Skip IncludedTaskfileVars (contains parent's vars, not source's)

This ensures tasks in included Taskfiles see:
1. Root vars (inheritance from parent)
2. Their own Taskfile's vars
3. Vars passed through includes: section
4. Call vars and task-level vars
2026-01-25 19:53:04 +01:00
Valentin Maerten
0dbeaaf187 feat(taskfile): skip var merge when SCOPED_INCLUDES enabled
When the SCOPED_INCLUDES experiment is enabled, variables from included
Taskfiles are no longer merged globally. They remain in their original
Taskfile within the DAG.

Exception: flatten includes still merge variables globally to allow
sharing common variables across multiple Taskfiles.
2026-01-25 19:53:04 +01:00
Valentin Maerten
da927ad5fe feat(graph): add Root() helper method
Add Root() method to TaskfileGraph to get the root vertex (entrypoint
Taskfile). This will be used for lazy variable resolution.

Note: Tasks already have Location.Taskfile which can be used to find
their source Taskfile in the graph, so GetVertexByNamespace is not
needed.
2026-01-25 19:49:16 +01:00
Valentin Maerten
9732f7e08b feat(executor): store TaskfileGraph for lazy resolution
Store the TaskfileGraph in the Executor so it can be used for lazy
variable resolution when SCOPED_INCLUDES experiment is enabled.

The graph is now preserved after reading, before merging into the
final Taskfile. This allows traversing the DAG at runtime to resolve
variables from the correct scope.
2026-01-25 19:49:16 +01:00
Valentin Maerten
1b418409d1 feat(experiments): add SCOPED_INCLUDES experiment
Add new experiment flag TASK_X_SCOPED_INCLUDES for scoped variable
resolution in included Taskfiles. When enabled, variables from included
Taskfiles will be isolated rather than merged globally.

This is the first step towards implementing lazy DAG-based variable
resolution with strict isolation between includes.
2026-01-25 19:49:16 +01: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
190 changed files with 7629 additions and 1962 deletions

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

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

View File

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

View File

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

View File

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

View File

@@ -16,25 +16,25 @@ jobs:
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v9
with:
version: v2.1.0
version: v2.8.0
lint-jsonschema:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: 3.13
python-version: 3.14
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- 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@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.25.x'
cache: true
- uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --snapshot --clean --config .goreleaser-pr.yml
- uses: actions/upload-artifact@v4
with:
name: task_linux_amd64
path: dist/task_linux_amd64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: task_linux_arm64
path: dist/task_linux_arm64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: task_darwin_amd64
path: dist/task_darwin_amd64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: task_darwin_arm64
path: dist/task_darwin_arm64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: task_windows_amd64
path: dist/task_windows_amd64.zip
- uses: actions/upload-artifact@v4
with:
name: checksums
path: dist/task_checksums.txt
- uses: peter-evans/find-comment@v3
id: find-comment
with:
token: ${{ secrets.GH_PAT || github.token }}
issue-number: ${{ github.event.pull_request.number }}
body-includes: '📦 Build artifacts ready!'
- uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ secrets.GH_PAT || 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

@@ -9,12 +9,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.25.x

View File

@@ -5,23 +5,32 @@ on:
tags:
- 'v*'
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: 1.25.x
- name: npm-login
run: |
npm config set '//registry.npmjs.org/:_authToken'=${{ secrets.NPM_TOKEN }}
- uses: actions/setup-node@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Update npm
run: npm install -g npm@latest
- name: Install Task
uses: go-task/setup-task@v1

View File

@@ -18,13 +18,13 @@ jobs:
runs-on: ${{matrix.platform}}
steps:
- name: Set up Go ${{matrix.go-version}}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{matrix.go-version}}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download Go modules
run: go mod download

1
.gitignore vendored
View File

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

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

@@ -69,7 +69,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
@@ -127,7 +127,7 @@ winget:
repository:
owner: go-task
name: winget-pkgs
branch: 'chore/task-{{.Version}}'
branch: 'task-{{.Version}}'
pull_request:
enabled: true
draft: false
@@ -136,6 +136,8 @@ winget:
owner: microsoft
name: winget-pkgs
branch: master
body: |
/cc @andreynering @pd93 @vmaerten
npms:

View File

@@ -2,8 +2,141 @@
## Unreleased
- Fixed bug that made a generic message, instead of an useful one, appear when
a Taskfile could not be found (#2431 by @andreynering).
- 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

View File

@@ -10,7 +10,7 @@
</p>
<p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
<a href="https://taskfile.dev/docs/installation">Installation</a> | <a href="https://taskfile.dev/docs/getting-started">Getting Started</a> | <a href="https://taskfile.dev/docs/guide">Docs</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://bsky.app/profile/taskfile.dev">Bluesky</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
</p>
<h1>Gold Sponsors</h1>
@@ -19,7 +19,12 @@
<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://magic.dev/">
<img src="website/src/public/img/magic.png" height="100px" width="200px" title="Magic" />
</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

@@ -121,11 +121,15 @@ func changelog(version *semver.Version) error {
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)
}
func setVersionFile(fileName string, version *semver.Version) error {

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/spf13/pflag"
@@ -15,7 +16,6 @@ import (
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/tui"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -29,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,
@@ -102,10 +117,6 @@ func run() error {
return nil
}
if flags.TUI {
return tui.Run()
}
if flags.Completion != "" {
script, err := task.Completion(flags.Completion)
if err != nil {
@@ -161,18 +172,25 @@ 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)
// Store CLI vars for scoped mode where they need highest priority
e.Compiler.CLIVars = globals
// 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

@@ -9,6 +9,7 @@ import (
"strings"
"sync"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
@@ -25,6 +26,8 @@ type Compiler struct {
TaskfileEnv *ast.Vars
TaskfileVars *ast.Vars
CLIVars *ast.Vars // CLI vars passed via command line (e.g., task foo VAR=value)
Graph *ast.TaskfileGraph
Logger *logger.Logger
@@ -44,8 +47,37 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *Call) (*ast.Vars, error)
return c.getVariables(t, call, false)
}
// isScopedMode returns true if scoped variable resolution should be used.
// Scoped mode requires the experiment to be enabled, a task with location info, and a graph.
func (c *Compiler) isScopedMode(t *ast.Task) bool {
return experiments.ScopedTaskfiles.Enabled() &&
t != nil &&
t.Location != nil &&
c.Graph != nil
}
func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
result := env.GetEnviron()
if c.isScopedMode(t) {
return c.getScopedVariables(t, call, evaluateShVars)
}
return c.getLegacyVariables(t, call, evaluateShVars)
}
// getScopedVariables resolves variables in scoped mode.
// In scoped mode:
// - OS env vars are in {{.env.XXX}} namespace, not at root
// - Variables from sibling includes are isolated
//
// Variable resolution order (lowest to highest priority):
// 1. Root Taskfile vars
// 2. Include Taskfile vars
// 3. Include passthrough vars (includes: name: vars:)
// 4. Task vars
// 5. Call vars
// 6. CLI vars
func (c *Compiler) getScopedVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
result := ast.NewVars()
specialVars, err := c.getSpecialVars(t, call)
if err != nil {
return nil, err
@@ -57,29 +89,221 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
return func(k string, v ast.Var) error {
cache := &templater.Cache{Vars: result}
// Replace values
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
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
}
if err := cache.Err(); err != nil {
return err
}
if newVar.Value != nil || newVar.Sh == nil {
result.Set(k, ast.Var{Value: newVar.Value})
return nil
}
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
if err != nil {
return err
}
result.Set(k, ast.Var{Value: static})
return nil
}
}
rangeFunc := getRangeFunc(c.Dir)
var taskRangeFunc func(k string, v ast.Var) error
if t != nil {
cache := &templater.Cache{Vars: result}
dir := templater.Replace(t.Dir, cache)
if err := cache.Err(); err != nil {
return nil, err
}
dir = filepathext.SmartJoin(c.Dir, dir)
taskRangeFunc = getRangeFunc(dir)
}
rootVertex, err := c.Graph.Root()
if err != nil {
return nil, err
}
envMap := make(map[string]any)
for _, e := range os.Environ() {
k, v, _ := strings.Cut(e, "=")
envMap[k] = v
}
resolveEnvToMap := func(k string, v ast.Var, dir string) error {
cache := &templater.Cache{Vars: result}
newVar := templater.ReplaceVar(v, cache)
if err := cache.Err(); err != nil {
return err
}
if newVar.Value != nil || newVar.Sh == nil {
if newVar.Value != nil {
envMap[k] = newVar.Value
}
return nil
}
if evaluateShVars {
envSlice := os.Environ()
for ek, ev := range envMap {
if s, ok := ev.(string); ok {
envSlice = append(envSlice, fmt.Sprintf("%s=%s", ek, s))
}
}
static, err := c.HandleDynamicVar(newVar, dir, envSlice)
if err != nil {
return err
}
envMap[k] = static
}
return nil
}
for k, v := range rootVertex.Taskfile.Env.All() {
if err := resolveEnvToMap(k, v, c.Dir); err != nil {
return nil, err
}
}
for k, v := range rootVertex.Taskfile.Vars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
if t.Location.Taskfile != rootVertex.URI {
predecessorMap, err := c.Graph.PredecessorMap()
if err != nil {
return nil, err
}
var parentChain []*ast.TaskfileVertex
currentURI := t.Location.Taskfile
for {
edges := predecessorMap[currentURI]
if len(edges) == 0 {
break
}
var parentURI string
for _, edge := range edges {
parentURI = edge.Source
break
}
if parentURI == rootVertex.URI {
break
}
parentVertex, err := c.Graph.Vertex(parentURI)
if err != nil {
return nil, err
}
parentChain = append([]*ast.TaskfileVertex{parentVertex}, parentChain...)
currentURI = parentURI
}
for _, parent := range parentChain {
parentDir := filepath.Dir(parent.URI)
for k, v := range parent.Taskfile.Env.All() {
if err := resolveEnvToMap(k, v, parentDir); err != nil {
return nil, err
}
}
// Vars use the parent's directory too
parentRangeFunc := getRangeFunc(parentDir)
for k, v := range parent.Taskfile.Vars.All() {
if err := parentRangeFunc(k, v); err != nil {
return nil, err
}
}
}
includeVertex, err := c.Graph.Vertex(t.Location.Taskfile)
if err != nil {
return nil, err
}
includeDir := filepath.Dir(includeVertex.URI)
for k, v := range includeVertex.Taskfile.Env.All() {
if err := resolveEnvToMap(k, v, includeDir); err != nil {
return nil, err
}
}
includeRangeFunc := getRangeFunc(includeDir)
for k, v := range includeVertex.Taskfile.Vars.All() {
if err := includeRangeFunc(k, v); err != nil {
return nil, err
}
}
}
if t.IncludeVars != nil {
for k, v := range t.IncludeVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
}
if call != nil {
for k, v := range t.Vars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
for k, v := range call.Vars.All() {
if err := taskRangeFunc(k, v); err != nil {
return nil, err
}
}
}
for k, v := range c.CLIVars.All() {
if err := rangeFunc(k, v); err != nil {
return nil, err
}
}
result.Set("env", ast.Var{Value: envMap})
return result, nil
}
// getLegacyVariables resolves variables in legacy mode.
// In legacy mode, all variables (including OS env) are merged at root level.
func (c *Compiler) getLegacyVariables(t *ast.Task, call *Call, evaluateShVars bool) (*ast.Vars, error) {
result := env.GetEnviron()
specialVars, err := c.getSpecialVars(t, call)
if err != nil {
return nil, err
}
for k, v := range specialVars {
result.Set(k, ast.Var{Value: v})
}
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
return func(k string, v ast.Var) error {
cache := &templater.Cache{Vars: result}
newVar := templater.ReplaceVar(v, cache)
if !evaluateShVars && newVar.Value == nil {
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
return nil
}
if !evaluateShVars {
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
if err := cache.Err(); err != nil {
return err
}
// If the variable is already set, we can set it and return
if newVar.Value != nil || newVar.Sh == nil {
result.Set(k, ast.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
if err != nil {
return err
@@ -92,8 +316,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
var taskRangeFunc func(k string, v ast.Var) error
if t != nil {
// NOTE(@andreynering): We're manually joining these paths here because
// this is the raw task, not the compiled one.
cache := &templater.Cache{Vars: result}
dir := templater.Replace(t.Dir, cache)
if err := cache.Err(); err != nil {
@@ -113,6 +335,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
return nil, err
}
}
if t != nil {
for k, v := range t.IncludeVars.All() {
if err := rangeFunc(k, v); err != nil {
@@ -148,7 +371,6 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string, e []string) (string,
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
// If the variable is not dynamic or it is empty, return an empty string
if v.Sh == nil || *v.Sh == "" {
return "", nil
}

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
set -l now (date +%s)
set -l ttl 1 # Cache for 1 second only
# Return cached value if still valid
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
printf '%s\n' $__task_experiments_cache
return
end
# Refresh cache
set -g __task_experiments_cache (task --experiments 2>/dev/null)
set -g __task_experiments_cache_time $now
printf '%s\n' $__task_experiments_cache
end
# Helper function to check if an experiment is enabled
function __task_is_experiment_enabled
set -l experiment $argv[1]
__task_get_experiments | string match -qr "^\* $experiment:.*on"
end
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
# 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,24 +1,42 @@
#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
cmd+=(--taskfile "$taskfile")
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
@@ -27,38 +45,107 @@ function __task_list() {
(( enabled )) || return 0
scripts=()
# 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/v4"
)
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
type (
TaskfileDecodeError struct {
Message string
@@ -53,10 +50,10 @@ func (err *TaskfileDecodeError) Error() string {
if len(te.Errors) > 1 {
fmt.Fprintln(buf, color.RedString("errs:"))
for _, message := range te.Errors {
fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
fmt.Fprintln(buf, color.RedString("- %s", message.Err.Error()))
}
} else {
fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
fmt.Fprintln(buf, color.RedString("err: %s", te.Errors[0].Err.Error()))
}
} else {
// Otherwise print the error message normally
@@ -128,11 +125,3 @@ func (err *TaskfileDecodeError) WithFileInfo(location string, snippet string) *T
err.Snippet = snippet
return err
}
func extractTypeErrorMessage(message string) string {
matches := typeErrorRegex.FindStringSubmatch(message)
if len(matches) == 2 {
return matches[1]
}
return message
}

View File

@@ -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

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
@@ -55,6 +63,7 @@ type (
// Internal
Taskfile *ast.Taskfile
Graph *ast.TaskfileGraph
Logger *logger.Logger
Compiler *Compiler
Output output.Output
@@ -63,14 +72,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 +236,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 {
@@ -253,6 +278,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 +373,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 +412,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 +605,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
@@ -171,7 +171,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
return
} else {
require.NoError(t, err)
@@ -192,7 +192,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 +205,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
}
}
tt.writeFixtureBuffer(t, g, buf)
tt.writeFixtureBuffer(t, g, buffer.buf)
}
// Run the test (with a name if it has one)
@@ -263,6 +263,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) {
@@ -347,6 +364,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 +639,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 +707,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 +1059,235 @@ 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},
}
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...)
}
}
//nolint:paralleltest // enableExperimentForTest modifies global state
func TestScopedTaskfiles(t *testing.T) {
// Legacy tests (without experiment) - vars should be merged globally
t.Run("legacy", func(t *testing.T) {
// Test with scoped taskfiles disabled (legacy) - vars should be merged globally
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
)
// In legacy mode, UNIQUE_B should be accessible (merged globally)
NewExecutorTest(t,
WithName("cross-include"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("a:try-access-b"),
)
})
// Scoped tests (with experiment enabled) - vars should be isolated
t.Run("scoped", func(t *testing.T) {
enableExperimentForTest(t, &experiments.ScopedTaskfiles, 1)
// Test with scoped taskfiles enabled - vars should be isolated
NewExecutorTest(t,
WithName("default"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
)
// Test inheritance: include can access root vars
NewExecutorTest(t,
WithName("inheritance-a"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("a:print"),
)
// Test isolation: each include sees its own vars
NewExecutorTest(t,
WithName("isolation-b"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("b:print"),
)
// In scoped mode, UNIQUE_B should be empty (isolated)
NewExecutorTest(t,
WithName("cross-include"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("a:try-access-b"),
)
// Test env namespace: {{.env.XXX}} should access env vars
NewExecutorTest(t,
WithName("env-namespace"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("print-env"),
)
// Test env separation: {{.ROOT_ENV}} at root should be empty (env not at root level)
NewExecutorTest(t,
WithName("env-separation"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("test-env-separation"),
)
// Test include env: include's env is accessible via {{.env.XXX}}
NewExecutorTest(t,
WithName("include-env"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("a:print-env"),
)
// Test call vars: vars passed when calling a task override task vars
NewExecutorTest(t,
WithName("call-vars"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("call-with-vars"),
)
// Test nested includes (3 levels: root → a → nested)
// Verifies that nested includes inherit vars from their parent chain
NewExecutorTest(t,
WithName("nested"),
WithExecutorOptions(
task.WithDir("testdata/scoped_taskfiles"),
task.WithSilent(true),
),
WithTask("a:nested:print"),
)
})
}

View File

@@ -19,6 +19,7 @@ var (
GentleForce Experiment
RemoteTaskfiles Experiment
EnvPrecedence Experiment
ScopedTaskfiles Experiment
)
// Inactive experiments. These are experiments that cannot be enabled, but are
@@ -33,18 +34,17 @@ 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)
EnvPrecedence = New("ENV_PRECEDENCE", config, 1)
ScopedTaskfiles = New("SCOPED_TASKFILES", config, 1)
AnyVariables = New("ANY_VARIABLES", config)
MapVariables = New("MAP_VARIABLES", config)
}

148
go.mod
View File

@@ -1,84 +1,140 @@
module github.com/go-task/task/v3
go 1.24.0
go 1.24.6
toolchain go1.25.6
require (
charm.land/bubbles/v2 v2.0.0-rc.1
charm.land/bubbletea/v2 v2.0.0-rc.2
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
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/alecthomas/chroma/v2 v2.23.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250930200525-31788bbe6486
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/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.4
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.3.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-20250921194925-171492585d48
mvdan.cc/sh/v3 v3.12.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/sync v0.19.0
golang.org/x/term v0.39.0
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b
)
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
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.17.0 // 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.2 // indirect
cloud.google.com/go/storage v1.58.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
github.com/charmbracelet/x/exp/color v0.0.0-20250915100343-2c2e5896ae6e // indirect
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/windows v0.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // 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.3.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 // indirect
github.com/charmbracelet/x/ansi v0.11.1 // 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.5.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // 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.7 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // 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.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // 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.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // 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/sahilm/fuzzy v0.1.1 // 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.5.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
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.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.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.256.0 // indirect
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

350
go.sum
View File

@@ -1,111 +1,188 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM=
charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
charm.land/bubbletea/v2 v2.0.0-rc.2 h1:TdTbUOFzbufDJmSz/3gomL6q+fR6HwfY+P13hXQzD7k=
charm.land/bubbletea/v2 v2.0.0-rc.2/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 h1:059k1h5vvZ4ASinki9nmBguxu9Rq0UDDSa6q8LOUphk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
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.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=
cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.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/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.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0EofhbyKhhk=
github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
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/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
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/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 h1:W6DpZX6zSkZr0iFq6JVh1vItLoxfYtNlaxOJtWp8Kis=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3/go.mod h1:65HTtKURcv/ict9ZQhr6zT84JqIjMcJbyrZYHHKNfKA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81/go.mod h1:poPFOXFTsJsnLbkV3H2KxAAXT7pdjxxLujLocWjkyzM=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250930200525-31788bbe6486 h1:bbbzFbDGxMnOlPyuBoMJwpSmwhO0nV60D1yKBRgzNxg=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250930200525-31788bbe6486/go.mod h1:URW1lCfdUqp9aey3HTV+jRvaGLuaj2d0jVk2NTRpklY=
github.com/charmbracelet/x/exp/color v0.0.0-20250915100343-2c2e5896ae6e h1:tgRdEJh8/v6MBs2ZR7g2m1fABjnGft5wXyI+X/l5WF8=
github.com/charmbracelet/x/exp/color v0.0.0-20250915100343-2c2e5896ae6e/go.mod h1:/tsSyfR1O2EokQP9iNzNK/fnf5FGdB4w0MOaJTBRp5Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 h1:fsWj8NF5njyMVzELc7++HsvRDvgz3VcgGAUgWBDWWWM=
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197/go.mod h1:xseGeVftoP9rVI+/8WKYrJFH6ior6iERGvklwwHz5+s=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
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.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 h1:7Rs87fbKJoIIxsQS8YKJYGYa0tlsDwwb0twQjV1KB+g=
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38/go.mod h1:6lfcr3MNP+kZR25sF1nQwJFuQnNYBlFy3PGX5rvslXc=
github.com/charmbracelet/x/ansi v0.11.1 h1:iXAC8SyMQDJgtcz9Jnw+HU8WMEctHzoTAETIeA3JXMk=
github.com/charmbracelet/x/ansi v0.11.1/go.mod h1:M49wjzpIujwPceJ+t5w3qh2i87+HRtHohgb5iTyepL0=
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.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
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/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/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
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.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
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/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/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
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.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=
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.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=
github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=
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/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
@@ -117,112 +194,131 @@ 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
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/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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
github.com/puzpuzpuz/xsync/v4 v4.3.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/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
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.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
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.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/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
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=
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.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
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.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/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.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.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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/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-20250921194925-171492585d48 h1:j0QO6IrQsn7rpqs9HxwwP7ae3uZDbzRZfu0gi8IDFiE=
mvdan.cc/sh/moreinterp v0.0.0-20250921194925-171492585d48/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.12.1-0.20260124232039-e74afc18e65b h1:PUPnLxbDzRO9kg/03l7TZk7+ywTv7FxmOhDHOtOdOtk=
mvdan.cc/sh/v3 v3.12.1-0.20260124232039-e74afc18e65b/go.mod h1:mencVHx2sy9XZG5wJbCA9nRUOE3zvMtoRXOmXMxH7sc=

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

View File

@@ -127,12 +127,8 @@ 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 := range p.WordsSeq(strings.NewReader(s)) {
words = append(words, w)
return true
})
if err != nil {
return nil, err
}
cfg := &expand.Config{
Env: expand.FuncEnviron(os.Getenv),
@@ -147,7 +143,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

@@ -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"
@@ -44,7 +47,6 @@ var (
Version bool
Help bool
Init bool
TUI bool
Completion string
List bool
ListAll bool
@@ -59,6 +61,7 @@ var (
Watch bool
Verbose bool
Silent bool
DisableFuzzy bool
AssumeYes bool
Dry bool
Summary bool
@@ -70,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() {
@@ -112,7 +122,6 @@ func init() {
pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
pflag.BoolVarP(&TUI, "tui", "T", false, "Runs Task in TUI mode.")
pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.")
pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.")
pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.")
@@ -125,7 +134,9 @@ func init() {
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.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
pflag.BoolVar(&Interactive, "interactive", getConfig(config, 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.BoolVar(&Summary, "summary", false, "Show summary about a task.")
@@ -136,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.BoolVarP(&Color, "color", "c", getConfig(config, 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, 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, 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.")
@@ -154,11 +166,39 @@ func init() {
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.StringSliceVar(&TrustedHosts, "trusted-hosts", getConfig(config, func() *[]string { return &config.Remote.TrustedHosts }, nil), "List of trusted hosts for remote Taskfiles (comma-separated).")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
pflag.StringVar(&RemoteCacheDir, "remote-cache-dir", getConfig(config, func() *string { return config.Remote.CacheDir }, env.GetTaskEnv("REMOTE_DIR")), "Directory to cache remote Taskfiles.")
pflag.StringVar(&CACert, "cacert", getConfig(config, func() *string { return config.Remote.CACert }, ""), "Path to a custom CA certificate for HTTPS connections.")
pflag.StringVar(&Cert, "cert", getConfig(config, func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
pflag.StringVar(&CertKey, "cert-key", getConfig(config, 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 > taskrc config > NO_COLOR > FORCE_COLOR/CI > default
colorExplicitlySet := pflag.Lookup("color").Changed || (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 {
@@ -202,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
}
@@ -240,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),
@@ -255,6 +307,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithOutputStyle(Output),
task.WithTaskSorter(sorter),
task.WithVersionCheck(true),
task.WithFailfast(Failfast),
)
}

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

View File

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

View File

@@ -1,43 +0,0 @@
package tui
import (
"fmt"
"github.com/charmbracelet/bubbles/v2/list"
"github.com/charmbracelet/bubbles/v2/spinner"
tea "github.com/charmbracelet/bubbletea/v2"
)
type listModel struct {
spinner spinner.Model
list list.Model
selectedIndex int
isLoading bool
}
func newListModel() listModel {
return listModel{
spinner: newSpinner(),
list: list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0),
isLoading: true,
}
}
func (m listModel) Init() tea.Cmd {
return m.spinner.Tick
}
func (m listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
func (m listModel) View() string {
switch {
case m.isLoading:
return fmt.Sprintf("%s %s", m.spinner.View(), "Loading tasks...")
default:
return "todo!"
}
}

View File

@@ -1,59 +0,0 @@
package tui
import (
tea "github.com/charmbracelet/bubbletea/v2"
)
type page int
const (
pageList = iota + 1
)
type mainModel struct {
page page
listPage listModel
isQuitting bool
}
func newMainModel() mainModel {
return mainModel{
page: pageList,
listPage: newListModel(),
}
}
func (m mainModel) Init() tea.Cmd {
return m.listPage.Init()
}
func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "ctrl+q":
m.isQuitting = true
return m, tea.Quit
}
}
switch m.page {
case pageList:
model, cmd := m.listPage.Update(msg)
m.listPage = model.(listModel)
return m, cmd
}
return m, nil
}
func (m mainModel) View() string {
switch {
case m.isQuitting:
return "Quitting..."
case m.page == pageList:
return m.listPage.View()
default:
return "..."
}
}

View File

@@ -1,16 +0,0 @@
package tui
import (
"github.com/charmbracelet/bubbles/v2/spinner"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/x/exp/charmtone"
)
var spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(charmtone.Violet.Hex()))
func newSpinner() spinner.Model {
return spinner.New(
spinner.WithSpinner(spinner.MiniDot),
spinner.WithStyle(spinnerStyle),
)
}

View File

@@ -1,13 +0,0 @@
package tui
import (
tea "github.com/charmbracelet/bubbletea/v2"
)
func Run() error {
m := newMainModel()
p := tea.NewProgram(m)
_, err := p.Run()
return err
}

View File

@@ -1 +1 @@
3.45.4
3.47.0

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, 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, 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: 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
@@ -50,7 +195,6 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
Name: requiredVar.Name,
})
}
}
if len(notAllowedValuesVars) > 0 {

View File

@@ -13,6 +13,7 @@ import (
"github.com/sajari/fuzzy"
"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/execext"
"github.com/go-task/task/v3/internal/filepathext"
@@ -36,7 +37,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
@@ -56,7 +56,11 @@ 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),
)
if os.IsNotExist(err) {
return nil, errors.TaskfileNotFoundError{
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
@@ -68,6 +72,7 @@ func (e *Executor) getRootNode() (taskfile.Node, error) {
return nil, err
}
e.Dir = node.Dir()
e.Entrypoint = node.Location()
return node, err
}
@@ -84,8 +89,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),
)
@@ -96,7 +105,8 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
}
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
e.Graph = graph
if e.Taskfile, err = graph.Merge(experiments.ScopedTaskfiles.Enabled()); err != nil {
return err
}
return nil
@@ -153,16 +163,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)
}
}
@@ -218,6 +228,7 @@ func (e *Executor) setupCompiler() error {
UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,
Graph: e.Graph,
Logger: e.Logger,
}
return nil

183
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,6 +148,30 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
return nil
}
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 (just-in-time for sequential task calls)
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
}
@@ -150,7 +195,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 +228,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 +259,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 +277,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 +307,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 +328,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 +342,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 +353,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 +432,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 +460,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 +503,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 +511,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 +524,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 +567,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"
@@ -569,7 +570,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 +787,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 +831,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 {
@@ -1052,7 +1077,7 @@ func TestIncludesOptionalImplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_implicit_false"
wd, _ := os.Getwd()
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
expected := fmt.Sprintf(message, wd, dir)
e := task.NewExecutor(
@@ -1072,7 +1097,7 @@ func TestIncludesOptionalExplicitFalse(t *testing.T) {
const dir = "testdata/includes_optional_explicit_false"
wd, _ := os.Getwd()
message := "stat %s/%s/TaskfileOptional.yml: no such file or directory"
message := "task: No Taskfile found at \"%s/%s/TaskfileOptional.yml\""
expected := fmt.Sprintf(message, wd, dir)
e := task.NewExecutor(
@@ -1849,6 +1874,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()
@@ -2564,6 +2612,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/v4"
"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/v4"
"github.com/go-task/task/v3/errors"
)

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,21 @@ func (tfg *TaskfileGraph) Visualize(filename string) error {
return draw.DOT(tfg.Graph, f)
}
func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
// Root returns the root vertex of the graph (the entrypoint Taskfile).
func (tfg *TaskfileGraph) Root() (*TaskfileVertex, error) {
hashes, err := graph.TopologicalSort(tfg.Graph)
if err != nil {
return nil, err
}
if len(hashes) == 0 {
return nil, fmt.Errorf("task: graph has no vertices")
}
return tfg.Vertex(hashes[0])
}
// Merge merges all included Taskfiles into the root Taskfile.
// If skipVarsMerge is true, variables are not merged (used for scoped includes).
func (tfg *TaskfileGraph) Merge(skipVarsMerge bool) (*Taskfile, error) {
hashes, err := graph.TopologicalSort(tfg.Graph)
if err != nil {
return nil, err
@@ -92,6 +106,7 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) {
if err := vertex.Taskfile.Merge(
includedVertex.Taskfile,
include,
skipVarsMerge,
); err != nil {
return err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import (
"regexp"
"strings"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/deepcopy"
@@ -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,12 @@ 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,
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/v4"
"github.com/go-task/task/v3/errors"
)
@@ -36,8 +36,9 @@ type Taskfile struct {
Interval time.Duration
}
// Merge merges the second Taskfile into the first
func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
// Merge merges the second Taskfile into the first.
// If skipVarsMerge is true, variables are not merged (used for scoped includes).
func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include, skipVarsMerge bool) error {
if !t1.Version.Equal(t2.Version) {
return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
}
@@ -59,8 +60,19 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
if t1.Tasks == nil {
t1.Tasks = NewTasks()
}
t1.Vars.Merge(t2.Vars, include)
t1.Env.Merge(t2.Env, include)
if t2.Silent {
for _, t := range t2.Tasks.All(nil) {
if t.Silent == nil {
v := true
t.Silent = &v
}
}
}
// Only merge vars if not using scoped includes, or if flattening
if !skipVarsMerge || include.Flatten {
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/v4"
"github.com/go-task/task/v3/taskfile/ast"
)

View File

@@ -8,7 +8,7 @@ import (
"sync"
"github.com/elliotchance/orderedmap/v3"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/filepathext"
@@ -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/v4"
"github.com/go-task/task/v3/errors"
)

View File

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

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,11 @@ 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) {
return nil, errors.TaskfileNotFoundError{URI: entrypoint, Walk: false}
}
return nil, err
}
@@ -51,10 +54,7 @@ func (node *FileNode) Read() ([]byte, error) {
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if strings.Contains(entrypoint, "://") {
return entrypoint, nil
}
if strings.HasPrefix(entrypoint, "git") {
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}

View File

@@ -3,20 +3,19 @@ package taskfile
import (
"context"
"fmt"
"io"
"net/url"
"os"
"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 +27,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 +101,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()
// Check if context was cancelled while waiting for lock
if err := ctx.Err(); err != nil {
return "", fmt.Errorf("context cancelled while waiting for repository lock: %w", err)
}
cacheDir := filepath.Join(os.TempDir(), "task-git-repos", cacheKey)
// check if repo is already cached (under the lock)
gitDir := filepath.Join(cacheDir, ".git")
if _, err := os.Stat(gitDir); err == nil {
return cacheDir, nil
}
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,6 +186,11 @@ func (node *GitNode) ReadContext(_ context.Context) ([]byte, error) {
}
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
// If the file is remote, we don't need to resolve the path
if isRemoteEntrypoint(entrypoint) {
return entrypoint, nil
}
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.url, filepath.Join(dir, entrypoint))
if node.ref != "" {
@@ -133,6 +226,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,
}
// 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,7 +106,7 @@ 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
}
@@ -58,7 +115,7 @@ func (node *HTTPNode) ReadContext(ctx context.Context) ([]byte, error) {
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,14 @@ package taskfile
import (
"context"
"fmt"
"net/url"
"os"
"sync"
"time"
"github.com/dominikbraun/graph"
"go.yaml.in/yaml/v4"
"golang.org/x/sync/errgroup"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
@@ -43,8 +44,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 +64,7 @@ func NewReader(opts ...ReaderOption) *Reader {
insecure: false,
download: false,
offline: false,
trustedHosts: nil,
tempDir: os.TempDir(),
cacheExpiryDuration: 0,
debugFunc: nil,
@@ -119,6 +125,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 +202,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 +271,28 @@ 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)
for _, pattern := range r.trustedHosts {
if host == pattern {
return true
}
}
return false
}
func (r *Reader) include(ctx context.Context, node Node) error {
// Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{
@@ -269,6 +356,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 +549,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,34 @@ 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"`
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 +52,20 @@ 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.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/v4"
"github.com/go-task/task/v3/taskrc/ast"
)

View File

@@ -59,11 +59,11 @@ func GetConfig(dir string) (*ast.TaskRC, error) {
// Find all the nodes from the given directory up to the users home directory
absDir, err := filepath.Abs(dir)
if err != nil {
return nil, err
return config, err
}
entrypoints, err := fsext.SearchAll("", absDir, defaultTaskRCs)
if err != nil {
return nil, err
return config, err
}
// Reverse the entrypoints since we want the child files to override parent ones

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)
})
}

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@

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

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

View File

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

View File

@@ -0,0 +1 @@

160
testdata/if/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,160 @@
version: '3'
vars:
SHOULD_RUN: "yes"
ENV: "prod"
FEATURE_ENABLED: "true"
FEATURE_DISABLED: "false"
tasks:
# Basic command-level if (condition met)
cmd-if-true:
cmds:
- cmd: echo "executed"
if: "true"
# Basic command-level if (condition not met)
cmd-if-false:
cmds:
- cmd: echo "should not appear"
if: "false"
- echo "this runs"
# Task-level if (condition met)
task-if-true:
if: "true"
cmds:
- echo "task executed"
# Task-level if (condition not met)
task-if-false:
if: "false"
cmds:
- echo "should not appear"
# With template variables
if-with-template:
cmds:
- cmd: echo "Running because SHOULD_RUN={{.SHOULD_RUN}}"
if: '[ "{{.SHOULD_RUN}}" = "yes" ]'
# If inside for loop
if-in-for-loop:
cmds:
- for: ["a", "b", "c"]
cmd: echo "processing {{.ITEM}}"
if: '[ "{{.ITEM}}" != "b" ]'
# If on task call
if-on-task-call:
cmds:
- task: subtask
if: "true"
subtask:
internal: true
cmds:
- echo "subtask ran"
# If combined with platforms (both must pass)
if-with-platforms:
cmds:
- cmd: echo "condition and platform met"
platforms: [linux, darwin, windows]
if: "true"
# Skip task call
skip-task-call:
cmds:
- task: subtask
if: "false"
- echo "after skipped task call"
# Task call in cmds with if condition met
task-call-if-true:
cmds:
- task: subtask
if: "true"
- echo "after task call"
# Task call in cmds with if condition not met
task-call-if-false:
cmds:
- task: subtask
if: "false"
- echo "continues after skipped task"
# Template eq - condition met
template-eq-true:
cmds:
- cmd: echo "env is prod"
if: '{{ eq .ENV "prod" }}'
# Template eq - condition not met
template-eq-false:
cmds:
- cmd: echo "should not appear"
if: '{{ eq .ENV "dev" }}'
- echo "this runs"
# Template ne (not equal)
template-ne:
cmds:
- cmd: echo "env is not dev"
if: '{{ ne .ENV "dev" }}'
# Template with boolean-like variable
template-bool-true:
cmds:
- cmd: echo "feature enabled"
if: '{{ eq .FEATURE_ENABLED "true" }}'
# Template with boolean-like variable (false)
template-bool-false:
cmds:
- cmd: echo "should not appear"
if: '{{ eq .FEATURE_DISABLED "true" }}'
- echo "feature was disabled"
# Direct true/false from template
template-direct-true:
cmds:
- cmd: echo "direct true works"
if: '{{ .FEATURE_ENABLED }}'
# Direct true/false from template (false case)
template-direct-false:
cmds:
- cmd: echo "should not appear"
if: '{{ .FEATURE_DISABLED }}'
- echo "direct false skipped correctly"
# Template with CLI variable override
template-cli-var:
cmds:
- cmd: echo "MY_VAR is yes"
if: '{{ eq .MY_VAR "yes" }}'
# Combined template conditions with and
template-and:
cmds:
- cmd: echo "both conditions met"
if: '{{ and (eq .ENV "prod") (eq .FEATURE_ENABLED "true") }}'
# Combined template conditions with or
template-or:
cmds:
- cmd: echo "at least one condition met"
if: '{{ or (eq .ENV "dev") (eq .ENV "prod") }}'
# Task-level if with template
task-level-template:
if: '{{ eq .ENV "prod" }}'
cmds:
- echo "task runs in prod"
# Task-level if with template (not met)
task-level-template-false:
if: '{{ eq .ENV "dev" }}'
cmds:
- echo "should not appear"

View File

@@ -0,0 +1 @@
this runs

View File

@@ -0,0 +1 @@
executed

View File

@@ -0,0 +1,7 @@
task: "if-in-for-loop" started
task: [if-in-for-loop] echo "processing a"
processing a
task: [if-in-for-loop] if condition not met - skipped
task: [if-in-for-loop] echo "processing c"
processing c
task: "if-in-for-loop" finished

View File

@@ -0,0 +1,5 @@
task: "task-call-if-false" started
task: [task-call-if-false] if condition not met - skipped
task: [task-call-if-false] echo "continues after skipped task"
continues after skipped task
task: "task-call-if-false" finished

View File

@@ -0,0 +1,2 @@
subtask ran
after task call

View File

@@ -0,0 +1 @@
task: if condition not met - skipped: "task-if-false"

View File

@@ -0,0 +1 @@
task executed

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