Compare commits

..

84 Commits

Author SHA1 Message Date
Andrey Nering
ca93c2be86 feat: getting starting with a tui 2025-10-02 14:20:33 -03:00
Pete Davison
4e84c6bb76 fix: links to static files 2025-09-23 22:34:43 +00:00
merusso
0f9baf62a1 docs: Update Remote Taskfiles default values (#2430)
* docs: Update Remote Taskfiles default values

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

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

* docs: Update executor.go comment

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

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

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

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

View File

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

View File

@@ -13,14 +13,14 @@ jobs:
name: Lint
strategy:
matrix:
go-version: [1.23.x, 1.24.x]
go-version: [1.24.x, 1.25.x]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: ${{matrix.go-version}}
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
@@ -32,43 +32,12 @@ jobs:
steps:
- uses: actions/setup-python@v5
with:
python-version: 3.12
python-version: 3.13
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: install check-jsonschema
run: python -m pip install 'check-jsonschema==0.27.3'
- name: check-jsonschema (metaschema)
run: check-jsonschema --check-metaschema website/static/schema.json
check_doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v46
with:
files: website/versioned_docs/**
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('website/versioned_docs has changed. Instead you need to update the docs in the website/docs folder.')
check_schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get changed files in the docs folder
id: changed-files-specific
uses: tj-actions/changed-files@v46
with:
files: |
website/static/schema.json
website/static/schema-taskrc.json
- uses: actions/github-script@v7
if: steps.changed-files-specific.outputs.any_changed == 'true'
with:
script: |
core.setFailed('schema.json or schema-taskrc.json has changed. Instead you need to update next-schema.json or next-schema-taskrc.json.')
run: check-jsonschema --check-metaschema website/src/public/schema.json

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,7 @@ nfpms:
formats:
- deb
- rpm
- apk
file_name_template: '{{.ProjectName}}_{{.Os}}_{{.Arch}}'
contents:
- src: completion/bash/task.bash
@@ -135,3 +136,37 @@ winget:
owner: microsoft
name: winget-pkgs
branch: master
npms:
- name: "@go-task/cli"
repository: "git+https://github.com/go-task/task.git"
bugs: https://github.com/go-task/task/issues
description: A task runner / simpler Make alternative written in Go
homepage: https://taskfile.dev
license: MIT
author: "The Task authors"
access: public
keywords:
- "task"
- "taskfile"
- "build-tool"
- "task-runner"
cloudsmiths:
- organization: "task"
repository: "{{if not .IsNightly}}task{{end}}"
formats:
- deb
- rpm
- apk
distributions:
deb:
- "any-distro/any-version"
rpm:
- "any-distro/any-version"
alpine:
- "alpine/any-version"
component: main
republish: true

1
.nvmrc
View File

@@ -1 +0,0 @@
22.18.0

View File

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

View File

@@ -1,5 +1,72 @@
# Changelog
## Unreleased
- Fixed bug that made a generic message, instead of an useful one, appear when
a Taskfile could not be found (#2431 by @andreynering).
## v3.45.4 - 2025-09-17
- Fixed a bug where `cache-expiry` could not be defined in `.taskrc.yml` (#2423
by @vmaerten).
- Fixed a bug where `.taskrc.yml` files in parent folders were not read
correctly (#2424 by @vmaerten).
- Fixed a bug where autocomplete in subfolders did not work with zsh (#2425 by
@vmaerten).
## v3.45.3 - 2025-09-15
- Task now includes built-in core utilities to greatly improve compatibility on
Windows. This means that your commands that uses `cp`, `mv`, `mkdir` or any
other common core utility will now work by default on Windows, without extra
setup. This is something we wanted to address for many many years, and it's
finally being shipped!
[Read our blog post this the topic](https://taskfile.dev/blog/windows-core-utils).
(#197, #2360 by @andreynering).
- :sparkles: Built and deployed a [brand new website](https://taskfile.dev)
using [VitePress](https://vitepress.dev) (#2359, #2369, #2371, #2375, #2378 by
@vmaerten, @andreynering, @pd93).
- Began releasing
[nightly builds](https://github.com/go-task/task/releases/tag/nightly). This
will allow people to test our changes before they are fully released and
without having to install Go to build them (#2358 by @vmaerten).
- Added support for global config files in `$XDG_CONFIG_HOME/task/taskrc.yml` or
`$HOME/.taskrc.yml`. Check out our new
[configuration guide](https://taskfile.dev/docs/reference/config) for more
details (#2247, #2380, #2390, #2391 by @vmaerten, @pd93).
- Added experiments to the taskrc schema to clarify the expected keys and values
(#2235 by @vmaerten).
- Added support for new properties in `.taskrc.yml`: insecure, verbose,
concurrency, remote offline, remote timeout, and remote expiry. :warning:
Note: setting offline via environment variable is no longer supported. (#2389
by @vmaerten)
- Added a `--nested` flag when outputting tasks using `--list --json`. This will
output tasks in a nested structure when tasks are namespaced (#2415 by @pd93).
- Enhanced support for tasks with wildcards: they are now logged correctly, and
wildcard parameters are fully considered during fingerprinting (#1808, #1795
by @vmaerten).
- Fixed panic when a variable was declared as an empty hash (`{}`) (#2416, #2417
by @trulede).
#### Package API
- Bumped the minimum version of Go to 1.24 (#2358 by @vmaerten).
#### Other news
We recently released our
[official GitHub Action](https://github.com/go-task/setup-task). This is based
on the fantastic work by the Arduino team who created and maintained the
community version. Now that this is officially adopted, fixes/updates should be
more timely. We have already merged a couple of longstanding PRs in our
[first release](https://github.com/go-task/setup-task/releases/tag/v1.0.0) (by
@pd93, @shrink, @trim21 and all the previous contributors to
[arduino/setup-task](https://github.com/arduino/setup-task/)).
## v3.45.0-v3.45.2 - 2025-09-15
Failed due to an issue with our release process.
## v3.44.1 - 2025-07-23
- Internal tasks will no longer be shown as suggestions since they cannot be

View File

@@ -1,6 +1,6 @@
<div align="center">
<a href="https://taskfile.dev">
<img src="website/static/img/logo.svg" width="200px" height="200px" />
<img src="website/src/public/img/logo.svg" width="200px" height="200px" />
</a>
<h1>Task</h1>
@@ -19,7 +19,7 @@
<tr>
<td align="center" valign="middle">
<a target="_blank" href="https://devowl.io">
<img src="/website/static/img/devowl.io.svg" height="100px" title="devowl.io" />
<img src="https://devowl.io/wp-content/uploads/meta/favicon.webp" height="100px" title="devowl.io" />
</a>
</td>
</tr>

View File

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

View File

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

View File

@@ -15,8 +15,8 @@ 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"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -102,6 +102,10 @@ func run() error {
return nil
}
if flags.TUI {
return tui.Run()
}
if flags.Completion != "" {
script, err := task.Completion(flags.Completion)
if err != nil {
@@ -111,63 +115,16 @@ func run() error {
return nil
}
if err := experiments.Validate(); err != nil {
log.Warnf("%s\n", err.Error())
}
// Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
flags.Entrypoint,
flags.Dir,
flags.Insecure,
)
if err != nil {
return err
}
tempDir, err := task.NewTempDir(node.Dir())
if err != nil {
return err
}
reader := taskfile.NewReader(
e := task.NewExecutor(
flags.WithFlags(),
taskfile.WithTempDir(tempDir.Remote),
taskfile.WithDebugFunc(func(s string) {
log.VerboseOutf(logger.Magenta, s)
}),
taskfile.WithPromptFunc(func(s string) error {
return log.Prompt(logger.Yellow, s, "n", "y", "yes")
}),
task.WithVersionCheck(true),
)
ctx, cf := context.WithTimeout(context.Background(), flags.Timeout)
defer cf()
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: flags.Timeout}
}
if err := e.Setup(); err != nil {
return err
}
executor, err := task.NewExecutor(graph,
flags.WithFlags(),
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
)
if err != nil {
return err
}
// If the download flag is specified, we should stop execution as soon as
// taskfile is downloaded
if flags.Download {
return nil
}
if flags.ClearCache {
cachePath := filepath.Join(executor.TempDir.Remote, "remote")
cachePath := filepath.Join(e.TempDir.Remote, "remote")
return os.RemoveAll(cachePath)
}
@@ -176,12 +133,13 @@ func run() error {
flags.ListAll,
flags.ListJson,
flags.NoStatus,
flags.Nested,
)
if listOptions.ShouldListTasks() {
if flags.Silent {
return executor.ListTaskNames(flags.ListAll)
return e.ListTaskNames(flags.ListAll)
}
foundTasks, err := executor.ListTasks(listOptions)
foundTasks, err := e.ListTasks(listOptions)
if err != nil {
return err
}
@@ -213,17 +171,17 @@ func run() error {
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})
executor.Taskfile.Vars.Merge(globals, nil)
e.Taskfile.Vars.Merge(globals, nil)
if !flags.Watch {
executor.InterceptInterruptSignals()
e.InterceptInterruptSignals()
}
ctx = context.Background()
ctx := context.Background()
if flags.Status {
return executor.Status(ctx, calls...)
return e.Status(ctx, calls...)
}
return executor.Run(ctx, calls...)
return e.Run(ctx, calls...)
}

View File

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

View File

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

View File

@@ -162,7 +162,7 @@ func (v MissingVar) String() string {
}
func (err *TaskMissingRequiredVarsError) Error() string {
var vars []string
vars := make([]string, 0, len(err.MissingVars))
for _, v := range err.MissingVars {
vars = append(vars, v.String())
}

View File

@@ -11,14 +11,18 @@ import (
// TaskfileNotFoundError is returned when no appropriate Taskfile is found when
// searching the filesystem.
type TaskfileNotFoundError struct {
URI string
Walk bool
URI string
Walk bool
AskInit bool
}
func (err TaskfileNotFoundError) Error() string {
var walkText string
if err.Walk {
walkText = " (or any of the parent directories)"
walkText = " (or any of the parent directories)."
}
if err.AskInit {
walkText += " Run `task --init` to create a new Taskfile."
}
return fmt.Sprintf(`task: No Taskfile found at %q%s`, err.URI, walkText)
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"io"
"os"
"path/filepath"
"sync"
"time"
@@ -27,21 +26,27 @@ type (
// within them.
Executor struct {
// Flags
Dir string
TempDir *TempDir
Force bool
ForceAll bool
Watch bool
Verbose bool
Silent bool
AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
Dir string
Entrypoint string
TempDir TempDir
Force bool
ForceAll bool
Insecure bool
Download bool
Offline bool
Timeout time.Duration
CacheExpiryDuration time.Duration
Watch bool
Verbose bool
Silent bool
AssumeYes bool
AssumeTerm bool // Used for testing
Dry bool
Summary bool
Parallel bool
Color bool
Concurrency int
Interval time.Duration
// I/O
Stdin io.Reader
@@ -67,17 +72,17 @@ type (
executionHashesMutex sync.Mutex
watchedDirs *xsync.MapOf[string, bool]
}
TempDir struct {
Remote string
Fingerprint string
}
)
// NewExecutor creates a new [Executor] and applies the given functional options
// to it.
func NewExecutor(graph *ast.TaskfileGraph, opts ...ExecutorOption) (*Executor, error) {
tf, err := graph.Merge()
if err != nil {
return nil, err
}
func NewExecutor(opts ...ExecutorOption) *Executor {
e := &Executor{
Taskfile: tf,
Timeout: time.Second * 10,
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
@@ -95,10 +100,7 @@ func NewExecutor(graph *ast.TaskfileGraph, opts ...ExecutorOption) (*Executor, e
executionHashesMutex: sync.Mutex{},
}
e.Options(opts...)
if err := e.setup(); err != nil {
return nil, err
}
return e, nil
return e
}
// Options loops through the given [ExecutorOption] functions and applies them
@@ -120,23 +122,33 @@ type dirOption struct {
}
func (o *dirOption) ApplyToExecutor(e *Executor) {
absDir, err := filepath.Abs(o.dir)
if err != nil {
e.Dir = o.dir
return
}
e.Dir = absDir
e.Dir = o.dir
}
// WithEntrypoint sets the entrypoint (main Taskfile) of the [Executor]. By
// default, Task will search for one of the default Taskfiles in the given
// directory.
func WithEntrypoint(entrypoint string) ExecutorOption {
return &entrypointOption{entrypoint}
}
type entrypointOption struct {
entrypoint string
}
func (o *entrypointOption) ApplyToExecutor(e *Executor) {
e.Entrypoint = o.entrypoint
}
// WithTempDir sets the temporary directory that will be used by [Executor] for
// storing temporary files like checksums and cached remote files. By default,
// the temporary directory is set to the user's temporary directory.
func WithTempDir(tempDir *TempDir) ExecutorOption {
func WithTempDir(tempDir TempDir) ExecutorOption {
return &tempDirOption{tempDir}
}
type tempDirOption struct {
tempDir *TempDir
tempDir TempDir
}
func (o *tempDirOption) ApplyToExecutor(e *Executor) {
@@ -171,6 +183,76 @@ func (o *forceAllOption) ApplyToExecutor(e *Executor) {
e.ForceAll = o.forceAll
}
// WithInsecure allows the [Executor] to make insecure connections when reading
// remote taskfiles. By default, insecure connections are rejected.
func WithInsecure(insecure bool) ExecutorOption {
return &insecureOption{insecure}
}
type insecureOption struct {
insecure bool
}
func (o *insecureOption) ApplyToExecutor(e *Executor) {
e.Insecure = o.insecure
}
// WithDownload forces the [Executor] to download a fresh copy of the taskfile
// from the remote source.
func WithDownload(download bool) ExecutorOption {
return &downloadOption{download}
}
type downloadOption struct {
download bool
}
func (o *downloadOption) ApplyToExecutor(e *Executor) {
e.Download = o.download
}
// WithOffline stops the [Executor] from being able to make network connections.
// It will still be able to read local files and cached copies of remote files.
func WithOffline(offline bool) ExecutorOption {
return &offlineOption{offline}
}
type offlineOption struct {
offline bool
}
func (o *offlineOption) ApplyToExecutor(e *Executor) {
e.Offline = o.offline
}
// 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 {
return &timeoutOption{timeout}
}
type timeoutOption struct {
timeout time.Duration
}
func (o *timeoutOption) ApplyToExecutor(e *Executor) {
e.Timeout = o.timeout
}
// WithCacheExpiryDuration sets the duration after which the cache is considered
// expired. By default, the cache is 0 (disabled).
func WithCacheExpiryDuration(duration time.Duration) ExecutorOption {
return &cacheExpiryDurationOption{duration: duration}
}
type cacheExpiryDurationOption struct {
duration time.Duration
}
func (o *cacheExpiryDurationOption) ApplyToExecutor(r *Executor) {
r.CacheExpiryDuration = o.duration
}
// 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.

View File

@@ -3,11 +3,9 @@ package task_test
import (
"bytes"
"cmp"
"context"
"fmt"
"os"
"path/filepath"
"slices"
"testing"
"github.com/sebdah/goldie/v2"
@@ -16,7 +14,6 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -36,12 +33,7 @@ type (
task string
vars map[string]any
input string
nodeDir string
nodeEntrypoint string
nodeInsecure bool
readerOpts []taskfile.ReaderOption
executorOpts []task.ExecutorOption
wantReaderError bool
wantSetupError bool
wantRunError bool
wantStatusError bool
@@ -54,9 +46,8 @@ type (
func NewExecutorTest(t *testing.T, opts ...ExecutorTestOption) {
t.Helper()
tt := &ExecutorTest{
task: "default",
vars: map[string]any{},
nodeDir: ".",
task: "default",
vars: map[string]any{},
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
@@ -153,52 +144,11 @@ func (tt *ExecutorTest) run(t *testing.T) {
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
ctx := context.Background()
// Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
tt.nodeEntrypoint,
tt.nodeDir,
tt.nodeInsecure,
)
require.NoError(t, err)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")),
)
// Set up a temporary directory for the taskfile reader and task executor
tempDir, err := task.NewTempDir(node.Dir())
require.NoError(t, err)
tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote))
// Set up the taskfile reader
reader := taskfile.NewReader(tt.readerOpts...)
graph, err := reader.Read(ctx, node)
if tt.wantReaderError {
require.Error(t, err)
tt.writeFixtureErrReader(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
executorOpts := slices.Concat(
// Apply the node directory and temp directory to the executor options
// by default, but allow them to by overridden by the test options
[]task.ExecutorOption{
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
},
// Apply the executor options from the test
opts := append(
tt.executorOpts,
// Force the input/output streams to be set to the test buffer
[]task.ExecutorOption{
task.WithStdout(&buf),
task.WithStderr(&buf),
},
task.WithStdout(&buf),
task.WithStderr(&buf),
)
// If the test has input, create a reader for it and add it to the
@@ -206,12 +156,19 @@ func (tt *ExecutorTest) run(t *testing.T) {
if tt.input != "" {
var reader bytes.Buffer
reader.WriteString(tt.input)
executorOpts = append(executorOpts, task.WithStdin(&reader))
opts = append(opts, task.WithStdin(&reader))
}
// Set up the task executor
executor, err := task.NewExecutor(graph, executorOpts...)
if tt.wantSetupError {
e := task.NewExecutor(opts...)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
)
// Call setup and check for errors
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
@@ -231,7 +188,8 @@ func (tt *ExecutorTest) run(t *testing.T) {
}
// Run the task and check for errors
if err := executor.Run(ctx, call); tt.wantRunError {
ctx := t.Context()
if err := e.Run(ctx, call); tt.wantRunError {
require.Error(t, err)
tt.writeFixtureErrRun(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
@@ -242,7 +200,7 @@ func (tt *ExecutorTest) run(t *testing.T) {
// If the status flag is set, run the status check
if tt.wantStatusError {
if err := executor.Status(ctx, call); err != nil {
if err := e.Status(ctx, call); err != nil {
tt.writeFixtureStatus(t, g, err.Error())
}
}
@@ -261,16 +219,19 @@ func (tt *ExecutorTest) run(t *testing.T) {
func TestEmptyTask(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/empty_task"),
WithExecutorOptions(),
WithExecutorOptions(
task.WithDir("testdata/empty_task"),
),
)
}
func TestEmptyTaskfile(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/empty_taskfile"),
WithReaderError(),
WithExecutorOptions(
task.WithDir("testdata/empty_taskfile"),
),
WithSetupError(),
WithFixtureTemplating(),
)
}
@@ -279,15 +240,15 @@ func TestEnv(t *testing.T) {
t.Setenv("QUX", "from_os")
NewExecutorTest(t,
WithName("env precedence disabled"),
WithNodeDir("testdata/env"),
WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true),
),
)
NewExecutorTest(t,
WithName("env precedence enabled"),
WithNodeDir("testdata/env"),
WithExecutorOptions(
task.WithDir("testdata/env"),
task.WithSilent(true),
),
WithExperiment(&experiments.EnvPrecedence, 1),
@@ -297,8 +258,8 @@ func TestEnv(t *testing.T) {
func TestVars(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/vars"),
WithExecutorOptions(
task.WithDir("testdata/vars"),
task.WithSilent(true),
),
)
@@ -308,19 +269,25 @@ func TestRequires(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("required var missing"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("missing-var"),
WithRunError(),
)
NewExecutorTest(t,
WithName("required var ok"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("missing-var"),
WithVar("FOO", "bar"),
)
NewExecutorTest(t,
WithName("fails validation"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var"),
WithVar("ENV", "dev"),
WithVar("FOO", "bar"),
@@ -328,37 +295,48 @@ func TestRequires(t *testing.T) {
)
NewExecutorTest(t,
WithName("passes validation"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var"),
WithVar("FOO", "one"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("required var missing + fails validation"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var"),
WithRunError(),
)
NewExecutorTest(t,
WithName("required var missing + fails validation"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("validation-var-dynamic"),
WithVar("FOO", "one"),
WithVar("ENV", "dev"),
)
NewExecutorTest(t,
WithName("require before compile"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("require-before-compile"),
WithRunError(),
)
NewExecutorTest(t,
WithName("var defined in task"),
WithNodeDir("testdata/requires"),
WithExecutorOptions(
task.WithDir("testdata/requires"),
),
WithTask("var-defined-in-task"),
)
}
// TODO: mock fs
func TestSpecialVars(t *testing.T) {
t.Parallel()
@@ -379,13 +357,12 @@ func TestSpecialVars(t *testing.T) {
"included:print-taskfile-dir",
}
for _, executorDir := range []string{dir, subdir} {
for _, dir := range []string{dir, subdir} {
for _, test := range tests {
name := fmt.Sprintf("%s-%s", executorDir, test)
NewExecutorTest(t,
WithName(name),
WithNodeDir(executorDir),
WithName(fmt.Sprintf("%s-%s", dir, test)),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
task.WithVersionCheck(true),
),
@@ -399,8 +376,8 @@ func TestSpecialVars(t *testing.T) {
func TestConcurrency(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/concurrency"),
WithExecutorOptions(
task.WithDir("testdata/concurrency"),
task.WithConcurrency(1),
),
WithPostProcessFn(PPSortedLines),
@@ -410,8 +387,8 @@ func TestConcurrency(t *testing.T) {
func TestParams(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/params"),
WithExecutorOptions(
task.WithDir("testdata/params"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
@@ -421,14 +398,15 @@ func TestParams(t *testing.T) {
func TestDeps(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/deps"),
WithExecutorOptions(
task.WithDir("testdata/deps"),
task.WithSilent(true),
),
WithPostProcessFn(PPSortedLines),
)
}
// TODO: mock fs
func TestStatus(t *testing.T) {
t.Parallel()
@@ -451,8 +429,8 @@ func TestStatus(t *testing.T) {
// gen-foo creates foo.txt, and will always fail it's status check.
NewExecutorTest(t,
WithName("run gen-foo 1 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-foo"),
@@ -463,8 +441,8 @@ func TestStatus(t *testing.T) {
// only exists after the first run.
NewExecutorTest(t,
WithName("run gen-bar 1 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -473,8 +451,8 @@ func TestStatus(t *testing.T) {
// if e.Verbose is set to true.
NewExecutorTest(t,
WithName("run gen-baz silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-silent-baz"),
@@ -489,8 +467,8 @@ func TestStatus(t *testing.T) {
// Run gen-bar a second time to produce a checksum file that matches bar.txt
NewExecutorTest(t,
WithName("run gen-bar 2 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -498,8 +476,8 @@ func TestStatus(t *testing.T) {
// Run gen-bar a third time, to make sure we've triggered the status check.
NewExecutorTest(t,
WithName("run gen-bar 3 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -511,8 +489,8 @@ func TestStatus(t *testing.T) {
require.NoError(t, err)
NewExecutorTest(t,
WithName("run gen-bar 4 silent"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithSilent(true),
),
WithTask("gen-bar"),
@@ -520,44 +498,56 @@ func TestStatus(t *testing.T) {
// all: not up-to-date
NewExecutorTest(t,
WithName("run gen-foo 2"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-foo"),
)
// status: not up-to-date
NewExecutorTest(t,
WithName("run gen-foo 3"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-foo"),
)
// sources: not up-to-date
NewExecutorTest(t,
WithName("run gen-bar 5"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-bar"),
)
// all: up-to-date
NewExecutorTest(t,
WithName("run gen-bar 6"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-bar"),
)
// sources: not up-to-date, no output produced.
NewExecutorTest(t,
WithName("run gen-baz 2"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-silent-baz"),
)
// up-to-date, no output produced
NewExecutorTest(t,
WithName("run gen-baz 3"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("gen-silent-baz"),
)
// up-to-date, output produced due to Verbose mode.
NewExecutorTest(t,
WithName("run gen-baz 4 verbose"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
task.WithVerbose(true),
),
WithTask("gen-silent-baz"),
@@ -570,24 +560,32 @@ func TestPrecondition(t *testing.T) {
const dir = "testdata/precondition"
NewExecutorTest(t,
WithName("a precondition has been met"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("a precondition was not met"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("impossible"),
WithRunError(),
)
NewExecutorTest(t,
WithName("precondition in dependency fails the task"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("depends_on_impossible"),
WithRunError(),
)
NewExecutorTest(t,
WithName("precondition in cmd fails the task"),
WithNodeDir(dir),
WithExecutorOptions(
task.WithDir(dir),
),
WithTask("executes_failing_task_as_cmd"),
WithRunError(),
)
@@ -598,21 +596,25 @@ func TestAlias(t *testing.T) {
NewExecutorTest(t,
WithName("alias"),
WithNodeDir("testdata/alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
),
WithTask("f"),
)
NewExecutorTest(t,
WithName("duplicate alias"),
WithNodeDir("testdata/alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
),
WithTask("x"),
WithRunError(),
)
NewExecutorTest(t,
WithName("alias summary"),
WithNodeDir("testdata/alias"),
WithExecutorOptions(
task.WithDir("testdata/alias"),
task.WithSummary(true),
),
WithTask("f"),
@@ -624,14 +626,16 @@ func TestLabel(t *testing.T) {
NewExecutorTest(t,
WithName("up to date"),
WithNodeDir("testdata/label_uptodate"),
WithExecutorOptions(
task.WithDir("testdata/label_uptodate"),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("summary"),
WithNodeDir("testdata/label_summary"),
WithExecutorOptions(
task.WithDir("testdata/label_summary"),
task.WithSummary(true),
),
WithTask("foo"),
@@ -639,20 +643,26 @@ func TestLabel(t *testing.T) {
NewExecutorTest(t,
WithName("status"),
WithNodeDir("testdata/label_status"),
WithExecutorOptions(
task.WithDir("testdata/label_status"),
),
WithTask("foo"),
WithStatusError(),
)
NewExecutorTest(t,
WithName("var"),
WithNodeDir("testdata/label_var"),
WithExecutorOptions(
task.WithDir("testdata/label_var"),
),
WithTask("foo"),
)
NewExecutorTest(t,
WithName("label in summary"),
WithNodeDir("testdata/label_summary"),
WithExecutorOptions(
task.WithDir("testdata/label_summary"),
),
WithTask("foo"),
)
}
@@ -679,8 +689,8 @@ func TestPromptInSummary(t *testing.T) {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("foo"),
@@ -698,8 +708,8 @@ func TestPromptWithIndirectTask(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("bar"),
@@ -712,8 +722,8 @@ func TestPromptAssumeYes(t *testing.T) {
NewExecutorTest(t,
WithName("--yes flag should skip prompt"),
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
task.WithAssumeYes(true),
),
@@ -723,8 +733,8 @@ func TestPromptAssumeYes(t *testing.T) {
NewExecutorTest(t,
WithName("task should raise errors.TaskCancelledError"),
WithNodeDir("testdata/prompt"),
WithExecutorOptions(
task.WithDir("testdata/prompt"),
task.WithAssumeTerm(true),
),
WithTask("foo"),
@@ -761,8 +771,8 @@ func TestForCmds(t *testing.T) {
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/for/cmds"),
WithExecutorOptions(
task.WithDir("testdata/for/cmds"),
task.WithSilent(true),
task.WithForce(true),
),
@@ -804,8 +814,8 @@ func TestForDeps(t *testing.T) {
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/for/deps"),
WithExecutorOptions(
task.WithDir("testdata/for/deps"),
task.WithSilent(true),
task.WithForce(true),
// Force output of each dep to be grouped together to prevent interleaving
@@ -850,8 +860,8 @@ func TestReference(t *testing.T) {
for _, test := range tests {
NewExecutorTest(t,
WithName(test.name),
WithNodeDir("testdata/var_references"),
WithExecutorOptions(
task.WithDir("testdata/var_references"),
task.WithSilent(true),
task.WithForce(true),
),
@@ -918,8 +928,8 @@ func TestVarInheritance(t *testing.T) {
for _, test := range tests {
NewExecutorTest(t,
WithName(test.name),
WithNodeDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
WithExecutorOptions(
task.WithDir(fmt.Sprintf("testdata/var_inheritance/v3/%s", test.name)),
task.WithSilent(true),
task.WithForce(true),
),
@@ -933,20 +943,26 @@ func TestFuzzyModel(t *testing.T) {
NewExecutorTest(t,
WithName("fuzzy"),
WithNodeDir("testdata/fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("instal"),
WithRunError(),
)
NewExecutorTest(t,
WithName("not-fuzzy"),
WithNodeDir("testdata/fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("install"),
)
NewExecutorTest(t,
WithName("intern"),
WithNodeDir("testdata/fuzzy"),
WithExecutorOptions(
task.WithDir("testdata/fuzzy"),
),
WithTask("intern"),
WithRunError(),
)
@@ -957,65 +973,17 @@ func TestIncludeChecksum(t *testing.T) {
NewExecutorTest(t,
WithName("correct"),
WithNodeDir("testdata/includes_checksum/correct"),
WithExecutorOptions(
task.WithDir("testdata/includes_checksum/correct"),
),
)
NewExecutorTest(t,
WithName("incorrect"),
WithNodeDir("testdata/includes_checksum/incorrect"),
WithReaderError(),
WithExecutorOptions(
task.WithDir("testdata/includes_checksum/incorrect"),
),
WithSetupError(),
WithFixtureTemplating(),
)
}
func TestWildcard(t *testing.T) {
t.Parallel()
tests := []struct {
name string
call string
wantErr bool
}{
{
name: "basic wildcard",
call: "wildcard-foo",
},
{
name: "double wildcard",
call: "foo-wildcard-bar",
},
{
name: "store wildcard",
call: "start-foo",
},
{
name: "matches exactly",
call: "matches-exactly-*",
},
{
name: "no matches",
call: "no-match",
wantErr: true,
},
{
name: "multiple matches",
call: "wildcard-foo-bar",
},
}
for _, test := range tests {
opts := []ExecutorTestOption{
WithName(test.name),
WithNodeDir("testdata/wildcards"),
WithExecutorOptions(
task.WithSilent(true),
task.WithForce(true),
),
WithTask(test.call),
}
if test.wantErr {
opts = append(opts, WithRunError())
}
NewExecutorTest(t, opts...)
}
}

View File

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

View File

@@ -2,9 +2,7 @@ package task_test
import (
"bytes"
"context"
"path/filepath"
"slices"
"testing"
"github.com/sebdah/goldie/v2"
@@ -12,7 +10,6 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/taskfile"
"github.com/go-task/task/v3/taskfile/ast"
)
@@ -29,17 +26,12 @@ type (
// running `task gen:fixtures`.
FormatterTest struct {
TaskTest
task string
vars map[string]any
nodeDir string
nodeEntrypoint string
nodeInsecure bool
readerOpts []taskfile.ReaderOption
executorOpts []task.ExecutorOption
listOptions task.ListOptions
wantReaderError bool
wantSetupError bool
wantListError bool
task string
vars map[string]any
executorOpts []task.ExecutorOption
listOptions task.ListOptions
wantSetupError bool
wantListError bool
}
)
@@ -49,9 +41,8 @@ type (
func NewFormatterTest(t *testing.T, opts ...FormatterTestOption) {
t.Helper()
tt := &FormatterTest{
task: "default",
vars: map[string]any{},
nodeDir: ".",
task: "default",
vars: map[string]any{},
TaskTest: TaskTest{
experiments: map[*experiments.Experiment]int{},
fixtureTemplateData: map[string]any{},
@@ -123,57 +114,23 @@ func (tt *FormatterTest) run(t *testing.T) {
f := func(t *testing.T) {
t.Helper()
var buf bytes.Buffer
ctx := context.Background()
// Create a new root node for the given entrypoint
node, err := taskfile.NewRootNode(
tt.nodeEntrypoint,
tt.nodeDir,
tt.nodeInsecure,
)
require.NoError(t, err)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(node.Dir(), "testdata")),
)
// Set up a temporary directory for the taskfile reader and task executor
tempDir, err := task.NewTempDir(node.Dir())
require.NoError(t, err)
tt.readerOpts = append(tt.readerOpts, taskfile.WithTempDir(tempDir.Remote))
// Set up the taskfile reader
reader := taskfile.NewReader(tt.readerOpts...)
graph, err := reader.Read(ctx, node)
if tt.wantReaderError {
require.Error(t, err)
tt.writeFixtureErrReader(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
return
} else {
require.NoError(t, err)
}
executorOpts := slices.Concat(
// Apply the node directory and temp directory to the executor options
// by default, but allow them to by overridden by the test options
[]task.ExecutorOption{
task.WithDir(node.Dir()),
task.WithTempDir(tempDir),
},
// Apply the executor options from the test
opts := append(
tt.executorOpts,
// Force the input/output streams to be set to the test buffer
[]task.ExecutorOption{
task.WithStdout(&buf),
task.WithStderr(&buf),
},
task.WithStdout(&buf),
task.WithStderr(&buf),
)
// Set up the task executor
executor, err := task.NewExecutor(graph, executorOpts...)
if tt.wantSetupError {
e := task.NewExecutor(opts...)
// Create a golden fixture file for the output
g := goldie.New(t,
goldie.WithFixtureDir(filepath.Join(e.Dir, "testdata")),
)
// Call setup and check for errors
if err := e.Setup(); tt.wantSetupError {
require.Error(t, err)
tt.writeFixtureErrSetup(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
@@ -189,7 +146,7 @@ func (tt *FormatterTest) run(t *testing.T) {
}
// Run the formatter and check for errors
if _, err := executor.ListTasks(tt.listOptions); tt.wantListError {
if _, err := e.ListTasks(tt.listOptions); tt.wantListError {
require.Error(t, err)
tt.writeFixtureErrList(t, g, err)
tt.writeFixtureBuffer(t, g, buf)
@@ -213,7 +170,9 @@ func TestNoLabelInList(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithNodeDir("testdata/label_list"),
WithExecutorOptions(
task.WithDir("testdata/label_list"),
),
WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true,
}),
@@ -225,7 +184,9 @@ func TestListAllShowsNoDesc(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithNodeDir("testdata/list_mixed_desc"),
WithExecutorOptions(
task.WithDir("testdata/list_mixed_desc"),
),
WithListOptions(task.ListOptions{
ListAllTasks: true,
}),
@@ -237,7 +198,9 @@ func TestListCanListDescOnly(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithNodeDir("testdata/list_mixed_desc"),
WithExecutorOptions(
task.WithDir("testdata/list_mixed_desc"),
),
WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true,
}),
@@ -248,7 +211,9 @@ func TestListDescInterpolation(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithNodeDir("testdata/list_desc_interpolation"),
WithExecutorOptions(
task.WithDir("testdata/list_desc_interpolation"),
),
WithListOptions(task.ListOptions{
ListOnlyTasksWithDescriptions: true,
}),
@@ -259,7 +224,9 @@ func TestJsonListFormat(t *testing.T) {
t.Parallel()
NewFormatterTest(t,
WithNodeDir("testdata/json_list_format"),
WithExecutorOptions(
task.WithDir("testdata/json_list_format"),
),
WithListOptions(task.ListOptions{
FormatTaskListAsJSON: true,
}),

39
go.mod
View File

@@ -1,12 +1,16 @@
module github.com/go-task/task/v3
go 1.23.0
go 1.24.0
require (
github.com/Ladicle/tabwriter v1.0.0
github.com/Masterminds/semver/v3 v3.4.0
github.com/alecthomas/chroma/v2 v2.20.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/dominikbraun/graph v0.23.0
github.com/elliotchance/orderedmap/v3 v3.1.0
@@ -19,16 +23,16 @@ require (
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/otiai10/copy v1.14.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.7.1
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.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.16.0
golang.org/x/term v0.33.0
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
)
@@ -36,26 +40,45 @@ require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/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/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/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-isatty v0.0.20 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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/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/stretchr/objx v0.5.2 // indirect
github.com/u-root/u-root v0.14.1-0.20250807200646-5e7721023dc7 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/xanzy/ssh-agent v0.3.3 // 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.34.0 // indirect
golang.org/x/sys v0.36.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

91
go.sum
View File

@@ -11,18 +11,44 @@ github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNx
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.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/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/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/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/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
@@ -36,6 +62,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/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=
@@ -76,8 +104,12 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -85,19 +117,25 @@ 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-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/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/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -107,8 +145,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
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/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=
@@ -119,18 +162,24 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/u-root/u-root v0.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/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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
@@ -138,13 +187,13 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD
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=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -154,11 +203,11 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
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=
@@ -173,5 +222,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/sh/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=

65
help.go
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"time"
"github.com/spf13/pflag"
@@ -13,10 +12,10 @@ import (
"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"
"github.com/go-task/task/v3/taskfile/ast"
"github.com/go-task/task/v3/taskrc"
taskrcast "github.com/go-task/task/v3/taskrc/ast"
)
const usage = `Usage: task [flags...] [task...]
@@ -45,6 +44,7 @@ var (
Version bool
Help bool
Init bool
TUI bool
Completion string
List bool
ListAll bool
@@ -52,6 +52,7 @@ var (
TaskSort string
Status bool
NoStatus bool
Nested bool
Insecure bool
Force bool
ForceAll bool
@@ -96,7 +97,9 @@ func init() {
// Parse the experiments
dir = cmp.Or(dir, filepath.Dir(entrypoint))
experiments.Parse(dir)
config, _ := taskrc.GetConfig(dir)
experiments.ParseWithConfig(dir, config)
// Parse the rest of the flags
log.SetFlags(0)
@@ -105,13 +108,11 @@ func init() {
log.Print(usage)
pflag.PrintDefaults()
}
offline, err := strconv.ParseBool(cmp.Or(env.GetTaskEnv("OFFLINE"), "false"))
if err != nil {
offline = false
}
pflag.BoolVar(&Version, "version", false, "Show Task version.")
pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
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.")
@@ -119,9 +120,10 @@ func init() {
pflag.StringVar(&TaskSort, "sort", "", "Changes the order of the tasks when listed. [default|alphanumeric|none].")
pflag.BoolVar(&Status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.")
pflag.BoolVar(&NoStatus, "no-status", false, "Ignore status when listing tasks as JSON")
pflag.BoolVar(&Insecure, "insecure", false, "Forces Task to download Taskfiles over insecure connections.")
pflag.BoolVar(&Nested, "nested", false, "Nest namespaces when listing tasks as JSON")
pflag.BoolVar(&Insecure, "insecure", getConfig(config, func() *bool { return config.Remote.Insecure }, false), "Forces Task to download Taskfiles over insecure connections.")
pflag.BoolVarP(&Watch, "watch", "w", false, "Enables watch of the given task.")
pflag.BoolVarP(&Verbose, "verbose", "v", false, "Enables verbose mode.")
pflag.BoolVarP(&Verbose, "verbose", "v", getConfig(config, func() *bool { return config.Verbose }, false), "Enables verbose mode.")
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
@@ -135,7 +137,7 @@ func init() {
pflag.StringVar(&Output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
pflag.BoolVar(&Output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
pflag.BoolVarP(&Color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
pflag.IntVarP(&Concurrency, "concurrency", "C", 0, "Limit number of tasks to run concurrently.")
pflag.IntVarP(&Concurrency, "concurrency", "C", getConfig(config, func() *int { return config.Concurrency }, 0), "Limit number of tasks to run concurrently.")
pflag.DurationVarP(&Interval, "interval", "I", 0, "Interval to watch for changes.")
pflag.BoolVarP(&Global, "global", "g", false, "Runs global Taskfile, from $HOME/{T,t}askfile.{yml,yaml}.")
pflag.BoolVar(&Experiments, "experiments", false, "Lists all the available experiments and whether or not they are enabled.")
@@ -151,12 +153,11 @@ func init() {
// Remote Taskfiles experiment will adds the "download" and "offline" flags
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", offline, "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", time.Second*10, "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", 0, "Expiry duration for cached remote Taskfiles.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
}
pflag.Parse()
}
@@ -197,12 +198,16 @@ func Validate() error {
return errors.New("task: --no-status only applies to --json with --list or --list-all")
}
if Nested && !ListJson {
return errors.New("task: --nested only applies to --json with --list or --list-all")
}
return nil
}
// WithFlags is a special internal functional option that is used to pass flags
// from the CLI into any constructor that accepts functional options.
func WithFlags() *flagsOption {
func WithFlags() task.ExecutorOption {
return &flagsOption{}
}
@@ -229,8 +234,14 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
e.Options(
task.WithDir(dir),
task.WithEntrypoint(Entrypoint),
task.WithForce(Force),
task.WithForceAll(ForceAll),
task.WithInsecure(Insecure),
task.WithDownload(Download),
task.WithOffline(Offline),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithWatch(Watch),
task.WithVerbose(Verbose),
task.WithSilent(Silent),
@@ -247,11 +258,15 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
)
}
func (o *flagsOption) ApplyToReader(r *taskfile.Reader) {
r.Options(
taskfile.WithInsecure(Insecure),
taskfile.WithDownload(Download),
taskfile.WithOffline(Offline),
taskfile.WithCacheExpiryDuration(CacheExpiryDuration),
)
// getConfig extracts a config value directly from a pointer field with a fallback default
func getConfig[T any](config *taskrcast.TaskRC, fieldFunc func() *T, fallback T) T {
if config == nil {
return fallback
}
field := fieldFunc()
if field != nil {
return *field
}
return fallback
}

View File

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

View File

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

43
internal/tui/list.go Normal file
View File

@@ -0,0 +1,43 @@
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!"
}
}

59
internal/tui/main.go Normal file
View File

@@ -0,0 +1,59 @@
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 "..."
}
}

16
internal/tui/spinner.go Normal file
View File

@@ -0,0 +1,16 @@
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),
)
}

13
internal/tui/tui.go Normal file
View File

@@ -0,0 +1,13 @@
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.44.1
3.45.4

32
package-lock.json generated
View File

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

View File

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

View File

@@ -4,13 +4,19 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/sajari/fuzzy"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/fsext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/output"
"github.com/go-task/task/v3/internal/version"
@@ -18,8 +24,18 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)
func (e *Executor) setup() error {
func (e *Executor) Setup() error {
e.setupLogger()
node, err := e.getRootNode()
if err != nil {
return err
}
if err := e.setupTempDir(); err != nil {
return err
}
if err := e.readTaskfile(node); err != nil {
return err
}
e.setupFuzzyModel()
e.setupStdFiles()
if err := e.setupOutput(); err != nil {
@@ -39,6 +55,53 @@ func (e *Executor) setup() error {
return nil
}
func (e *Executor) getRootNode() (taskfile.Node, error) {
node, err := taskfile.NewRootNode(e.Entrypoint, e.Dir, e.Insecure, e.Timeout)
if os.IsNotExist(err) {
return nil, errors.TaskfileNotFoundError{
URI: fsext.DefaultDir(e.Entrypoint, e.Dir),
Walk: true,
AskInit: true,
}
}
if err != nil {
return nil, err
}
e.Dir = node.Dir()
return node, err
}
func (e *Executor) readTaskfile(node taskfile.Node) error {
ctx, cf := context.WithTimeout(context.Background(), e.Timeout)
defer cf()
debugFunc := func(s string) {
e.Logger.VerboseOutf(logger.Magenta, s)
}
promptFunc := func(s string) error {
return e.Logger.Prompt(logger.Yellow, s, "n", "y", "yes")
}
reader := taskfile.NewReader(
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithDebugFunc(debugFunc),
taskfile.WithPromptFunc(promptFunc),
)
graph, err := reader.Read(ctx, node)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: e.Timeout}
}
return err
}
if e.Taskfile, err = graph.Merge(); err != nil {
return err
}
return nil
}
func (e *Executor) setupFuzzyModel() {
if e.Taskfile == nil {
return
@@ -60,6 +123,52 @@ func (e *Executor) setupFuzzyModel() {
e.fuzzyModel = model
}
func (e *Executor) setupTempDir() error {
if e.TempDir != (TempDir{}) {
return nil
}
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, ".task"),
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
}
} else if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return err
}
projectDir, _ := filepath.Abs(e.Dir)
projectName := filepath.Base(projectDir)
e.TempDir = TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}
} else {
e.TempDir = TempDir{
Remote: filepathext.SmartJoin(e.Dir, tempDir),
Fingerprint: filepathext.SmartJoin(e.Dir, tempDir),
}
}
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir != "" {
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
if err != nil {
return err
}
e.TempDir.Remote = remoteTempDir
} else {
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
}
}
return nil
}
func (e *Executor) setupStdFiles() {
if e.Stdin == nil {
e.Stdin = os.Stdin
@@ -105,7 +214,7 @@ func (e *Executor) setupCompiler() error {
e.Compiler = &Compiler{
Dir: e.Dir,
Entrypoint: e.Taskfile.Location,
Entrypoint: e.Entrypoint,
UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,

View File

@@ -172,7 +172,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
if t.Method != "" {
method = t.Method
}
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
@@ -467,7 +466,6 @@ func (e *Executor) GetTask(call *Call) (*ast.Task, error) {
DidYouMean: didYouMean,
}
}
return matchingTask, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -46,17 +46,22 @@ type Task struct {
Namespace string
IncludeVars *Vars
IncludedTaskfileVars *Vars
FullName string
}
func (t *Task) Name() string {
if t.Label != "" {
return t.Label
}
if t.FullName != "" {
return t.FullName
}
return t.Task
}
func (t *Task) LocalName() string {
name := t.Task
name := t.FullName
name = strings.TrimPrefix(name, t.Namespace)
name = strings.TrimPrefix(name, ":")
return name
@@ -220,6 +225,7 @@ func (t *Task) DeepCopy() *Task {
Location: t.Location.DeepCopy(),
Requires: t.Requires.DeepCopy(),
Namespace: t.Namespace,
FullName: t.FullName,
}
return c
}

View File

@@ -18,7 +18,10 @@ type Var struct {
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.MappingNode:
key := node.Content[0].Value
key := "<none>"
if len(node.Content) > 0 {
key = node.Content[0].Value
}
switch key {
case "sh", "ref", "map":
var m struct {

View File

@@ -3,6 +3,7 @@ package taskfile
import (
"context"
"strings"
"time"
giturls "github.com/chainguard-dev/git-urls"
@@ -32,6 +33,7 @@ func NewRootNode(
entrypoint string,
dir string,
insecure bool,
timeout time.Duration,
) (Node, error) {
dir = fsext.DefaultDir(entrypoint, dir)
// If the entrypoint is "-", we read from stdin

View File

@@ -18,15 +18,21 @@ type FileNode struct {
}
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
var err error
base := NewBaseNode(dir, opts...)
entrypoint, base.dir, err = fsext.Search(entrypoint, base.dir, defaultTaskfiles)
// Find the entrypoint file
resolvedEntrypoint, err := fsext.Search(entrypoint, dir, defaultTaskfiles)
if err != nil {
return nil, err
}
// Resolve the directory
resolvedDir, err := fsext.ResolveDir(entrypoint, resolvedEntrypoint, dir)
if err != nil {
return nil, err
}
return &FileNode{
baseNode: base,
entrypoint: entrypoint,
baseNode: NewBaseNode(resolvedDir, opts...),
entrypoint: resolvedEntrypoint,
}, nil
}

View File

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

View File

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

View File

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

View File

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

137
taskrc/taskrc_test.go Normal file
View File

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

View File

@@ -1,78 +0,0 @@
package task
import (
"path/filepath"
"strings"
"github.com/go-task/task/v3/internal/env"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
)
type TempDir struct {
Remote string
Fingerprint string
}
func NewTempDir(dir string) (*TempDir, error) {
tempDir, err := setupTempDirFingerprint(dir)
if err != nil {
return nil, err
}
err = setupTempDirRemote(dir, tempDir)
if err != nil {
return nil, err
}
return tempDir, nil
}
func setupTempDirFingerprint(dir string) (*TempDir, error) {
tempDir := env.GetTaskEnv("TEMP_DIR")
if tempDir == "" {
return &TempDir{
Remote: filepathext.SmartJoin(dir, ".task"),
Fingerprint: filepathext.SmartJoin(dir, ".task"),
}, nil
}
if filepath.IsAbs(tempDir) || strings.HasPrefix(tempDir, "~") {
tempDir, err := execext.ExpandLiteral(tempDir)
if err != nil {
return nil, err
}
projectDir, _ := filepath.Abs(dir)
projectName := filepath.Base(projectDir)
return &TempDir{
Remote: tempDir,
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
}, nil
}
return &TempDir{
Remote: filepathext.SmartJoin(dir, tempDir),
Fingerprint: filepathext.SmartJoin(dir, tempDir),
}, nil
}
func setupTempDirRemote(dir string, tempDir *TempDir) error {
remoteDir := env.GetTaskEnv("REMOTE_DIR")
if remoteDir == "" {
return nil
}
if filepath.IsAbs(remoteDir) || strings.HasPrefix(remoteDir, "~") {
remoteTempDir, err := execext.ExpandLiteral(remoteDir)
if err != nil {
return err
}
tempDir.Remote = remoteTempDir
return nil
}
tempDir.Remote = filepathext.SmartJoin(dir, ".task")
return nil
}

View File

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

View File

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

View File

@@ -22,3 +22,14 @@ tasks:
run: once
cmds:
- echo starting {{.CONTENT}} >> hash.txt
deploy:
cmds:
- rm -rf wildcard.txt
- task: deploy:infra
- task: deploy:js
- task: deploy:go
deploy:*:
run: once
cmd: echo "Deploy {{index .MATCH 0}}" >> wildcard.txt

View File

@@ -1 +0,0 @@
Hello foo

View File

@@ -1 +0,0 @@
Hello foo bar

View File

@@ -1 +0,0 @@
I don't consume matches: []

View File

@@ -1 +0,0 @@
Hello foo-bar

View File

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

View File

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

View File

@@ -1 +0,0 @@
Starting foo

View File

@@ -44,9 +44,14 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
if err != nil {
return nil, err
}
fullName := origTask.Task
if matches, exists := vars.Get("MATCH"); exists {
for _, match := range matches.Value.([]string) {
fullName = strings.Replace(fullName, "*", match, 1)
}
}
cache := &templater.Cache{Vars: vars}
new := ast.Task{
Task: origTask.Task,
Label: templater.Replace(origTask.Label, cache),
@@ -76,6 +81,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
Requires: origTask.Requires,
Watch: origTask.Watch,
Namespace: origTask.Namespace,
FullName: fullName,
}
new.Dir, err = execext.ExpandLiteral(new.Dir)
if err != nil {

18
website/.gitignore vendored
View File

@@ -1,21 +1,9 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
i18n
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vitepress/cache
.vitepress/dist
.task/

View File

1
website/.prettierignore Normal file
View File

@@ -0,0 +1 @@
pnpm-lock.yaml

5
website/.vitepress/components.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -0,0 +1,97 @@
<template>
<div class="author-compact" v-if="author">
<img :src="author.avatar" :alt="author.name" class="author-avatar" />
<div class="author-info">
<div class="author-name-line">
<span class="author-name">{{ author.name }}</span>
<div class="author-socials">
<a
v-for="{ link, icon } in author.links"
:key="link"
:href="link"
target="_blank"
class="social-link"
>
<span :class="`vpi-social-${icon}`"></span>
</a>
</div>
</div>
<span class="author-bio">{{ author.title }}</span>
</div>
</div>
</template>
<script setup>
import { team } from '../team.ts';
import { computed } from 'vue';
const props = defineProps({
author: String
});
const author = computed(() => {
return team.find((m) => m.slug === props.author) || null;
});
</script>
<style scoped>
.author-compact {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 1.5rem 0;
}
.author-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
.author-info {
display: flex;
flex-direction: column;
gap: 0.1rem;
flex: 1;
}
.author-name-line {
display: flex;
align-items: center;
gap: 0.75rem;
}
.author-name {
font-weight: 600;
color: var(--vp-c-text-1);
font-size: 0.95rem;
}
.author-bio {
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.author-socials {
display: flex;
gap: 0.5rem;
}
.social-link {
color: var(--vp-c-text-2);
transition: color 0.2s;
display: flex;
align-items: center;
}
.social-link:hover {
color: var(--vp-c-brand-1);
}
@media (max-width: 768px) {
.author-compact {
margin-bottom: 1rem;
}
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<article class="blog-post">
<div class="post-header">
<h3 class="post-title">
<a :href="url">{{ title }}</a>
</h3>
<div class="post-meta">
<time :datetime="date" class="post-date">
{{ formatDate(date) }}
</time>
</div>
</div>
<div class="post-content">
<div class="post-image" v-if="image">
<img :src="image" :alt="title" />
</div>
<div class="post-text">
<AuthorCard :author="author" />
<p class="post-description">{{ description }}</p>
<div class="post-footer">
<div class="post-tags" v-if="tags?.length">
<strong>Tags:</strong>
<code v-for="tag in tags" :key="tag" class="post-tag">{{
tag
}}</code>
</div>
<a :href="url" class="read-more"> Read more </a>
</div>
</div>
</div>
</article>
</template>
<script setup>
import AuthorCard from './AuthorCard.vue';
const props = defineProps({
title: String,
url: String,
date: String,
author: String,
description: String,
tags: Array,
image: String
});
function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
</script>
<style scoped>
.blog-post {
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 2rem;
margin-bottom: 2rem;
}
.blog-post:last-child {
border-bottom: none;
margin-bottom: 0;
}
.post-title {
margin: 0 0 0.5rem 0;
font-size: 1.8rem;
font-weight: 600;
}
.post-title a {
transition: color 0.2s;
}
.post-title a:hover {
color: var(--vp-c-brand-1);
}
.post-date {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.post-content {
display: flex;
gap: 2rem;
align-items: flex-start;
}
.post-image {
flex-shrink: 0;
width: 300px;
}
.post-image img {
width: 100%;
height: auto;
border-radius: 8px;
object-fit: cover;
aspect-ratio: 16 / 9;
}
.post-text {
flex: 1;
}
.post-description {
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 1.5rem 0;
font-size: 1.05rem;
}
.post-footer {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.post-tags {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.post-tag {
background: var(--vp-c-default-soft);
color: var(--vp-c-text-2);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
margin-left: 0.5rem;
font-family: var(--vp-font-family-mono);
}
.read-more {
color: var(--vp-c-brand-1);
text-decoration: none;
font-weight: 500;
transition: all 0.2s;
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-brand-1);
border-radius: 6px;
font-size: 0.9rem;
}
.read-more:hover {
background: var(--vp-c-brand-1);
color: white;
}
/* Responsive */
@media (max-width: 768px) {
.post-content {
flex-direction: column;
gap: 1rem;
}
.post-image {
width: 100%;
}
.post-title {
font-size: 1.5rem;
}
.post-footer {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { VPHomeSponsors } from 'vitepress/theme';
import { sponsors } from '../sponsors';
</script>
<template>
<div class="content">
<div class="content-container">
<main class="main">
<VPHomeSponsors
v-if="sponsors"
message="Task is free and open source, made possible by wonderful sponsors."
:data="sponsors"
/>
</main>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,245 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme';
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';
import VPSocialLinks from 'vitepress/dist/client/theme-default/components/VPSocialLinks.vue';
interface Props {
size?: 'small' | 'medium';
member: TeamMember;
}
interface TeamMember extends DefaultTheme.TeamMember {
icon?: string;
}
withDefaults(defineProps<Props>(), {
size: 'medium'
});
</script>
<template>
<article class="VPTeamMembersItem" :class="[size]">
<div class="profile">
<figure class="avatar">
<img class="avatar-img" :src="member.avatar" :alt="member.name" />
</figure>
<div class="data">
<h1 class="name">
<img :src="member.icon" alt="profile-icon" />
{{ member.name }}
</h1>
<p v-if="member.title || member.org" class="affiliation">
<span v-if="member.title" class="title">
{{ member.title }}
</span>
<span v-if="member.title && member.org" class="at"> @ </span>
<VPLink
v-if="member.org"
class="org"
:class="{ link: member.orgLink }"
:href="member.orgLink"
no-icon
>
{{ member.org }}
</VPLink>
</p>
<p v-if="member.desc" class="desc" v-html="member.desc" />
<div v-if="member.links" class="links">
<VPSocialLinks :links="member.links" :me="false" />
</div>
</div>
</div>
<div v-if="member.sponsor" class="sp">
<VPLink class="sp-link" :href="member.sponsor" no-icon>
<span class="vpi-heart sp-icon" /> {{ member.actionText || 'Sponsor' }}
</VPLink>
</div>
</article>
</template>
<style scoped>
.VPTeamMembersItem {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 12px;
width: 100%;
height: 100%;
overflow: hidden;
}
.VPTeamMembersItem.small .profile {
padding: 32px;
}
.VPTeamMembersItem.small .data {
padding-top: 20px;
}
.VPTeamMembersItem.small .avatar {
width: 64px;
height: 64px;
}
.VPTeamMembersItem.small .name {
line-height: 24px;
font-size: 16px;
}
.VPTeamMembersItem.small .affiliation {
padding-top: 4px;
line-height: 20px;
font-size: 14px;
}
.VPTeamMembersItem.small .desc {
padding-top: 12px;
line-height: 20px;
font-size: 14px;
}
.VPTeamMembersItem.small .links {
margin: 0 -16px -20px;
padding: 10px 0 0;
}
.VPTeamMembersItem.medium .profile {
padding: 48px 32px;
}
.VPTeamMembersItem.medium .data {
padding-top: 24px;
text-align: center;
}
.VPTeamMembersItem.medium .avatar {
width: 96px;
height: 96px;
}
.VPTeamMembersItem.medium .name {
letter-spacing: 0.15px;
line-height: 28px;
font-size: 20px;
}
.VPTeamMembersItem.medium .affiliation {
padding-top: 4px;
font-size: 16px;
}
.VPTeamMembersItem.medium .desc {
padding-top: 16px;
max-width: 288px;
font-size: 16px;
}
.VPTeamMembersItem.medium .links {
margin: 0 -16px -12px;
padding: 16px 12px 0;
}
.VPTeamMembersItem .profile .name {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
}
.VPTeamMembersItem .profile .name img {
display: inline-block;
height: 22px;
background-repeat: no-repeat;
}
.profile {
flex-grow: 1;
background-color: var(--vp-c-bg-soft);
}
.data {
text-align: center;
}
.avatar {
position: relative;
flex-shrink: 0;
margin: 0 auto;
border-radius: 50%;
box-shadow: var(--vp-shadow-3);
}
.avatar-img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 50%;
object-fit: cover;
}
.name {
margin: 0;
font-weight: 600;
}
.affiliation {
margin: 0;
font-weight: 500;
color: var(--vp-c-text-2);
}
.org.link {
color: var(--vp-c-text-2);
transition: color 0.25s;
}
.org.link:hover {
color: var(--vp-c-brand-1);
}
.desc {
margin: 0 auto;
}
.desc :deep(a) {
font-weight: 500;
color: var(--vp-c-brand-1);
text-decoration-style: dotted;
transition: color 0.25s;
}
.links {
display: flex;
justify-content: center;
height: 56px;
}
.sp-link {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 16px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-sponsor);
background-color: var(--vp-c-bg-soft);
transition:
color 0.25s,
background-color 0.25s;
}
.sp .sp-link.link:hover,
.sp .sp-link.link:focus {
outline: none;
color: var(--vp-c-white);
background-color: var(--vp-c-sponsor);
}
.sp-icon {
margin-right: 8px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
import { VPBadge } from 'vitepress/theme';
</script>
<template>
<VPBadge type="info"> <slot />+ </VPBadge>
</template>

View File

@@ -0,0 +1,333 @@
import { defineConfig } from 'vitepress';
import githubLinksPlugin from './plugins/github-links';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';
import {
groupIconMdPlugin,
groupIconVitePlugin,
localIconLoader
} from 'vitepress-plugin-group-icons';
import { team } from './team.ts';
import { taskDescription, taskName } from './meta.ts';
import { fileURLToPath, URL } from 'node:url';
const version = readFileSync(
resolve(__dirname, '../../internal/version/version.txt'),
'utf8'
).trim();
const urlVersion =
process.env.NODE_ENV === 'development'
? {
current: 'https://taskfile.dev/',
next: 'http://localhost:3002/'
}
: {
current: 'https://taskfile.dev/',
next: 'https://next.taskfile.dev/'
};
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: taskName,
description: taskDescription,
lang: 'en-US',
head: [
[
'link',
{
rel: 'icon',
type: 'image/x-icon',
href: '/img/favicon.icon',
sizes: '48x48'
}
],
[
'link',
{
rel: 'icon',
sizes: 'any',
type: 'image/svg+xml',
href: '/img/logo.svg'
}
],
[
'link',
{
rel: 'canonical',
href: 'https://taskfile.dev/'
}
],
[
'meta',
{ name: 'author', content: `${team.map((c) => c.name).join(', ')}` }
],
[
'meta',
{
name: 'keywords',
content:
'task runner, build tool, taskfile, yaml build tool, go task runner, make alternative, cross-platform build tool, makefile alternative, automation tool, ci cd pipeline, developer productivity, build automation, command line tool, go binary, yaml configuration'
}
],
[
"script",
{
defer: "",
src: "https://umami.taskfile.dev/script.js",
"data-website-id": "084030b0-0e3f-4891-8d2a-0c12c40f5933"
}
]
],
srcDir: 'src',
cleanUrls: true,
markdown: {
config: (md) => {
md.use(githubLinksPlugin, {
baseUrl: 'https://github.com',
repo: 'go-task/task'
});
md.use(tabsMarkdownPlugin);
md.use(groupIconMdPlugin);
}
},
vite: {
plugins: [
groupIconVitePlugin({
customIcon: {
'.taskrc.yml': localIconLoader(
import.meta.url,
'./theme/icons/task.svg'
),
'Taskfile.yml': localIconLoader(
import.meta.url,
'./theme/icons/task.svg'
)
}
})
],
resolve: {
alias: [
{
find: /^.*\/VPTeamMembersItem\.vue$/,
replacement: fileURLToPath(
new URL('./components/VPTeamMembersItem.vue', import.meta.url)
)
}
]
}
},
themeConfig: {
logo: '/img/logo.svg',
carbonAds: {
code: 'CESI65QJ',
placement: 'taskfiledev'
},
search: {
provider: 'algolia',
options: {
appId: '7IZIJ13AI7',
apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc',
indexName: 'taskfile'
}
},
nav: [
{ text: 'Home', link: '/' },
{
text: 'Docs',
link: '/docs/guide',
activeMatch: '^/docs'
},
{ text: 'Blog', link: '/blog', activeMatch: '^/blog' },
{ text: 'Donate', link: '/donate' },
{ text: 'Team', link: '/team' },
{
text: process.env.NODE_ENV === 'development' ? 'Next' : `v${version}`,
items: [
{
items: [
{
text: `v${version}`,
link: urlVersion.current
},
{
text: 'Next',
link: urlVersion.next
}
]
}
]
}
],
sidebar: {
'/blog/': [
{
text: '2025',
collapsed: false,
items: [
{
text: 'Built-in Core Utilities',
link: '/blog/windows-core-utils'
}
]
},
{
text: '2024',
collapsed: false,
items: [
{
text: 'Any Variables',
link: '/blog/any-variables'
}
]
},
{
text: '2023',
collapsed: false,
items: [
{
text: 'Introducing Experiments',
link: '/blog/task-in-2023'
}
]
}
],
'/': [
{
text: 'Installation',
link: '/docs/installation'
},
{
text: 'Getting Started',
link: '/docs/getting-started'
},
{
text: 'Guide',
link: '/docs/guide'
},
{
text: 'Reference',
collapsed: true,
items: [
{
text: 'Taskfile Schema',
link: '/docs/reference/schema'
},
{
text: 'Environment',
link: '/docs/reference/environment'
},
{
text: 'Configuration',
link: '/docs/reference/config'
},
{
text: 'CLI',
link: '/docs/reference/cli'
},
{
text: 'Templating',
link: '/docs/reference/templating'
},
{
text: 'Package API',
link: '/docs/reference/package'
}
]
},
{
text: 'Experiments',
collapsed: true,
link: '/docs/experiments/',
items: [
{
text: 'Env Precedence (#1038)',
link: '/docs/experiments/env-precedence'
},
{
text: 'Gentle Force (#1200)',
link: '/docs/experiments/gentle-force'
},
{
text: 'Remote Taskfiles (#1317)',
link: '/docs/experiments/remote-taskfiles'
}
]
},
{
text: 'Deprecations',
collapsed: true,
link: '/docs/deprecations/',
items: [
{
text: 'Completion Scripts',
link: '/docs/deprecations/completion-scripts'
},
{
text: 'Template Functions',
link: '/docs/deprecations/template-functions'
},
{
text: 'Version 2 Schema (#1197)',
link: '/docs/deprecations/version-2-schema'
}
]
},
{
text: 'Taskfile Versions',
link: '/docs/taskfile-versions'
},
{
text: 'Integrations',
link: '/docs/integrations'
},
{
text: 'Community',
link: '/docs/community'
},
{
text: 'Style Guide',
link: '/docs/styleguide'
},
{
text: 'Contributing',
link: '/docs/contributing'
},
{
text: 'Releasing',
link: '/docs/releasing'
},
{
text: 'Changelog',
link: '/docs/changelog'
},
{
text: 'FAQ',
link: '/docs/faq'
}
],
// Hacky to disable sidebar for these pages
'/donate': [],
'/team': []
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/go-task/task' },
{ icon: 'discord', link: 'https://discord.gg/6TY36E39UK' },
{ icon: 'x', link: 'https://twitter.com/taskfiledev' },
{ icon: 'bluesky', link: 'https://bsky.app/profile/taskfile.dev' },
{ icon: 'mastodon', link: 'https://fosstodon.org/@task' }
],
footer: {
message:
'Built with <a target="_blank" href="https://www.netlify.com">Netlify</a>'
}
},
sitemap: {
hostname: 'https://taskfile.dev'
}
});

View File

@@ -0,0 +1,5 @@
export const taskName = 'Task';
export const taskDescription =
'A fast, cross-platform build tool inspired by Make, designed for modern workflows.';
export const ogUrl = 'https://taskfile.dev/';

View File

@@ -0,0 +1,63 @@
import type MarkdownIt from 'markdown-it';
interface PluginOptions {
repo: string;
}
function githubLinksPlugin(
md: MarkdownIt,
options: PluginOptions = {} as PluginOptions
): void {
const baseUrl = 'https://github.com';
const { repo } = options;
md.core.ruler.after('inline', 'github-links', (state): void => {
const tokens = state.tokens;
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].type === 'inline' && tokens[i].children) {
const inlineTokens = tokens[i].children!;
for (let j = 0; j < inlineTokens.length; j++) {
if (inlineTokens[j].type === 'text') {
let text: string = inlineTokens[j].content!;
const protectedRefs: string[] = [];
let protectIndex: number = 0;
text = text.replace(
/[\w.-]+\/[\w.-]+#(\d+)/g,
(match: string): string => {
const placeholder: string = `__PROTECTED_${protectIndex}__`;
protectedRefs[protectIndex] = match;
protectIndex++;
return placeholder;
}
);
text = text.replace(
/#(\d+)/g,
`<a href="${baseUrl}/${repo}/issues/$1" target="_blank" class="github-pr-link">#$1</a>`
);
text = text.replace(
/@([a-zA-Z0-9_-]+)(?![\w@.])/g,
`<a href="${baseUrl}/$1" target="_blank" class="github-user-mention">@$1</a>`
);
protectedRefs.forEach((ref: string, index: number): void => {
text = text.replace(`__PROTECTED_${index}__`, ref);
});
if (text !== inlineTokens[j].content) {
inlineTokens[j].content = text;
inlineTokens[j].type = 'html_inline';
}
}
}
}
}
});
}
export default githubLinksPlugin;

View File

@@ -0,0 +1,13 @@
export const sponsors = [
{
tier: 'Gold Sponsors',
size: 'big',
items: [
{
name: 'devowl',
url: 'https://devowl.io/',
img: '/img/devowl.io.svg'
}
]
}
];

View File

@@ -0,0 +1,45 @@
export const team = [
{
slug: 'andreynering',
avatar: 'https://www.github.com/andreynering.png',
name: 'Andrey Nering',
icon: '/img/flag-brazil.svg',
title: 'Creator & Maintainer',
sponsor: 'https://github.com/sponsors/andreynering',
links: [
{ icon: 'github', link: 'https://github.com/andreynering' },
{ icon: 'discord', link: 'https://discord.com/users/310141681926275082' },
{ icon: 'x', link: 'https://x.com/andreynering' },
{
icon: 'bluesky',
link: 'https://bsky.app/profile/andreynering.bsky.social'
},
{ icon: 'mastodon', link: 'https://mastodon.social/@andreynering' }
]
},
{
slug: 'pd93',
avatar: 'https://www.github.com/pd93.png',
name: 'Pete Davison',
icon: '/img/flag-wales.svg',
title: 'Maintainer',
sponsor: 'https://github.com/sponsors/pd93',
links: [
{ icon: 'github', link: 'https://github.com/pd93' },
{ icon: 'bluesky', link: 'https://bsky.app/profile/pd93.uk' }
]
},
{
slug: 'vmaerten',
avatar: 'https://www.github.com/vmaerten.png',
name: 'Valentin Maerten',
icon: '/img/flag-france.svg',
title: 'Maintainer',
sponsor: 'https://github.com/sponsors/vmaerten',
links: [
{ icon: 'github', link: 'https://github.com/vmaerten' },
{ icon: 'x', link: 'https://x.com/v_maerten' },
{ icon: 'bluesky', link: 'https://bsky.app/profile/vmaerten.bsky.social' }
]
}
];

View File

@@ -0,0 +1,147 @@
:root {
--ifm-color-primary: #43aba2;
--vp-home-hero-name-color: var(--ifm-color-primary);
--vp-c-brand-1: var(--ifm-color-primary);
--vp-c-brand-2: var(--ifm-color-primary);
--vp-c-brand-3: var(--ifm-color-primary);
--vp-icon-info: #3b82f6;
--vp-icon-tip: #10b981;
--vp-icon-warning: #f59e0b;
--vp-icon-danger: #ef4444;
--vp-icon-details: #6b7280;
}
.dark {
--vp-icon-info: #93c5fd;
--vp-icon-tip: #34d399;
--vp-icon-warning: #fbbf24;
--vp-icon-danger: #f87171;
--vp-icon-details: #9ca3af;
}
img[src*='shields.io'] {
display: inline;
vertical-align: text-bottom;
}
img[src*='custom-icon-badges.demolab.com'] {
display: inline;
height: 1em;
vertical-align: text-bottom;
}
.github-user-mention {
font-weight: 700 !important;
}
.vp-doc .blog-post:first-of-type {
margin-top: 2rem;
}
.blog-post {
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.blog-post:nth-of-type(1) {
animation-delay: 0.1s;
}
.blog-post:nth-of-type(2) {
animation-delay: 0.2s;
}
.blog-post:nth-of-type(3) {
animation-delay: 0.3s;
}
.custom-block .custom-block-title::before {
content: '';
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: middle;
flex-shrink: 0;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
}
.custom-block.info .custom-block-title::before {
background-color: var(--vp-icon-info);
-webkit-mask-image: url('./icons/info.svg');
mask-image: url('./icons/info.svg');
}
.custom-block.tip .custom-block-title::before {
background-color: var(--vp-icon-tip);
-webkit-mask-image: url('./icons/tip.svg');
mask-image: url('./icons/tip.svg');
}
.custom-block.warning .custom-block-title::before {
background-color: var(--vp-icon-warning);
-webkit-mask-image: url('./icons/warning.svg');
mask-image: url('./icons/warning.svg');
}
.custom-block.danger .custom-block-title::before {
background-color: var(--vp-icon-danger);
-webkit-mask-image: url('./icons/danger.svg');
mask-image: url('./icons/danger.svg');
}
.custom-block.details[open] summary::before {
transform: rotate(90deg);
}
.custom-block .custom-block-title {
display: flex;
align-items: center;
}
@supports not (mask-image: none) {
.custom-block .custom-block-title::before,
.custom-block.details summary::before {
font-size: 18px;
width: auto;
height: auto;
background: none !important;
-webkit-mask: none !important;
mask: none !important;
}
.custom-block.info .custom-block-title::before {
content: '';
}
.custom-block.tip .custom-block-title::before {
content: '💡';
}
.custom-block.warning .custom-block-title::before {
content: '⚠️';
}
.custom-block.danger .custom-block-title::before {
content: '🔥';
}
}
.VPTeamPage > .VPTeamPageTitle {
padding-top: 0
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<path d="M7.998 14.5c2.832 0 5-1.98 5-4.5 0-1.463-.68-2.19-1.879-3.383l-.036-.037c-1.013-1.008-2.3-2.29-2.834-4.434-.322.256-.63.579-.864.953-.432.696-.621 1.58-.046 2.73.473.947.67 2.284-.278 3.232-.61.61-1.545.84-2.403.633a2.788 2.788 0 0 1-1.436-.874A3.21 3.21 0 0 0 3 10c0 2.53 2.164 4.5 4.998 4.5zM9.533.753C9.496.34 9.16.009 8.77.146 7.035.75 4.34 3.187 5.997 6.5c.344.689.285 1.218.003 1.5-.419.419-1.54.487-2.04-.832-.173-.454-.659-.762-1.035-.454C2.036 7.44 1.5 8.702 1.5 10c0 3.512 2.998 6 6.498 6s6.5-2.5 6.5-6c0-2.137-1.128-3.26-2.312-4.438-1.19-1.184-2.436-2.425-2.653-4.81z"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

View File

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 435 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/>
</svg>

After

Width:  |  Height:  |  Size: 796 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -0,0 +1,24 @@
import DefaultTheme from 'vitepress/theme';
import type { Theme } from 'vitepress';
import './custom.css';
import HomePage from '../components/HomePage.vue';
import AuthorCard from '../components/AuthorCard.vue';
import BlogPost from '../components/BlogPost.vue';
import Version from '../components/Version.vue';
import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client';
import { h } from 'vue';
import 'virtual:group-icons.css';
export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'home-features-after': () => h(HomePage)
});
},
enhanceApp({ app }) {
app.component('AuthorCard', AuthorCard);
app.component('BlogPost', BlogPost);
app.component('Version', Version);
enhanceAppWithTabs(app);
}
} satisfies Theme;

View File

@@ -1,29 +1,35 @@
version: "3"
version: '3'
tasks:
yarn:install:
desc: Setup Docusaurus locally
install:
desc: Setup VitePress locally
cmds:
- yarn install
- pnpm install
sources:
- package.json
- yarn.lock
- pnpm-lock.yaml
default:
desc: Start website
deps: [yarn:install]
deps: [install]
aliases: [s, start]
vars:
HOST: '{{default "0.0.0.0" .HOST}}'
PORT: '{{default "3001" .PORT}}'
cmds:
- npx docusaurus start --no-open --host={{.HOST}} --port={{.PORT}}
- pnpm dev --host={{.HOST}} --port={{.PORT}}
lint:
desc: Lint website
deps: [install]
cmds:
- pnpm lint
build:
desc: Build website
deps: [yarn:install]
deps: [install]
cmds:
- npx docusaurus build
- pnpm build
preview:
desc: Preview Website
@@ -33,20 +39,19 @@ tasks:
HOST: '{{default "localhost" .HOST}}'
PORT: '{{default "3001" .PORT}}'
cmds:
- npx docusaurus serve --no-open --host={{.HOST}} --port={{.PORT}}
- pnpm preview --host={{.HOST}} --port={{.PORT}}
clean:
desc: Clean temp directories
cmds:
- rm -rf ./build
- rm -rf ./vitepress/dist
deploy:
desc: Build and deploy Docusaurus
summary: Requires GIT_USER and GIT_PASS envs to be previous set
deploy:next:
desc: Build and deploy next.taskfile.dev
cmds:
- npx docusaurus deploy
- pnpm netlify deploy --prod --site=4e13dfcf-fc0d-4bec-ad60-b918a8dc3942
upgrade:
desc: Upgrade Docusaurus
deploy:prod:
desc: Build and deploy taskfile.dev
cmds:
- yarn upgrade @docusaurus/core@latest @docusaurus/preset-classic@latest @docusaurus/module-type-aliases@latest @docusaurus/tsconfig@latest @docusaurus/types@latest
- pnpm netlify deploy --prod --site=e625bc6a-1cd3-465d-ad30-7bbddaeb4f31

View File

@@ -1,3 +0,0 @@
export default {
presets: ['@docusaurus/core/lib/babel/preset'],
};

View File

@@ -1,10 +0,0 @@
andreynering:
name: Andrey Nering
title: Maintainer of Task
url: https://github.com/andreynering
image_url: https://github.com/andreynering.png
pd93:
name: Pete Davison
title: Maintainer of Task
url: https://github.com/pd93
image_url: https://github.com/pd93.png

View File

@@ -1,7 +0,0 @@
export const GITHUB_URL = 'https://github.com/go-task/task';
export const TWITTER_URL = 'https://twitter.com/taskfiledev';
export const BLUESKY_URL = 'https://bsky.app/profile/taskfile.dev';
export const MASTODON_URL = 'https://fosstodon.org/@task';
export const DISCORD_URL = 'https://discord.gg/6TY36E39UK';
export const STACK_OVERFLOW = 'https://stackoverflow.com/questions/tagged/taskfile';
export const ANSWER_OVERFLOW = 'https://www.answeroverflow.com/c/974121106208354339';

View File

@@ -1,38 +0,0 @@
---
slug: /community/
sidebar_position: 10
---
# Community
Some of the work to improve the Task ecosystem is done by the community, be it
installation methods or integrations with code editor. I (the author) am
thankful for everyone that helps me to improve the overall experience.
## Integrations
Many of our integrations are contributed and maintained by the community. You
can view the full list of community integrations
[here](/integrations#community-integrations).
## Installation methods
Some installation methods are maintained by third party:
- [GitHub Actions](https://github.com/arduino/setup-task) by @arduino
- [AUR](https://aur.archlinux.org/packages/go-task-bin) by @carlsmedstad
- [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json)
- [Fedora](https://packages.fedoraproject.org/pkgs/golang-github-task/go-task/)
- [NixOS](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/go-task/default.nix)
- [Conda](https://github.com/conda-forge/go-task-feedstock/)
## More
Also, thanks for all the
[code contributors](https://github.com/go-task/task/graphs/contributors),
[financial contributors](https://opencollective.com/task), all those who
[reported bugs](https://github.com/go-task/task/issues?q=is%3Aissue) and
[answered questions](https://github.com/go-task/task/discussions).
If you know something that is missing in this document, please submit a pull
request.

View File

@@ -1,177 +0,0 @@
---
slug: /contributing/
sidebar_position: 12
---
# Contributing
Contributions to Task are very welcome, but we ask that you read this document
before submitting a PR.
:::note
This document applies to the core [Task][task] repository _and_ [Task for Visual
Studio Code][vscode-task].
:::
## Before you start
- **Check existing work** - Is there an existing PR? Are there issues discussing
the feature/change you want to make? Please make sure you consider/address
these discussions in your work.
- **Backwards compatibility** - Will your change break existing Taskfiles? It is
much more likely that your change will merged if it backwards compatible. Is
there an approach you can take that maintains this compatibility? If not,
consider opening an issue first so that API changes can be discussed before
you invest your time into a PR.
- **Experiments** - If there is no way to make your change backward compatible
then there is a procedure to introduce breaking changes into minor versions.
We call these "[experiments][experiments]". If you're intending to work on an
experiment, then please read the [experiments workflow][experiments-workflow]
document carefully and submit a proposal first.
## 1. Setup
- **Go** - Task is written in [Go][go]. We always support the latest two major
Go versions, so make sure your version is recent enough.
- **Node.js** - [Node.js][nodejs] is used to host Task's documentation server
and is required if you want to run this server locally. It is also required if
you want to contribute to the Visual Studio Code extension.
- **Yarn** - [Yarn][yarn] is the Node.js package manager used by Task.
## 2. Making changes
- **Code style** - Try to maintain the existing code style where possible. Go
code should be formatted and linted by [`golangci-lint`][golangci-lint]. This
wraps the [`gofumpt`][gofumpt] and [`gci`][gci] formatters and a number of
linters. We recommend that you take a look at the [golangci-lint
docs][golangci-lint-docs] for a guide on how to setup your editor to
auto-format your code. Any Markdown or TypeScript files should be formatted
and linted by [Prettier][prettier]. This style is enforced by our CI to ensure
that we have a consistent style across the project. You can use the `task
lint` command to lint the code locally and the `task lint:fix` command to try
to automatically fix any issues that are found. You can also use the `task
fmt` command to auto-format the files if your editor doesn't do it for you.
- **Documentation** - Ensure that you add/update any relevant documentation. See
the [updating documentation](#updating-documentation) section below.
- **Tests** - Ensure that you add/update any relevant tests and that all tests
are passing before submitting the PR. See the [writing tests](#writing-tests)
section below.
### Running your changes
To run Task with working changes, you can use `go run ./cmd/task`. To run a
development build of task against a test Taskfile in `testdata`, you can use
`go run ./cmd/task --dir ./testdata/<my_test_dir> <task_name>`.
To run Task for Visual Studio Code, you can open the project in VSCode and hit
F5 (or whatever you debug keybind is set to). This will open a new VSCode window
with the extension running. Debugging this way is recommended as it will allow
you to set breakpoints and step through the code. Otherwise, you can run
`task package` which will generate a `.vsix` file that can be used to manually
install the extension.
### Updating documentation
Task uses [Docusaurus][docusaurus] to host a documentation server. The code for
this is located in the core Task repository. This can be setup and run locally
by using `task website` (requires `nodejs` & `yarn`). All content is written in
[MDX][mdx] (an extension of Markdown) and is located in the `website/docs`
directory. All Markdown documents should have an 80 character line wrap limit
(enforced by Prettier).
When making a change, consider whether a change to the [Usage Guide](/usage) is
necessary. This document contains descriptions and examples of how to use Task
features. If you're adding a new feature, try to find an appropriate place to
add a new section. If you're updating an existing feature, ensure that the
documentation and any examples are up-to-date. Ensure that any examples follow
the [Taskfile Styleguide](/styleguide).
If you added a new command or flag, ensure that you add it to the [CLI
Reference](/reference/cli). New fields also need to be added to the [Schema
Reference](/reference/schema) and [JSON Schema][json-schema]. The descriptions
for fields in the docs and the schema should match.
### Writing tests
A lot of Task's tests are held in the `task_test.go` file in the project root
and this is where you'll most likely want to add new ones too. Most of these
tests also have a subdirectory in the `testdata` directory where any
Taskfiles/data required to run the tests are stored.
When making a changes, consider whether new tests are required. These tests
should ensure that the functionality you are adding will continue to work in the
future. Existing tests may also need updating if you have changed Task's
behavior.
You may also consider adding unit tests for any new functions you have added.
The unit tests should follow the Go convention of being location in a file named
`*_test.go` in the same package as the code being tested.
## 3. Committing your code
Try to write meaningful commit messages and avoid having too many commits on the
PR. Most PRs should likely have a single commit (although for bigger PRs it may
be reasonable to split it in a few). Git squash and rebase is your friend!
If you're not sure how to format your commit message, check out [Conventional
Commits][conventional-commits]. This style is not enforced, but it is a good way
to make your commit messages more readable and consistent.
## 4. Submitting a PR
- **Describe your changes** - Ensure that you provide a comprehensive
description of your changes.
- **Issue/PR links** - Link any previous work such as related issues or PRs.
Please describe how your changes differ to/extend this work.
- **Examples** - Add any examples or screenshots that you think are useful to
demonstrate the effect of your changes.
- **Draft PRs** - If your changes are incomplete, but you would like to discuss
them, open the PR as a draft and add a comment to start a discussion. Using
comments rather than the PR description allows the description to be updated
later while preserving any discussions.
## FAQ
> I want to contribute, where do I start?
Take a look at the list of [open issues for Task][task-open-issues] or [Task for
Visual Studio Code][vscode-task-open-issues]. We have a [good first
issue][good-first-issue] label for simpler issues that are ideal for first time
contributions.
All kinds of contributions are welcome, whether its a typo fix or a shiny new
feature. You can also contribute by upvoting/commenting on issues, helping to
answer questions or contributing to other [community projects](/community).
> I'm stuck, where can I get help?
If you have questions, feel free to ask them in the `#help` forum channel on our
[Discord server][discord-server] or open a [Discussion][discussion] on GitHub.
---
{/* prettier-ignore-start */}
[experiments]: /experiments
[experiments-workflow]: /experiments#workflow
[task]: https://github.com/go-task/task
[vscode-task]: https://github.com/go-task/vscode-task
[go]: https://go.dev
[gofumpt]: https://github.com/mvdan/gofumpt
[gci]: https://github.com/daixiang0/gci
[golangci-lint]: https://golangci-lint.run
[golangci-lint-docs]: https://golangci-lint.run/welcome/integrations/
[prettier]: https://prettier.io
[nodejs]: https://nodejs.org/en/
[yarn]: https://yarnpkg.com/
[docusaurus]: https://docusaurus.io
[json-schema]: https://github.com/go-task/task/blob/main/website/static/schema.json
[task-open-issues]: https://github.com/go-task/task/issues
[vscode-task-open-issues]: https://github.com/go-task/vscode-task/issues
[good-first-issue]: https://github.com/go-task/task/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
[discord-server]: https://discord.gg/6TY36E39UK
[discussion]: https://github.com/go-task/task/discussions
[conventional-commits]: https://www.conventionalcommits.org
[mdx]: https://mdxjs.com/
{/* prettier-ignore-end */}

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