mirror of
https://github.com/go-task/task.git
synced 2026-05-18 13:15:41 +02:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e176311d | ||
|
|
1c68f0fee4 | ||
|
|
118ef01a69 | ||
|
|
148b090d8e | ||
|
|
28a96d1427 | ||
|
|
47f5e6ab89 | ||
|
|
fe09c01637 | ||
|
|
7ef3164b16 | ||
|
|
b48a32b103 | ||
|
|
2d2c408652 | ||
|
|
c381923d3e | ||
|
|
7bfddaa25a | ||
|
|
5581954fb1 | ||
|
|
c4f708b222 | ||
|
|
27056a9827 | ||
|
|
a35910429c | ||
|
|
9a7e79258c | ||
|
|
8dd3f4b119 | ||
|
|
9ecc8fc878 | ||
|
|
e078261f12 | ||
|
|
bdb3ffddd1 | ||
|
|
a72e70b026 | ||
|
|
c5eea294aa | ||
|
|
0fff404eb8 | ||
|
|
61172fa8da | ||
|
|
a6bc3f51cc | ||
|
|
1af7bf2670 | ||
|
|
d75536bf00 | ||
|
|
ce3e058f89 | ||
|
|
8d0f0b049c | ||
|
|
e619bad4a9 | ||
|
|
e6ea0647d7 | ||
|
|
d1dc271b9a | ||
|
|
f5082f3692 | ||
|
|
30c59bf387 | ||
|
|
38d0fc2c55 | ||
|
|
460e587c66 | ||
|
|
ad5a3166ac | ||
|
|
ddccd1bb61 | ||
|
|
96a690ac2f | ||
|
|
cb07189bab | ||
|
|
7e6577eb5f | ||
|
|
58ab26c4ab | ||
|
|
65d332dfd0 | ||
|
|
5eaf0b2dcd | ||
|
|
56f3735b38 | ||
|
|
23d578ac8c | ||
|
|
1bf850592c | ||
|
|
0be05795b9 | ||
|
|
08a2a91180 | ||
|
|
84cc5e57b0 | ||
|
|
5aa68e47e5 | ||
|
|
15aa4b86af | ||
|
|
114d5e1404 | ||
|
|
8ab5fe0e80 | ||
|
|
c89a6add48 | ||
|
|
888071e234 | ||
|
|
ff2e0f846a | ||
|
|
3c177d3fdc | ||
|
|
41bc490e0f | ||
|
|
f8e3742d11 | ||
|
|
a6100b39f8 | ||
|
|
1275ab1b5b | ||
|
|
0c05dcbe0f | ||
|
|
e9983e299f | ||
|
|
a450f2daea | ||
|
|
c77c8a419b | ||
|
|
a233b52c65 | ||
|
|
0e2c9cc88f | ||
|
|
dd9cec611a | ||
|
|
6985413f93 | ||
|
|
cf77768c82 | ||
|
|
6c3b13b676 | ||
|
|
ad45c7aeb3 | ||
|
|
e4b4d04abd | ||
|
|
a3bdb6c40a | ||
|
|
eb39dd94d0 | ||
|
|
21cd573770 | ||
|
|
281d259e6e | ||
|
|
1cb5daf73e | ||
|
|
3747b2ab7f | ||
|
|
d727ef5393 | ||
|
|
a72b65b3b2 | ||
|
|
ef3b853728 | ||
|
|
f302b50519 | ||
|
|
c243b0ec7e | ||
|
|
32158dac87 | ||
|
|
0a59890a46 | ||
|
|
defbcf6acd | ||
|
|
045d054a5f | ||
|
|
0941de3318 | ||
|
|
b259edeb65 | ||
|
|
35119c12ab | ||
|
|
f6ff775d11 | ||
|
|
5e9851f42f | ||
|
|
51c569ef37 | ||
|
|
1ca432a80d | ||
|
|
e781b3d4e0 | ||
|
|
81ff1cdea0 | ||
|
|
1f2cbfb932 | ||
|
|
4b6c79aca5 | ||
|
|
5739495739 | ||
|
|
9d72fa3250 | ||
|
|
4123ffc780 | ||
|
|
cdafc67bef | ||
|
|
9ee4f21d62 | ||
|
|
133086d647 | ||
|
|
88b095020e | ||
|
|
cc14996b71 | ||
|
|
375106c988 | ||
|
|
6ce6a38899 | ||
|
|
76030c9146 | ||
|
|
a71020eab5 | ||
|
|
6bef2ff8a9 | ||
|
|
413dcd28a8 | ||
|
|
da6f5c66a0 | ||
|
|
6012da7a21 | ||
|
|
46c5eafe35 | ||
|
|
830b745112 | ||
|
|
b52d4e4f40 | ||
|
|
3aaa3223a0 | ||
|
|
a9ff58d0fe | ||
|
|
eeaebaf8c7 | ||
|
|
2213141fcb | ||
|
|
19956889a7 | ||
|
|
4c580ebf18 | ||
|
|
3dccde270a | ||
|
|
53dd0b138a | ||
|
|
ea85909e8b | ||
|
|
6bf6fe7ead | ||
|
|
f39c6352ac | ||
|
|
4294cc92b9 | ||
|
|
40d77156df | ||
|
|
856ba3b8c2 | ||
|
|
0810ef01b0 | ||
|
|
527bbc3bf5 | ||
|
|
912bbcab8e | ||
|
|
aa45491510 | ||
|
|
1e25ceab29 | ||
|
|
a74b0bc679 | ||
|
|
a3fce1c302 | ||
|
|
7958cf50b3 | ||
|
|
b0efbad591 | ||
|
|
30e9c7d4cd | ||
|
|
baa5e2c378 | ||
|
|
cc97e2da1d | ||
|
|
a55e21bbb7 | ||
|
|
8d138a5eea | ||
|
|
635e3f4e7d | ||
|
|
252d549e3f | ||
|
|
182d43e8d8 | ||
|
|
f35e51e4e5 | ||
|
|
fb3c64c46e | ||
|
|
7535467f45 | ||
|
|
3e5cd6cdfd | ||
|
|
dcc060af89 | ||
|
|
55593090fa |
@@ -8,6 +8,6 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
|
||||
[*.{md,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
|
||||
[*.{md,mdx,yml,yaml,json,toml,htm,html,js,css,svg,sh,bash,fish}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
* text=auto
|
||||
*.mdx -linguist-detectable
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
||||
github: [andreynering, pd93]
|
||||
github: [andreynering, pd93, vmaerten]
|
||||
open_collective: task
|
||||
custom: https://taskfile.dev/donate/
|
||||
|
||||
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -1,5 +1,9 @@
|
||||
> Thanks for your pull request, we really appreciate contributions!
|
||||
>
|
||||
> Please understand that it may take some time to be reviewed.
|
||||
>
|
||||
> Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
|
||||
<!--
|
||||
|
||||
Thanks for your pull request, we really appreciate contributions!
|
||||
|
||||
Please understand that it may take some time to be reviewed.
|
||||
|
||||
Also, make sure to follow the [Contribution Guide](https://taskfile.dev/contributing/).
|
||||
|
||||
-->
|
||||
|
||||
48
.github/renovate.json
vendored
Normal file
48
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
"group:allNonMajor",
|
||||
"schedule:monthly"
|
||||
],
|
||||
"mode": "full",
|
||||
"reviewers": ["team:developer"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "Github Action",
|
||||
"labels": ["area: github actions", "area: dependencies"],
|
||||
"matchPackageNames": [
|
||||
"*"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["npm", "nvm"],
|
||||
"groupName": "Website",
|
||||
"labels": ["lang: javascript", "area: dependencies"],
|
||||
"matchPackageNames": [
|
||||
"*"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["gomod"],
|
||||
"groupName": "golang",
|
||||
"labels": ["lang: go", "area: dependencies"],
|
||||
"matchPackageNames": [
|
||||
"*"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
issue-awaiting-response:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-closed.yml
vendored
2
.github/workflows/issue-closed.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
issue-closed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
14
.github/workflows/issue-experiment.yml
vendored
14
.github/workflows/issue-experiment.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} proposed', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} draft', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} candidate', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} stable', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} released', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} abandoned', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
if: github.event.label.name == format('experiment{0} superseded', ':')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-needs-triage.yml
vendored
2
.github/workflows/issue-needs-triage.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
issue-needs-triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
|
||||
27
.github/workflows/lint.yml
vendored
27
.github/workflows/lint.yml
vendored
@@ -13,19 +13,19 @@ jobs:
|
||||
name: Lint
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.55.2
|
||||
version: v1.60.1
|
||||
|
||||
lint-jsonschema:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -34,10 +34,25 @@ jobs:
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install check-jsonschema
|
||||
run: python -m pip install 'check-jsonschema==0.27.3'
|
||||
|
||||
- name: check-jsonschema (metaschema)
|
||||
run: check-jsonschema --check-metaschema website/static/schema.json
|
||||
check_doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files in the docs folder
|
||||
id: changed-files-specific
|
||||
uses: tj-actions/changed-files@v45
|
||||
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.')
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -10,15 +10,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.x
|
||||
go-version: 1.22.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -13,18 +13,18 @@ jobs:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
- name: Set up Go ${{matrix.go-version}}
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
|
||||
38
.github/workflows/upload-source-documents.yml
vendored
38
.github/workflows/upload-source-documents.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Upload Source Documents
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_files_to_crowdin:
|
||||
name: Push files to Crowdin
|
||||
if: github.repository == 'go-task/task'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Verify changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: |
|
||||
website/docs
|
||||
website/blog
|
||||
website/i18n/en
|
||||
website/src/pages
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload source documents
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: task crowdin:push
|
||||
env:
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
working-directory: ./website
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,8 +24,7 @@ dist/
|
||||
|
||||
# editors
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/*-sample.json
|
||||
.vscode/settings.json
|
||||
.fleet/
|
||||
|
||||
# exuberant ctags
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
version: 2
|
||||
|
||||
builds:
|
||||
- binary: task
|
||||
main: ./cmd/task
|
||||
@@ -11,11 +14,16 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- riscv64
|
||||
goarm:
|
||||
- '6'
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: '386'
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
@@ -41,7 +49,7 @@ release:
|
||||
draft: true
|
||||
|
||||
snapshot:
|
||||
name_template: "{{.Tag}}"
|
||||
version_template: "{{.Version}}"
|
||||
|
||||
checksum:
|
||||
name_template: "task_checksums.txt"
|
||||
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"golang.go",
|
||||
"task.vscode-task"
|
||||
]
|
||||
}
|
||||
114
CHANGELOG.md
114
CHANGELOG.md
@@ -1,5 +1,117 @@
|
||||
# Changelog
|
||||
|
||||
## v3.40.0 - 2024-11-05
|
||||
|
||||
- Fixed output of some functions (e.g. `splitArgs`/`splitLines`) not working in
|
||||
for loops (#1822, #1823 by @stawii).
|
||||
- Added a new `TASK_OFFLINE` environment variable to configure the `--offline`
|
||||
flag and expose it as a special variable in the templating system (#1470,
|
||||
#1716 by @vmaerten and @pd93).
|
||||
- Fixed a bug where multiple remote includes caused all prompts to display
|
||||
without waiting for user input (#1832, #1833 by @vmaerten and @pd93).
|
||||
- When using the
|
||||
"[Remote Taskfiles](https://taskfile.dev/experiments/remote-taskfiles/)".
|
||||
experiment, you can now include Taskfiles from Git repositories (#1652 by
|
||||
@vmaerten).
|
||||
- Improved the error message when a dotenv file cannot be parsed (#1842 by
|
||||
@pbitty).
|
||||
- Fix issue with directory when using the remote experiment (#1757 by @pbitty).
|
||||
- Fixed an issue where a special variable was used in combination with a dotenv
|
||||
file (#1232, #1810 by @vmaerten).
|
||||
- Refactor the way Task reads Taskfiles to improve readability (#1771 by
|
||||
@pbitty).
|
||||
- Added a new option to ensure variable is within the list of values (#1827 by
|
||||
@vmaerten).
|
||||
- Allow multiple prompts to be specified for a task (#1861, #1866 by @mfbmina).
|
||||
- Added new template function: `numCPU`, which returns the number of logical
|
||||
CPUs usable (#1890, #1887 by @Amoghrd).
|
||||
- Fixed a bug where non-nil, empty dynamic variables are returned as an empty
|
||||
interface (#1903, #1904 by @pd93).
|
||||
|
||||
## v3.39.2 - 2024-09-19
|
||||
|
||||
- Fix dynamic variables not working properly for a defer: statement (#1803,
|
||||
#1818 by @vmaerten).
|
||||
|
||||
## v3.39.1 - 2024-09-18
|
||||
|
||||
- Added Renovate configuration to automatically create PRs to keep dependencies
|
||||
up to date (#1783 by @vmaerten).
|
||||
- Fixed a bug where the help was displayed twice (#1805, #1806 by @vmaerten).
|
||||
- Fixed a bug where ZSH and PowerShell completions did not work when using the
|
||||
recommended method. (#1813, #1809 by @vmaerten and @shirayu)
|
||||
- Fix variables not working properly for a `defer:` statement (#1803, #1814 by
|
||||
@vmaerten and @andreynering).
|
||||
|
||||
## v3.39.0 - 2024-09-07
|
||||
|
||||
- Added
|
||||
[Env Precedence Experiment](https://taskfile.dev/experiments/env-precedence)
|
||||
(#1038, #1633 by @vmaerten).
|
||||
- Added a CI lint job to ensure that the docs are updated correctly (#1719 by
|
||||
@vmaerten).
|
||||
- Updated minimum required Go version to 1.22 (#1758 by @pd93).
|
||||
- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes
|
||||
with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).
|
||||
- Expose a new `ALIAS` special variable, which will contain the alias used to
|
||||
call the current task. Falls back to the task name. (#1764 by @DanStory).
|
||||
- Fixed `TASK_REMOTE_DIR` environment variable not working when the path was
|
||||
absolute. (#1715 by @vmaerten).
|
||||
- Added an option to declare an included Taskfile as flattened (#1704 by
|
||||
@vmaerten).
|
||||
- Added a new
|
||||
[`--completion` flag](https://taskfile.dev/installation/#setup-completions) to
|
||||
output completion scripts for various shells (#293, #1157 by @pd93).
|
||||
- This is now the preferred way to install completions.
|
||||
- The completion scripts in the `completion` directory
|
||||
[are now deprecated](https://taskfile.dev/deprecations/completion-scripts/).
|
||||
- Added the ability to
|
||||
[loop over a matrix of values](https://taskfile.dev/usage/#looping-over-a-matrix)
|
||||
(#1766, #1767, #1784 by @pd93).
|
||||
- Fixed a bug in fish completion where aliases were not displayed (#1781, #1782
|
||||
by @vmaerten).
|
||||
- Fixed panic when having a flattened included Taskfile that contains a
|
||||
`default` task (#1777, #1778 by @vmaerten).
|
||||
- Optimized file existence checks for remote Taskfiles (#1713 by @vmaerten).
|
||||
|
||||
## v3.38.0 - 2024-06-30
|
||||
|
||||
- Added `TASK_EXE` special variable (#1616, #1624 by @pd93 and @andreynering).
|
||||
- Some YAML parsing errors will now show in a more user friendly way (#1619 by
|
||||
@pd93).
|
||||
- Prefixed outputs will now be colorized by default (#1572 by
|
||||
@AlexanderArvidsson)
|
||||
- [References](https://taskfile.dev/usage/#referencing-other-variables) are now
|
||||
generally available (no experiments required) (#1654 by @pd93).
|
||||
- Templating functions can now be used in references (#1645, #1654 by @pd93).
|
||||
- Added a new
|
||||
[templating reference page](https://taskfile.dev/reference/templating/) to the
|
||||
documentation (#1614, #1653 by @pd93).
|
||||
- If using the
|
||||
[Map Variables experiment (1)](https://taskfile.dev/experiments/map-variables/?proposal=1),
|
||||
references are available by
|
||||
[prefixing a string with a `#`](https://taskfile.dev/experiments/map-variables/?proposal=1#references)
|
||||
(#1654 by @pd93).
|
||||
- If using the
|
||||
[Map Variables experiment (2)](https://taskfile.dev/experiments/map-variables/?proposal=2),
|
||||
the `yaml` and `json` keys are no longer available (#1654 by @pd93).
|
||||
- Added a new `TASK_REMOTE_DIR` environment variable to configure where cached
|
||||
remote Taskfiles are stored (#1661 by @vmaerten).
|
||||
- Added a new `--clear-cache` flag to clear the cache of remote Taskfiles (#1639
|
||||
by @vmaerten).
|
||||
- Improved the readability of cached remote Taskfile filenames (#1636 by
|
||||
@vmaerten).
|
||||
- Starting releasing a binary for the `riscv64` architecture on Linux (#1699 by
|
||||
@mengzhuo).
|
||||
- Added `CLI_SILENT` and `CLI_VERBOSE` variables (#1480, #1669 by @Vince-Smith).
|
||||
- Fixed a couple of bugs with the `prompt:` feature (#1657 by @pd93).
|
||||
- Fixed JSON Schema to disallow invalid properties (#1657 by @pd93).
|
||||
- Fixed version checks not working as intended (#872, #1663 by @vmaerten).
|
||||
- Fixed a bug where included tasks were run multiple times even if `run: once`
|
||||
was set (#852, #1655 by @pd93).
|
||||
- Fixed some bugs related to column formatting in the terminal (#1350, #1637,
|
||||
#1656 by @vmaerten).
|
||||
|
||||
## v3.37.2 - 2024-05-12
|
||||
|
||||
- Fixed a bug where an empty Taskfile would cause a panic (#1648 by @pd93).
|
||||
@@ -20,7 +132,7 @@
|
||||
- Refactored how Task reads, parses and merges Taskfiles using a DAG (#1563,
|
||||
#1607 by @pd93).
|
||||
- Fix a bug which stopped tasks from using `stdin` as input (#1593, #1623 by
|
||||
@pd03).
|
||||
@pd93).
|
||||
- Fix error when a file or directory in the project contained a special char
|
||||
like `&`, `(` or `)` (#1551, #1584 by @andreynering).
|
||||
- Added alias `q` for template function `shellQuote` (#1601, #1603 by @vergenzt)
|
||||
|
||||
50
Taskfile.yml
50
Taskfile.yml
@@ -27,7 +27,7 @@ tasks:
|
||||
- go install -v ./cmd/task
|
||||
|
||||
generate:
|
||||
desc: Runs Go generate to create mocks
|
||||
desc: Runs Mockery to create mocks
|
||||
aliases: [gen, g]
|
||||
deps: [install:mockery]
|
||||
sources:
|
||||
@@ -57,6 +57,7 @@ tasks:
|
||||
|
||||
clean:
|
||||
desc: Cleans temp files and folders
|
||||
aliases: [clear]
|
||||
cmds:
|
||||
- rm -rf dist/
|
||||
- rm -rf tmp/
|
||||
@@ -123,10 +124,53 @@ tasks:
|
||||
cmds:
|
||||
- go install github.com/goreleaser/goreleaser@latest
|
||||
|
||||
release:
|
||||
release:*:
|
||||
desc: Prepare the project for a new release
|
||||
summary: |
|
||||
This task will do the following:
|
||||
|
||||
- Update the version and date in the CHANGELOG.md file
|
||||
- Update the version in the package.json and package-lock.json files
|
||||
- Copy the latest docs to the "current" version on the website
|
||||
- Commit the changes
|
||||
- Create a new tag
|
||||
- Push the commit/tag to the repository
|
||||
- Create a GitHub release
|
||||
|
||||
To use the task, simply run "task release:<version>" where "<version>" is is one of:
|
||||
|
||||
- "major" - Bumps the major number
|
||||
- "minor" - Bumps the minor number
|
||||
- "patch" - Bumps the patch number
|
||||
- A semver compatible version number (e.g. "1.2.3")
|
||||
vars:
|
||||
VERSION:
|
||||
sh: "go run ./cmd/release --version {{index .MATCH 0}}"
|
||||
COMPLETE_MESSAGE: |
|
||||
Creating release with GoReleaser: https://github.com/go-task/task/actions/workflows/release.yml
|
||||
|
||||
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"
|
||||
msg: "You must be on the main branch to release"
|
||||
- sh: "[[ -z $(git diff --shortstat main) ]]"
|
||||
msg: "You must have a clean working tree to release"
|
||||
prompt: "Are you sure you want to release version {{.VERSION}}?"
|
||||
cmds:
|
||||
- go run ./cmd/release {{.CLI_ARGS}}
|
||||
- cmd: echo "Releasing v{{.VERSION}}"
|
||||
silent: true
|
||||
- "go run ./cmd/release {{.VERSION}}"
|
||||
- "git add --all"
|
||||
- "git commit -m v{{.VERSION}}"
|
||||
- "git push"
|
||||
- "git tag v{{.VERSION}}"
|
||||
- "git push origin tag v{{.VERSION}}"
|
||||
- cmd: printf "%s" '{{.COMPLETE_MESSAGE}}'
|
||||
silent: true
|
||||
|
||||
npm:publish:
|
||||
desc: Publish release to npm
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/otiai10/copy"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,6 +26,16 @@ var (
|
||||
versionRegex = regexp.MustCompile(`(?m)^ "version": "\d+\.\d+\.\d+",$`)
|
||||
)
|
||||
|
||||
// Flags
|
||||
var (
|
||||
versionFlag bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
pflag.BoolVarP(&versionFlag, "version", "v", false, "resolved version number")
|
||||
pflag.Parse()
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := release(); err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -33,7 +44,7 @@ func main() {
|
||||
}
|
||||
|
||||
func release() error {
|
||||
if len(os.Args) != 2 {
|
||||
if len(pflag.Args()) != 1 {
|
||||
return errors.New("error: expected version number")
|
||||
}
|
||||
|
||||
@@ -42,11 +53,14 @@ func release() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bumpVersion(version, os.Args[1]); err != nil {
|
||||
if err := bumpVersion(version, pflag.Arg(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(version)
|
||||
if versionFlag {
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := changelog(version); err != nil {
|
||||
return err
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
ver "github.com/go-task/task/v3/internal/version"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -58,7 +59,7 @@ func run() error {
|
||||
entrypoint := flags.Entrypoint
|
||||
|
||||
if flags.Version {
|
||||
fmt.Printf("Task version: %s\n", ver.GetVersion())
|
||||
fmt.Printf("Task version: %s\n", ver.GetVersionWithSum())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -82,6 +83,15 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.Completion != "" {
|
||||
script, err := task.Completion(flags.Completion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(script)
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.Global {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -125,16 +135,15 @@ func run() error {
|
||||
OutputStyle: flags.Output,
|
||||
TaskSorter: taskSorter,
|
||||
}
|
||||
|
||||
listOptions := task.NewListOptions(flags.List, flags.ListAll, flags.ListJson, flags.NoStatus)
|
||||
if err := listOptions.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.Setup(); err != nil {
|
||||
err := e.Setup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if experiments.AnyVariables.Enabled {
|
||||
logger.Warnf("The 'Any Variables' experiment flag is no longer required to use non-map variable types. If you wish to use map variables, please use 'TASK_X_MAP_VARIABLES' instead. See https://github.com/go-task/task/issues/1585\n")
|
||||
}
|
||||
@@ -145,6 +154,14 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if flags.ClearCache {
|
||||
cache, err := taskfile.NewCache(e.TempDir.Remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cache.Clear()
|
||||
}
|
||||
|
||||
if (listOptions.ShouldListTasks()) && flags.Silent {
|
||||
return e.ListTaskNames(flags.ListAll)
|
||||
}
|
||||
@@ -179,6 +196,9 @@ func run() error {
|
||||
|
||||
globals.Set("CLI_ARGS", ast.Var{Value: cliArgs})
|
||||
globals.Set("CLI_FORCE", ast.Var{Value: flags.Force || flags.ForceAll})
|
||||
globals.Set("CLI_SILENT", ast.Var{Value: flags.Silent})
|
||||
globals.Set("CLI_VERBOSE", ast.Var{Value: flags.Verbose})
|
||||
globals.Set("CLI_OFFLINE", ast.Var{Value: flags.Offline})
|
||||
e.Taskfile.Vars.Merge(globals, nil)
|
||||
|
||||
if !flags.Watch {
|
||||
|
||||
34
completion.go
Normal file
34
completion.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:embed completion/bash/task.bash
|
||||
var completionBash string
|
||||
|
||||
//go:embed completion/fish/task.fish
|
||||
var completionFish string
|
||||
|
||||
//go:embed completion/ps/task.ps1
|
||||
var completionPowershell string
|
||||
|
||||
//go:embed completion/zsh/_task
|
||||
var completionZsh string
|
||||
|
||||
func Completion(completion string) (string, error) {
|
||||
// Get the file extension for the selected shell
|
||||
switch completion {
|
||||
case "bash":
|
||||
return completionBash, nil
|
||||
case "fish":
|
||||
return completionFish, nil
|
||||
case "powershell":
|
||||
return completionPowershell, nil
|
||||
case "zsh":
|
||||
return completionZsh, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown shell: %s", completion)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function __task_get_tasks --description "Prints all available tasks with their d
|
||||
end
|
||||
|
||||
# Grab names and descriptions (if any) of the tasks
|
||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(aliases.*/\1\t\2/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
|
||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
|
||||
if test $output
|
||||
echo $output
|
||||
end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#compdef task
|
||||
|
||||
local context state state_descr line
|
||||
compdef _task task
|
||||
typeset -A opt_args
|
||||
|
||||
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
|
||||
@@ -39,26 +38,33 @@ function __task_list() {
|
||||
_describe 'Task to run' scripts
|
||||
}
|
||||
|
||||
_arguments \
|
||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
|
||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
|
||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
|
||||
'(-c --color)'{-c,--color}'[colored output]' \
|
||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
|
||||
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
|
||||
'(-s --silent)'{-s,--silent}'[disable echoing]' \
|
||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
|
||||
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
|
||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
|
||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
|
||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
|
||||
+ '(operation)' \
|
||||
{-l,--list}'[list describable tasks]' \
|
||||
{-a,--list-all}'[list all tasks]' \
|
||||
{-i,--init}'[create new Taskfile.yml]' \
|
||||
'(-*)'{-h,--help}'[show help]' \
|
||||
'(-*)--version[show version and exit]' \
|
||||
'*: :__task_list'
|
||||
_task() {
|
||||
_arguments \
|
||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' \
|
||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' \
|
||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]' \
|
||||
'(-c --color)'{-c,--color}'[colored output]' \
|
||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs' \
|
||||
'(--dry)--dry[dry-run mode, compile and print tasks only]' \
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)' \
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: ' \
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: ' \
|
||||
'(-s --silent)'{-s,--silent}'[disable echoing]' \
|
||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]' \
|
||||
'(--summary)--summary[show summary\: field from tasks instead of running them]' \
|
||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files' \
|
||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]' \
|
||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]' \
|
||||
+ '(operation)' \
|
||||
{-l,--list}'[list describable tasks]' \
|
||||
{-a,--list-all}'[list all tasks]' \
|
||||
{-i,--init}'[create new Taskfile.yml]' \
|
||||
'(-*)'{-h,--help}'[show help]' \
|
||||
'(-*)--version[show version and exit]' \
|
||||
'*: :__task_list'
|
||||
}
|
||||
|
||||
# don't run the completion function when being source-ed or eval-ed
|
||||
if [ "$funcstack[1]" = "_task" ]; then
|
||||
_task "$@"
|
||||
fi
|
||||
|
||||
179
errors/error_taskfile_decode.go
Normal file
179
errors/error_taskfile_decode.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/quick"
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/fatih/color"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed themes/*.xml
|
||||
var embedded embed.FS
|
||||
|
||||
var typeErrorRegex = regexp.MustCompile(`line \d+: (.*)`)
|
||||
|
||||
func init() {
|
||||
r, err := embedded.Open("themes/task.xml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
style, err := chroma.NewXMLStyle(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
styles.Register(style)
|
||||
}
|
||||
|
||||
type (
|
||||
TaskfileDecodeError struct {
|
||||
Message string
|
||||
Location string
|
||||
Line int
|
||||
Column int
|
||||
Tag string
|
||||
Snippet TaskfileSnippet
|
||||
Err error
|
||||
}
|
||||
TaskfileSnippet struct {
|
||||
Lines []string
|
||||
StartLine int
|
||||
EndLine int
|
||||
Padding int
|
||||
}
|
||||
)
|
||||
|
||||
func NewTaskfileDecodeError(err error, node *yaml.Node) *TaskfileDecodeError {
|
||||
// If the error is already a DecodeError, return it
|
||||
taskfileInvalidErr := &TaskfileDecodeError{}
|
||||
if errors.As(err, &taskfileInvalidErr) {
|
||||
return taskfileInvalidErr
|
||||
}
|
||||
return &TaskfileDecodeError{
|
||||
Line: node.Line,
|
||||
Column: node.Column,
|
||||
Tag: node.ShortTag(),
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (err *TaskfileDecodeError) Error() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Print the error message
|
||||
if err.Message != "" {
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", err.Message))
|
||||
} else {
|
||||
// Extract the errors from the TypeError
|
||||
te := &yaml.TypeError{}
|
||||
if errors.As(err.Err, &te) {
|
||||
if len(te.Errors) > 1 {
|
||||
fmt.Fprintln(buf, color.RedString("errs:"))
|
||||
for _, message := range te.Errors {
|
||||
fmt.Fprintln(buf, color.RedString("- %s", extractTypeErrorMessage(message)))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", extractTypeErrorMessage(te.Errors[0])))
|
||||
}
|
||||
} else {
|
||||
// Otherwise print the error message normally
|
||||
fmt.Fprintln(buf, color.RedString("err: %s", err.Err))
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(buf, color.RedString("file: %s:%d:%d", err.Location, err.Line, err.Column))
|
||||
|
||||
// Print the snippet
|
||||
maxLineNumberDigits := digits(err.Snippet.EndLine)
|
||||
lineNumberSpacer := strings.Repeat(" ", maxLineNumberDigits)
|
||||
columnSpacer := strings.Repeat(" ", err.Column-1)
|
||||
for i, line := range err.Snippet.Lines {
|
||||
currentLine := err.Snippet.StartLine + i + 1
|
||||
|
||||
lineIndicator := " "
|
||||
if currentLine == err.Line {
|
||||
lineIndicator = ">"
|
||||
}
|
||||
columnIndicator := "^"
|
||||
|
||||
// Print each line
|
||||
lineIndicator = color.RedString(lineIndicator)
|
||||
columnIndicator = color.RedString(columnIndicator)
|
||||
lineNumberFormat := fmt.Sprintf("%%%dd", maxLineNumberDigits)
|
||||
lineNumber := fmt.Sprintf(lineNumberFormat, currentLine)
|
||||
fmt.Fprintf(buf, "%s %s | %s", lineIndicator, lineNumber, line)
|
||||
|
||||
// Print the column indicator
|
||||
if currentLine == err.Line {
|
||||
fmt.Fprintf(buf, "\n %s | %s%s", lineNumberSpacer, columnSpacer, columnIndicator)
|
||||
}
|
||||
|
||||
// If there are more lines to print, add a newline
|
||||
if i < len(err.Snippet.Lines)-1 {
|
||||
fmt.Fprintln(buf)
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (err *TaskfileDecodeError) Unwrap() error {
|
||||
return err.Err
|
||||
}
|
||||
|
||||
func (err *TaskfileDecodeError) Code() int {
|
||||
return CodeTaskfileDecode
|
||||
}
|
||||
|
||||
func (err *TaskfileDecodeError) WithMessage(format string, a ...any) *TaskfileDecodeError {
|
||||
err.Message = fmt.Sprintf(format, a...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (err *TaskfileDecodeError) WithTypeMessage(t string) *TaskfileDecodeError {
|
||||
err.Message = fmt.Sprintf("cannot unmarshal %s into %s", err.Tag, t)
|
||||
return err
|
||||
}
|
||||
|
||||
func (err *TaskfileDecodeError) WithFileInfo(location string, b []byte, padding int) *TaskfileDecodeError {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := quick.Highlight(buf, string(b), "yaml", "terminal", "task"); err != nil {
|
||||
buf.WriteString(string(b))
|
||||
}
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
start := max(err.Line-1-padding, 0)
|
||||
end := min(err.Line+padding, len(lines)-1)
|
||||
|
||||
err.Location = location
|
||||
err.Snippet = TaskfileSnippet{
|
||||
Lines: lines[start:end],
|
||||
StartLine: start,
|
||||
EndLine: end,
|
||||
Padding: padding,
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func extractTypeErrorMessage(message string) string {
|
||||
matches := typeErrorRegex.FindStringSubmatch(message)
|
||||
if len(matches) == 2 {
|
||||
return matches[1]
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func digits(number int) int {
|
||||
count := 0
|
||||
for number != 0 {
|
||||
number /= 10
|
||||
count += 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -12,14 +12,14 @@ const (
|
||||
const (
|
||||
CodeTaskfileNotFound int = iota + 100
|
||||
CodeTaskfileAlreadyExists
|
||||
CodeTaskfileInvalid
|
||||
CodeTaskfileDecode
|
||||
CodeTaskfileFetchFailed
|
||||
CodeTaskfileNotTrusted
|
||||
CodeTaskfileNotSecure
|
||||
CodeTaskfileCacheNotFound
|
||||
CodeTaskfileVersionCheckError
|
||||
CodeTaskfileNetworkTimeout
|
||||
_ // CodeTaskfileDuplicateInclude
|
||||
CodeTaskfileInvalid
|
||||
CodeTaskfileCycle
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
CodeTaskCalledTooManyTimes
|
||||
CodeTaskCancelled
|
||||
CodeTaskMissingRequiredVars
|
||||
CodeTaskNotAllowedVars
|
||||
)
|
||||
|
||||
// TaskError extends the standard error interface with a Code method. This code will
|
||||
@@ -58,3 +59,8 @@ func Is(err, target error) bool {
|
||||
func As(err error, target any) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
|
||||
// Unwrap wraps the standard errors.Unwrap function so that we don't need to alias that package.
|
||||
func Unwrap(err error) error {
|
||||
return errors.Unwrap(err)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,19 @@ func (err *TaskNameConflictError) Code() int {
|
||||
return CodeTaskNameConflict
|
||||
}
|
||||
|
||||
type TaskNameFlattenConflictError struct {
|
||||
TaskName string
|
||||
Include string
|
||||
}
|
||||
|
||||
func (err *TaskNameFlattenConflictError) Error() string {
|
||||
return fmt.Sprintf(`task: Found multiple tasks (%s) included by "%s""`, err.TaskName, err.Include)
|
||||
}
|
||||
|
||||
func (err *TaskNameFlattenConflictError) Code() int {
|
||||
return CodeTaskNameConflict
|
||||
}
|
||||
|
||||
// TaskCalledTooManyTimesError is returned when the maximum task call limit is
|
||||
// exceeded. This is to prevent infinite loops and cyclic dependencies.
|
||||
type TaskCalledTooManyTimesError struct {
|
||||
@@ -145,3 +158,29 @@ func (err *TaskMissingRequiredVars) Error() string {
|
||||
func (err *TaskMissingRequiredVars) Code() int {
|
||||
return CodeTaskMissingRequiredVars
|
||||
}
|
||||
|
||||
type NotAllowedVar struct {
|
||||
Value string
|
||||
Enum []string
|
||||
Name string
|
||||
}
|
||||
|
||||
type TaskNotAllowedVars struct {
|
||||
TaskName string
|
||||
NotAllowedVars []NotAllowedVar
|
||||
}
|
||||
|
||||
func (err *TaskNotAllowedVars) Error() string {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString(fmt.Sprintf("task: Task %q cancelled because it is missing required variables:\n", err.TaskName))
|
||||
for _, s := range err.NotAllowedVars {
|
||||
builder.WriteString(fmt.Sprintf(" - %s has an invalid value : '%s' (allowed values : %v)\n", s.Name, s.Value, s.Enum))
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (err *TaskNotAllowedVars) Code() int {
|
||||
return CodeTaskNotAllowedVars
|
||||
}
|
||||
|
||||
17
errors/themes/task.xml
Normal file
17
errors/themes/task.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<style name="task">
|
||||
<entry type="Background" style="bg:#eee8d5"/>
|
||||
<entry type="Keyword" style="#859900"/>
|
||||
<entry type="KeywordConstant" style=""/>
|
||||
<entry type="KeywordNamespace" style="#dc322f"/>
|
||||
<entry type="KeywordType" style=""/>
|
||||
<entry type="Name" style="#268bd2"/>
|
||||
<entry type="NameBuiltin" style="#cb4b16"/>
|
||||
<entry type="NameClass" style="#cb4b16"/>
|
||||
<entry type="NameTag" style=""/>
|
||||
<entry type="Literal" style="#2aa198"/>
|
||||
<entry type="LiteralNumber" style=""/>
|
||||
<entry type="OperatorWord" style="#859900"/>
|
||||
<entry type="Comment" style="italic #93a1a1"/>
|
||||
<entry type="Generic" style="#d33682"/>
|
||||
<entry type="Text" style="#586e75"/>
|
||||
</style>
|
||||
47
go.mod
47
go.mod
@@ -1,35 +1,60 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.21.0
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Ladicle/tabwriter v1.0.0
|
||||
github.com/Masterminds/semver/v3 v3.3.0
|
||||
github.com/alecthomas/chroma/v2 v2.14.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dominikbraun/graph v0.23.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-git/go-billy/v5 v5.6.0
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0
|
||||
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90
|
||||
github.com/go-task/template v0.1.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-zglob v0.0.4
|
||||
github.com/mattn/go-zglob v0.0.6
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
github.com/otiai10/copy v1.14.0
|
||||
github.com/radovskyb/watcher v1.0.7
|
||||
github.com/sajari/fuzzy v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/whilp/git-urls v1.0.0
|
||||
github.com/zeebo/xxh3 v1.0.2
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/term v0.19.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/term v0.25.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/sh/v3 v3.8.0
|
||||
mvdan.cc/sh/v3 v3.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // 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-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
189
go.sum
189
go.sum
@@ -1,27 +1,80 @@
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg=
|
||||
github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
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/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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
|
||||
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
|
||||
github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
|
||||
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 h1:JBbiZ2CXIZ9Upe3O2yI5+3ksWoa7hNVNi4BINs8TIrs=
|
||||
github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
|
||||
github.com/go-task/template v0.1.0 h1:ym/r2G937RZA1bsgiWedNnY9e5kxDT+3YcoAnuIetTE=
|
||||
github.com/go-task/template v0.1.0/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
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=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -29,44 +82,128 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
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-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
|
||||
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
||||
github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=
|
||||
github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
|
||||
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.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
|
||||
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
|
||||
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
|
||||
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
|
||||
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU=
|
||||
github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE=
|
||||
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8=
|
||||
mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY=
|
||||
mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4=
|
||||
mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY=
|
||||
|
||||
7
hash.go
7
hash.go
@@ -1,6 +1,7 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-task/task/v3/internal/hash"
|
||||
@@ -8,11 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func (e *Executor) GetHash(t *ast.Task) (string, error) {
|
||||
r := t.Run
|
||||
if r == "" {
|
||||
r = e.Taskfile.Run
|
||||
}
|
||||
|
||||
r := cmp.Or(t.Run, e.Taskfile.Run)
|
||||
var h hash.HashFunc
|
||||
switch r {
|
||||
case "always":
|
||||
|
||||
35
help.go
35
help.go
@@ -7,8 +7,8 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/Ladicle/tabwriter"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/go-task/task/v3/internal/editors"
|
||||
@@ -105,7 +105,8 @@ func (e *Executor) ListTasks(o ListOptions) (bool, error) {
|
||||
for _, task := range tasks {
|
||||
e.Logger.FOutf(w, logger.Yellow, "* ")
|
||||
e.Logger.FOutf(w, logger.Green, task.Task)
|
||||
e.Logger.FOutf(w, logger.Default, ": \t%s", task.Desc)
|
||||
desc := strings.ReplaceAll(task.Desc, "\n", " ")
|
||||
e.Logger.FOutf(w, logger.Default, ": \t%s", desc)
|
||||
if len(task.Aliases) > 0 {
|
||||
e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", "))
|
||||
}
|
||||
@@ -159,23 +160,21 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
}
|
||||
var g errgroup.Group
|
||||
for i := range tasks {
|
||||
task := tasks[i]
|
||||
j := i
|
||||
aliases := []string{}
|
||||
if len(task.Aliases) > 0 {
|
||||
aliases = task.Aliases
|
||||
if len(tasks[i].Aliases) > 0 {
|
||||
aliases = tasks[i].Aliases
|
||||
}
|
||||
g.Go(func() error {
|
||||
o.Tasks[j] = editors.Task{
|
||||
Name: task.Name(),
|
||||
Desc: task.Desc,
|
||||
Summary: task.Summary,
|
||||
o.Tasks[i] = editors.Task{
|
||||
Name: tasks[i].Name(),
|
||||
Desc: tasks[i].Desc,
|
||||
Summary: tasks[i].Summary,
|
||||
Aliases: aliases,
|
||||
UpToDate: false,
|
||||
Location: &editors.Location{
|
||||
Line: task.Location.Line,
|
||||
Column: task.Location.Column,
|
||||
Taskfile: task.Location.Taskfile,
|
||||
Line: tasks[i].Location.Line,
|
||||
Column: tasks[i].Location.Column,
|
||||
Taskfile: tasks[i].Location.Taskfile,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -185,12 +184,12 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
|
||||
// Get the fingerprinting method to use
|
||||
method := e.Taskfile.Method
|
||||
if task.Method != "" {
|
||||
method = task.Method
|
||||
if tasks[i].Method != "" {
|
||||
method = tasks[i].Method
|
||||
}
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), task,
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), tasks[i],
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
@@ -198,7 +197,7 @@ func (e *Executor) ToEditorOutput(tasks []*ast.Task, noStatus bool) (*editors.Ta
|
||||
return err
|
||||
}
|
||||
|
||||
o.Tasks[j].UpToDate = upToDate
|
||||
o.Tasks[i].UpToDate = upToDate
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -3,14 +3,13 @@ package compiler
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
@@ -47,14 +46,12 @@ func (c *Compiler) FastGetVariables(t *ast.Task, call *ast.Call) (*ast.Vars, err
|
||||
|
||||
func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool) (*ast.Vars, error) {
|
||||
result := GetEnviron()
|
||||
if t != nil {
|
||||
specialVars, err := c.getSpecialVars(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range specialVars {
|
||||
result.Set(k, ast.Var{Value: v})
|
||||
}
|
||||
specialVars, err := c.getSpecialVars(t, call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range specialVars {
|
||||
result.Set(k, ast.Var{Value: v})
|
||||
}
|
||||
|
||||
getRangeFunc := func(dir string) func(k string, v ast.Var) error {
|
||||
@@ -77,20 +74,8 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool
|
||||
if err := cache.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Evaluate JSON
|
||||
if newVar.Json != "" {
|
||||
if err := json.Unmarshal([]byte(newVar.Json), &newVar.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Evaluate YAML
|
||||
if newVar.Yaml != "" {
|
||||
if err := yaml.Unmarshal([]byte(newVar.Yaml), &newVar.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// If the variable is not dynamic, we can set it and return
|
||||
if newVar.Value != nil || newVar.Sh == "" {
|
||||
// If the variable is already set, we can set it and return
|
||||
if newVar.Value != nil {
|
||||
result.Set(k, ast.Var{Value: newVar.Value})
|
||||
return nil
|
||||
}
|
||||
@@ -151,10 +136,15 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
|
||||
c.muDynamicCache.Lock()
|
||||
defer c.muDynamicCache.Unlock()
|
||||
|
||||
// If the variable is not dynamic or it is empty, return an empty string
|
||||
if v.Sh == nil || *v.Sh == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if c.dynamicCache == nil {
|
||||
c.dynamicCache = make(map[string]string, 30)
|
||||
}
|
||||
if result, ok := c.dynamicCache[v.Sh]; ok {
|
||||
if result, ok := c.dynamicCache[*v.Sh]; ok {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -165,7 +155,7 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
|
||||
|
||||
var stdout bytes.Buffer
|
||||
opts := &execext.RunCommandOptions{
|
||||
Command: v.Sh,
|
||||
Command: *v.Sh,
|
||||
Dir: dir,
|
||||
Stdout: &stdout,
|
||||
Stderr: c.Logger.Stderr,
|
||||
@@ -179,7 +169,7 @@ func (c *Compiler) HandleDynamicVar(v ast.Var, dir string) (string, error) {
|
||||
result := strings.TrimSuffix(stdout.String(), "\r\n")
|
||||
result = strings.TrimSuffix(result, "\n")
|
||||
|
||||
c.dynamicCache[v.Sh] = result
|
||||
c.dynamicCache[*v.Sh] = result
|
||||
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", v.Sh, result)
|
||||
|
||||
return result, nil
|
||||
@@ -193,14 +183,19 @@ func (c *Compiler) ResetCache() {
|
||||
c.dynamicCache = nil
|
||||
}
|
||||
|
||||
func (c *Compiler) getSpecialVars(t *ast.Task) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"TASK": t.Task,
|
||||
func (c *Compiler) getSpecialVars(t *ast.Task, call *ast.Call) (map[string]string, error) {
|
||||
allVars := map[string]string{
|
||||
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
||||
"ROOT_TASKFILE": filepathext.SmartJoin(c.Dir, c.Entrypoint),
|
||||
"ROOT_DIR": c.Dir,
|
||||
"TASKFILE": t.Location.Taskfile,
|
||||
"TASKFILE_DIR": filepath.Dir(t.Location.Taskfile),
|
||||
"USER_WORKING_DIR": c.UserWorkingDir,
|
||||
"TASK_VERSION": version.GetVersion(),
|
||||
}, nil
|
||||
}
|
||||
if t != nil {
|
||||
maps.Copy(allVars, map[string]string{"TASK": t.Task, "TASKFILE": t.Location.Taskfile, "TASKFILE_DIR": filepath.Dir(t.Location.Taskfile)})
|
||||
}
|
||||
if call != nil {
|
||||
maps.Copy(allVars, map[string]string{"ALIAS": call.Task})
|
||||
}
|
||||
return allVars, nil
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
|
||||
|
||||
case reflect.Struct:
|
||||
// Loop over each field and call traverseFunc recursively
|
||||
for i := 0; i < v.NumField(); i += 1 {
|
||||
for i := range v.NumField() {
|
||||
if err := traverseFunc(copy.Field(i), v.Field(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,7 +95,7 @@ func TraverseStringsFunc[T any](v T, fn func(v string) (string, error)) (T, erro
|
||||
// Create an empty copy from the original value's type
|
||||
copy.Set(reflect.MakeSlice(v.Type(), v.Len(), v.Cap()))
|
||||
// Loop over each element and call traverseFunc recursively
|
||||
for i := 0; i < v.Len(); i += 1 {
|
||||
for i := range v.Len() {
|
||||
if err := traverseFunc(copy.Index(i), v.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
9
internal/env/env.go
vendored
9
internal/env/env.go
vendored
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -11,15 +12,15 @@ func Get(t *ast.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
environ := os.Environ()
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
if !isTypeAllowed(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
if !experiments.EnvPrecedence.Enabled {
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
}
|
||||
environ = append(environ, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
|
||||
@@ -90,14 +90,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
return r.Run(ctx, p)
|
||||
}
|
||||
|
||||
// IsExitError returns true the given error is an exis status error
|
||||
func IsExitError(err error) bool {
|
||||
if _, ok := interp.IsExitStatus(err); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
|
||||
// if available.
|
||||
func Expand(s string) (string, error) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/Ladicle/tabwriter"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
RemoteTaskfiles Experiment
|
||||
AnyVariables Experiment
|
||||
MapVariables Experiment
|
||||
EnvPrecedence Experiment
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -37,6 +38,7 @@ func init() {
|
||||
RemoteTaskfiles = New("REMOTE_TASKFILES")
|
||||
AnyVariables = New("ANY_VARIABLES", "1", "2")
|
||||
MapVariables = New("MAP_VARIABLES", "1", "2")
|
||||
EnvPrecedence = New("ENV_PRECEDENCE")
|
||||
}
|
||||
|
||||
func New(xName string, enabledValues ...string) Experiment {
|
||||
@@ -70,6 +72,7 @@ func getEnvFilePath() string {
|
||||
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
|
||||
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
|
||||
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
fs.Usage = func() {}
|
||||
_ = fs.Parse(os.Args[1:])
|
||||
// If the directory is set, find a .env file in that directory.
|
||||
if dir != "" {
|
||||
@@ -104,5 +107,6 @@ func List(l *logger.Logger) error {
|
||||
printExperiment(w, l, GentleForce)
|
||||
printExperiment(w, l, RemoteTaskfiles)
|
||||
printExperiment(w, l, MapVariables)
|
||||
printExperiment(w, l, EnvPrecedence)
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
@@ -38,6 +40,7 @@ var (
|
||||
Version bool
|
||||
Help bool
|
||||
Init bool
|
||||
Completion string
|
||||
List bool
|
||||
ListAll bool
|
||||
ListJson bool
|
||||
@@ -65,6 +68,7 @@ var (
|
||||
Experiments bool
|
||||
Download bool
|
||||
Offline bool
|
||||
ClearCache bool
|
||||
Timeout time.Duration
|
||||
)
|
||||
|
||||
@@ -75,10 +79,14 @@ func init() {
|
||||
log.Print(usage)
|
||||
pflag.PrintDefaults()
|
||||
}
|
||||
|
||||
offline, err := strconv.ParseBool(cmp.Or(os.Getenv("TASK_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.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.")
|
||||
pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.")
|
||||
@@ -101,7 +109,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 tasks to run concurrently.")
|
||||
pflag.IntVarP(&Concurrency, "concurrency", "C", 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.")
|
||||
@@ -117,8 +125,9 @@ 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", false, "Forces Task to only use local or cached Taskfiles.")
|
||||
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(&ClearCache, "clear-cache", false, "Clear the remote cache.")
|
||||
}
|
||||
|
||||
pflag.Parse()
|
||||
@@ -129,6 +138,10 @@ func Validate() error {
|
||||
return errors.New("task: You can't set both --download and --offline flags")
|
||||
}
|
||||
|
||||
if Download && ClearCache {
|
||||
return errors.New("task: You can't set both --download and --clear-cache flags")
|
||||
}
|
||||
|
||||
if Global && Dir != "" {
|
||||
log.Fatal("task: You can't set both --global and --dir")
|
||||
return nil
|
||||
|
||||
@@ -15,7 +15,7 @@ func Empty(*ast.Task) (string, error) {
|
||||
}
|
||||
|
||||
func Name(t *ast.Task) (string, error) {
|
||||
return t.Task, nil
|
||||
return fmt.Sprintf("%s:%s", t.Location.Taskfile, t.LocalName()), nil
|
||||
}
|
||||
|
||||
func Hash(t *ast.Task) (string, error) {
|
||||
|
||||
@@ -52,6 +52,30 @@ func Red() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_RED", color.FgRed)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightBlue() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_BLUE", color.FgHiBlue)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightGreen() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_GREEN", color.FgHiGreen)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightCyan() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_CYAN", color.FgHiCyan)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightYellow() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_YELLOW", color.FgHiYellow)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightMagenta() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_MAGENTA", color.FgHiMagenta)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func BrightRed() PrintFunc {
|
||||
return color.New(envColor("TASK_COLOR_BRIGHT_RED", color.FgHiRed)...).FprintfFunc()
|
||||
}
|
||||
|
||||
func envColor(env string, defaultColor color.Attribute) []color.Attribute {
|
||||
if os.Getenv("FORCE_COLOR") != "" {
|
||||
color.NoColor = false
|
||||
@@ -65,7 +89,7 @@ func envColor(env string, defaultColor color.Attribute) []color.Attribute {
|
||||
// Otherwise, split by semicolons (ANSI color codes) and use them as is.
|
||||
attributeStrs := strings.Split(override, ",")
|
||||
if len(attributeStrs) == 3 {
|
||||
attributeStrs = append([]string{"38", "2"}, attributeStrs...)
|
||||
attributeStrs = slices.Concat([]string{"38", "2"}, attributeStrs)
|
||||
} else {
|
||||
attributeStrs = strings.Split(override, ";")
|
||||
}
|
||||
@@ -156,7 +180,7 @@ func (l *Logger) Prompt(color Color, prompt string, defaultValue string, continu
|
||||
return errors.New("no continue values provided")
|
||||
}
|
||||
|
||||
l.Outf(color, "%s [%s/%s]\n", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
|
||||
l.Outf(color, "%s [%s/%s]: ", prompt, strings.ToLower(continueValues[0]), strings.ToUpper(defaultValue))
|
||||
|
||||
reader := bufio.NewReader(l.Stdin)
|
||||
input, err := reader.ReadString('\n')
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
@@ -15,7 +16,7 @@ type Output interface {
|
||||
type CloseFunc func(err error) error
|
||||
|
||||
// Build the Output for the requested ast.Output.
|
||||
func BuildFor(o *ast.Output) (Output, error) {
|
||||
func BuildFor(o *ast.Output, logger *logger.Logger) (Output, error) {
|
||||
switch o.Name {
|
||||
case "interleaved", "":
|
||||
if err := checkOutputGroupUnset(o); err != nil {
|
||||
@@ -32,7 +33,7 @@ func BuildFor(o *ast.Output) (Output, error) {
|
||||
if err := checkOutputGroupUnset(o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Prefixed{}, nil
|
||||
return NewPrefixed(logger), nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: output style %q not recognized`, o.Name)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
@@ -107,7 +109,11 @@ func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
|
||||
|
||||
func TestPrefixed(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Prefixed{}
|
||||
l := &logger.Logger{
|
||||
Color: false,
|
||||
}
|
||||
|
||||
var o output.Output = output.NewPrefixed(l)
|
||||
w, _, cleanup := o.WrapWriter(&b, io.Discard, "prefix", nil)
|
||||
|
||||
t.Run("simple use cases", func(t *testing.T) {
|
||||
@@ -132,3 +138,33 @@ func TestPrefixed(t *testing.T) {
|
||||
assert.Equal(t, "[prefix] Test!\n", b.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrefixedWithColor(t *testing.T) {
|
||||
color.NoColor = false
|
||||
|
||||
var b bytes.Buffer
|
||||
l := &logger.Logger{
|
||||
Color: true,
|
||||
}
|
||||
|
||||
var o output.Output = output.NewPrefixed(l)
|
||||
|
||||
writers := make([]io.Writer, 16)
|
||||
for i := range writers {
|
||||
writers[i], _, _ = o.WrapWriter(&b, io.Discard, fmt.Sprintf("prefix-%d", i), nil)
|
||||
}
|
||||
|
||||
t.Run("colors should loop", func(t *testing.T) {
|
||||
for i, w := range writers {
|
||||
b.Reset()
|
||||
|
||||
color := output.PrefixColorSequence[i%len(output.PrefixColorSequence)]
|
||||
|
||||
var prefix bytes.Buffer
|
||||
l.FOutf(&prefix, color, fmt.Sprintf("prefix-%d", i))
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, fmt.Sprintf("[%s] foo\n[%s] bar\n", prefix.String(), prefix.String()), b.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,20 +6,36 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
)
|
||||
|
||||
type Prefixed struct{}
|
||||
type Prefixed struct {
|
||||
logger *logger.Logger
|
||||
seen map[string]uint
|
||||
counter *uint
|
||||
}
|
||||
|
||||
func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
|
||||
pw := &prefixWriter{writer: stdOut, prefix: prefix}
|
||||
func NewPrefixed(logger *logger.Logger) Prefixed {
|
||||
var counter uint
|
||||
|
||||
return Prefixed{
|
||||
seen: make(map[string]uint),
|
||||
counter: &counter,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ *templater.Cache) (io.Writer, io.Writer, CloseFunc) {
|
||||
pw := &prefixWriter{writer: stdOut, prefix: prefix, prefixed: &p}
|
||||
return pw, pw, func(error) error { return pw.close() }
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
writer io.Writer
|
||||
prefix string
|
||||
buff bytes.Buffer
|
||||
writer io.Writer
|
||||
prefixed *Prefixed
|
||||
prefix string
|
||||
buff bytes.Buffer
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) Write(p []byte) (int, error) {
|
||||
@@ -56,6 +72,11 @@ func (pw *prefixWriter) writeOutputLines(force bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
var PrefixColorSequence = []logger.Color{
|
||||
logger.Yellow, logger.Blue, logger.Magenta, logger.Cyan, logger.Green, logger.Red,
|
||||
logger.BrightYellow, logger.BrightBlue, logger.BrightMagenta, logger.BrightCyan, logger.BrightGreen, logger.BrightRed,
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) writeLine(line string) error {
|
||||
if line == "" {
|
||||
return nil
|
||||
@@ -63,6 +84,27 @@ func (pw *prefixWriter) writeLine(line string) error {
|
||||
if !strings.HasSuffix(line, "\n") {
|
||||
line += "\n"
|
||||
}
|
||||
_, err := fmt.Fprintf(pw.writer, "[%s] %s", pw.prefix, line)
|
||||
|
||||
idx, ok := pw.prefixed.seen[pw.prefix]
|
||||
|
||||
if !ok {
|
||||
idx = *pw.prefixed.counter
|
||||
pw.prefixed.seen[pw.prefix] = idx
|
||||
|
||||
*pw.prefixed.counter++
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(pw.writer, "["); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
color := PrefixColorSequence[idx%uint(len(PrefixColorSequence))]
|
||||
pw.prefixed.logger.FOutf(pw.writer, color, pw.prefix)
|
||||
|
||||
if _, err := fmt.Fprint(pw.writer, "] "); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprint(pw.writer, line)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ var templateFuncs template.FuncMap
|
||||
|
||||
func init() {
|
||||
taskFuncs := template.FuncMap{
|
||||
"OS": func() string { return runtime.GOOS },
|
||||
"ARCH": func() string { return runtime.GOARCH },
|
||||
"OS": func() string { return runtime.GOOS },
|
||||
"ARCH": func() string { return runtime.GOARCH },
|
||||
"numCPU": func() int { return runtime.NumCPU() },
|
||||
"catLines": func(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", " ")
|
||||
return strings.ReplaceAll(s, "\n", " ")
|
||||
|
||||
@@ -2,6 +2,7 @@ package templater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
@@ -40,7 +41,15 @@ func ResolveRef(ref string, cache *Cache) any {
|
||||
cache.cacheMap = cache.Vars.ToCacheMap()
|
||||
}
|
||||
|
||||
val, err := template.ResolveRef(ref, cache.cacheMap)
|
||||
if ref == "." {
|
||||
return cache.cacheMap
|
||||
}
|
||||
t, err := template.New("resolver").Funcs(templateFuncs).Parse(fmt.Sprintf("{{%s}}", ref))
|
||||
if err != nil {
|
||||
cache.err = err
|
||||
return nil
|
||||
}
|
||||
val, err := t.Resolve(cache.cacheMap)
|
||||
if err != nil {
|
||||
cache.err = err
|
||||
return nil
|
||||
@@ -119,8 +128,6 @@ func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var
|
||||
Live: v.Live,
|
||||
Ref: v.Ref,
|
||||
Dir: v.Dir,
|
||||
Json: ReplaceWithExtra(v.Json, cache, extra),
|
||||
Yaml: ReplaceWithExtra(v.Yaml, cache, extra),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,21 +5,29 @@ import (
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var version = ""
|
||||
|
||||
func GetVersion() string {
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
var (
|
||||
version = ""
|
||||
sum = ""
|
||||
)
|
||||
|
||||
func init() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info.Main.Version == "" {
|
||||
return "unknown"
|
||||
version = "unknown"
|
||||
} else {
|
||||
if version == "" {
|
||||
version = info.Main.Version
|
||||
}
|
||||
if sum == "" {
|
||||
sum = info.Main.Sum
|
||||
}
|
||||
}
|
||||
|
||||
ver := info.Main.Version
|
||||
if info.Main.Sum != "" {
|
||||
ver += fmt.Sprintf(" (%s)", info.Main.Sum)
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
func GetVersion() string {
|
||||
return version
|
||||
}
|
||||
|
||||
func GetVersionWithSum() string {
|
||||
return fmt.Sprintf("%s (%s)", version, sum)
|
||||
}
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.37.2",
|
||||
"version": "3.40.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.37.2",
|
||||
"version": "3.40.0",
|
||||
"description": "A task runner / simpler Make alternative written in Go",
|
||||
"scripts": {
|
||||
"postinstall": "go-npm install",
|
||||
|
||||
25
requires.go
25
requires.go
@@ -1,13 +1,13 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call *ast.Call) error {
|
||||
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task, call *ast.Call) error {
|
||||
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -18,9 +18,19 @@ func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call
|
||||
}
|
||||
|
||||
var missingVars []string
|
||||
var notAllowedValuesVars []errors.NotAllowedVar
|
||||
for _, requiredVar := range t.Requires.Vars {
|
||||
if !vars.Exists(requiredVar) {
|
||||
missingVars = append(missingVars, requiredVar)
|
||||
value, isString := vars.Get(requiredVar.Name).Value.(string)
|
||||
if !vars.Exists(requiredVar.Name) {
|
||||
missingVars = append(missingVars, requiredVar.Name)
|
||||
} else {
|
||||
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
|
||||
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
|
||||
Value: value,
|
||||
Enum: requiredVar.Enum,
|
||||
Name: requiredVar.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +41,12 @@ func (e *Executor) areTaskRequiredVarsSet(ctx context.Context, t *ast.Task, call
|
||||
}
|
||||
}
|
||||
|
||||
if len(notAllowedValuesVars) > 0 {
|
||||
return &errors.TaskNotAllowedVars{
|
||||
TaskName: t.Name(),
|
||||
NotAllowedVars: notAllowedValuesVars,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
37
setup.go
37
setup.go
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -69,7 +70,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
|
||||
e.Download,
|
||||
e.Offline,
|
||||
e.Timeout,
|
||||
e.TempDir,
|
||||
e.TempDir.Remote,
|
||||
e.Logger,
|
||||
)
|
||||
graph, err := reader.Read()
|
||||
@@ -95,7 +96,7 @@ func (e *Executor) setupFuzzyModel() {
|
||||
words = append(words, taskName)
|
||||
|
||||
for _, task := range e.Taskfile.Tasks.Values() {
|
||||
words = append(words, task.Aliases...)
|
||||
words = slices.Concat(words, task.Aliases)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,12 +105,15 @@ func (e *Executor) setupFuzzyModel() {
|
||||
}
|
||||
|
||||
func (e *Executor) setupTempDir() error {
|
||||
if e.TempDir != "" {
|
||||
if e.TempDir != (TempDir{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if os.Getenv("TASK_TEMP_DIR") == "" {
|
||||
e.TempDir = filepathext.SmartJoin(e.Dir, ".task")
|
||||
e.TempDir = TempDir{
|
||||
Remote: filepathext.SmartJoin(e.Dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(e.Dir, ".task"),
|
||||
}
|
||||
} else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") {
|
||||
tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR"))
|
||||
if err != nil {
|
||||
@@ -117,9 +121,28 @@ func (e *Executor) setupTempDir() error {
|
||||
}
|
||||
projectDir, _ := filepath.Abs(e.Dir)
|
||||
projectName := filepath.Base(projectDir)
|
||||
e.TempDir = filepathext.SmartJoin(tempDir, projectName)
|
||||
e.TempDir = TempDir{
|
||||
Remote: tempDir,
|
||||
Fingerprint: filepathext.SmartJoin(tempDir, projectName),
|
||||
}
|
||||
|
||||
} else {
|
||||
e.TempDir = filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR"))
|
||||
e.TempDir = TempDir{
|
||||
Remote: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
|
||||
Fingerprint: filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")),
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("TASK_REMOTE_DIR") != "" {
|
||||
if filepath.IsAbs(os.Getenv("TASK_REMOTE_DIR")) || strings.HasPrefix(os.Getenv("TASK_REMOTE_DIR"), "~") {
|
||||
remoteTempDir, err := execext.Expand(os.Getenv("TASK_REMOTE_DIR"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.TempDir.Remote = remoteTempDir
|
||||
} else {
|
||||
e.TempDir.Remote = filepathext.SmartJoin(e.Dir, ".task")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -155,7 +178,7 @@ func (e *Executor) setupOutput() error {
|
||||
}
|
||||
|
||||
var err error
|
||||
e.Output, err = output.BuildFor(&e.OutputStyle)
|
||||
e.Output, err = output.BuildFor(&e.OutputStyle, e.Logger)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
15
signals.go
15
signals.go
@@ -8,24 +8,25 @@ import (
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
)
|
||||
|
||||
const interruptSignalsCount = 3
|
||||
|
||||
// NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals
|
||||
// so the Task process is not killed immediately and processes running have
|
||||
// time to do cleanup work.
|
||||
func (e *Executor) InterceptInterruptSignals() {
|
||||
ch := make(chan os.Signal, 3)
|
||||
ch := make(chan os.Signal, interruptSignalsCount)
|
||||
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
for i := 1; i <= 3; i++ {
|
||||
for i := range interruptSignalsCount {
|
||||
sig := <-ch
|
||||
|
||||
if i < 3 {
|
||||
e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig)
|
||||
continue
|
||||
if i+1 >= interruptSignalsCount {
|
||||
e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
e.Logger.Errf(logger.Red, "task: Signal received for the third time: %q. Forcing shutdown\n", sig)
|
||||
os.Exit(1)
|
||||
e.Logger.Outf(logger.Yellow, "task: Signal received: %q\n", sig)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
SLEEPIT, _ = filepath.Abs("./bin/sleepit")
|
||||
)
|
||||
var SLEEPIT, _ = filepath.Abs("./bin/sleepit")
|
||||
|
||||
func TestSignalSentToProcessGroup(t *testing.T) {
|
||||
task, err := getTaskPath()
|
||||
@@ -147,7 +145,7 @@ func TestSignalSentToProcessGroup(t *testing.T) {
|
||||
// where the negative PID means the corresponding process group. Note that
|
||||
// this negative PID works only as long as the caller of the kill(2) system
|
||||
// call has a different PID, which is the case for this test.
|
||||
for i := 1; i <= tc.sendSigs; i++ {
|
||||
for range tc.sendSigs - 1 {
|
||||
if err := syscall.Kill(-sut.Process.Pid, syscall.SIGINT); err != nil {
|
||||
t.Fatalf("sending INT signal to the process group: %v", err)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (e *Executor) Status(ctx context.Context, calls ...*ast.Call) error {
|
||||
// Check if the task is up-to-date
|
||||
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
@@ -46,7 +46,7 @@ func (e *Executor) statusOnError(t *ast.Task) error {
|
||||
if method == "" {
|
||||
method = e.Taskfile.Method
|
||||
}
|
||||
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir, e.Dry)
|
||||
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir.Fingerprint, e.Dry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
75
task.go
75
task.go
@@ -11,6 +11,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
@@ -34,13 +36,18 @@ const (
|
||||
MaximumTaskCall = 1000
|
||||
)
|
||||
|
||||
type TempDir struct {
|
||||
Remote string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// Executor executes a Taskfile
|
||||
type Executor struct {
|
||||
Taskfile *ast.Taskfile
|
||||
|
||||
Dir string
|
||||
Entrypoint string
|
||||
TempDir string
|
||||
TempDir TempDir
|
||||
Force bool
|
||||
ForceAll bool
|
||||
Insecure bool
|
||||
@@ -183,16 +190,6 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
release := e.acquireConcurrencyLimit()
|
||||
defer release()
|
||||
|
||||
if t.Prompt != "" {
|
||||
if err := e.Logger.Prompt(logger.Yellow, t.Prompt, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
|
||||
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
|
||||
} else if errors.Is(err, logger.ErrPromptCancelled) {
|
||||
return &errors.TaskCancelledByUserError{TaskName: call.Task}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.startExecution(ctx, t, func(ctx context.Context) error {
|
||||
e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task)
|
||||
if err := e.runDeps(ctx, t); err != nil {
|
||||
@@ -205,7 +202,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsSet(ctx, t, call); err != nil {
|
||||
if err := e.areTaskRequiredVarsSet(t, call); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -222,7 +219,7 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithTempDir(e.TempDir.Fingerprint),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
@@ -238,13 +235,27 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range t.Prompt {
|
||||
if p != "" && !e.Dry {
|
||||
if err := e.Logger.Prompt(logger.Yellow, p, "n", "y", "yes"); errors.Is(err, logger.ErrNoTerminal) {
|
||||
return &errors.TaskCancelledNoTerminalError{TaskName: call.Task}
|
||||
} else if errors.Is(err, logger.ErrPromptCancelled) {
|
||||
return &errors.TaskCancelledByUserError{TaskName: call.Task}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.mkdir(t); err != nil {
|
||||
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
|
||||
}
|
||||
|
||||
var deferredExitCode uint8
|
||||
|
||||
for i := range t.Cmds {
|
||||
if t.Cmds[i].Defer {
|
||||
defer e.runDeferred(t, call, i)
|
||||
defer e.runDeferred(t, call, i, &deferredExitCode)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -253,9 +264,13 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
|
||||
}
|
||||
|
||||
if execext.IsExitError(err) && t.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
|
||||
continue
|
||||
exitCode, isExitError := interp.IsExitStatus(err)
|
||||
if isExitError {
|
||||
if t.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
|
||||
continue
|
||||
}
|
||||
deferredExitCode = exitCode
|
||||
}
|
||||
|
||||
if call.Indirect {
|
||||
@@ -307,10 +322,26 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) {
|
||||
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
origTask, err := e.GetTask(call)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := t.Cmds[i]
|
||||
vars, _ := e.Compiler.GetVariables(origTask, call)
|
||||
cache := &templater.Cache{Vars: vars}
|
||||
extra := map[string]any{}
|
||||
|
||||
if deferredExitCode != nil && *deferredExitCode > 0 {
|
||||
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
|
||||
}
|
||||
|
||||
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
|
||||
|
||||
if err := e.runCommand(ctx, t, call, i); err != nil {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
|
||||
}
|
||||
@@ -367,7 +398,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
|
||||
if closeErr := close(err); closeErr != nil {
|
||||
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
|
||||
}
|
||||
if execext.IsExitError(err) && cmd.IgnoreError {
|
||||
if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
|
||||
return nil
|
||||
}
|
||||
@@ -489,14 +520,12 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*ast.Task, error) {
|
||||
|
||||
// Compile the list of tasks
|
||||
for i := range tasks {
|
||||
idx := i
|
||||
task := tasks[idx]
|
||||
g.Go(func() error {
|
||||
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: task.Task})
|
||||
compiledTask, err := e.FastCompiledTask(&ast.Call{Task: tasks[i].Task})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tasks[idx] = compiledTask
|
||||
tasks[i] = compiledTask
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
469
task_test.go
469
task_test.go
@@ -5,6 +5,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
rand "math/rand/v2"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -12,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -19,7 +24,9 @@ import (
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
@@ -60,17 +67,19 @@ func (fct fileContentTest) Run(t *testing.T) {
|
||||
for f := range fct.Files {
|
||||
_ = os.Remove(filepathext.SmartJoin(fct.Dir, f))
|
||||
}
|
||||
|
||||
e := &task.Executor{
|
||||
Dir: fct.Dir,
|
||||
TempDir: filepathext.SmartJoin(fct.Dir, ".task"),
|
||||
Dir: fct.Dir,
|
||||
TempDir: task.TempDir{
|
||||
Remote: filepathext.SmartJoin(fct.Dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(fct.Dir, ".task"),
|
||||
},
|
||||
Entrypoint: fct.Entrypoint,
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
}
|
||||
|
||||
require.NoError(t, e.Setup(), "e.Setup()")
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: fct.Target}), "e.Run(target)")
|
||||
|
||||
for name, expectContent := range fct.Files {
|
||||
t.Run(fct.name(name), func(t *testing.T) {
|
||||
path := filepathext.SmartJoin(e.Dir, name)
|
||||
@@ -105,6 +114,7 @@ func TestEmptyTaskfile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
t.Setenv("QUX", "from_os")
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/env",
|
||||
Target: "default",
|
||||
@@ -113,9 +123,21 @@ func TestEnv(t *testing.T) {
|
||||
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
|
||||
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
|
||||
"multiple_type.txt": "FOO='1' BAR='true' BAZ='1.1'\n",
|
||||
"not-overriden.txt": "QUX='from_os'\n",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
t.Setenv("TASK_X_ENV_PRECEDENCE", "1")
|
||||
experiments.EnvPrecedence = experiments.New("ENV_PRECEDENCE")
|
||||
ttt := fileContentTest{
|
||||
Dir: "testdata/env",
|
||||
Target: "overriden",
|
||||
TrimSpace: false,
|
||||
Files: map[string]string{
|
||||
"overriden.txt": "QUX='from_taskfile'\n",
|
||||
},
|
||||
}
|
||||
ttt.Run(t)
|
||||
}
|
||||
|
||||
func TestVars(t *testing.T) {
|
||||
@@ -133,6 +155,39 @@ func TestVars(t *testing.T) {
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestRequires(t *testing.T) {
|
||||
const dir = "testdata/requires"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
|
||||
require.NoError(t, e.Setup())
|
||||
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "missing-var"}), "task: Task \"missing-var\" cancelled because it is missing required variables: foo")
|
||||
buff.Reset()
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
vars := &ast.Vars{}
|
||||
vars.Set("foo", ast.Var{Value: "bar"})
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{
|
||||
Task: "missing-var",
|
||||
Vars: vars,
|
||||
}))
|
||||
buff.Reset()
|
||||
|
||||
require.NoError(t, e.Setup())
|
||||
require.ErrorContains(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}), "task: Task \"validation-var\" cancelled because it is missing required variables:\n - foo has an invalid value : 'bar' (allowed values : [one two])")
|
||||
buff.Reset()
|
||||
|
||||
require.NoError(t, e.Setup())
|
||||
vars.Set("foo", ast.Var{Value: "one"})
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "validation-var", Vars: vars}))
|
||||
buff.Reset()
|
||||
}
|
||||
|
||||
func TestSpecialVars(t *testing.T) {
|
||||
const dir = "testdata/special_vars"
|
||||
const subdir = "testdata/special_vars/subdir"
|
||||
@@ -272,11 +327,14 @@ func TestStatus(t *testing.T) {
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
TempDir: filepathext.SmartJoin(dir, ".task"),
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
Dir: dir,
|
||||
TempDir: task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
},
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
// gen-foo creates foo.txt, and will always fail it's status check.
|
||||
@@ -468,7 +526,10 @@ func TestStatusChecksum(t *testing.T) {
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
tempdir := filepathext.SmartJoin(dir, ".task")
|
||||
tempdir := task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
}
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
TempDir: tempdir,
|
||||
@@ -485,7 +546,7 @@ func TestStatusChecksum(t *testing.T) {
|
||||
|
||||
// Capture the modification time, so we can ensure the checksum file
|
||||
// is not regenerated when the hash hasn't changed.
|
||||
s, err := os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task))
|
||||
s, err := os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
|
||||
require.NoError(t, err)
|
||||
time := s.ModTime()
|
||||
|
||||
@@ -493,7 +554,7 @@ func TestStatusChecksum(t *testing.T) {
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.task}))
|
||||
assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String())
|
||||
|
||||
s, err = os.Stat(filepathext.SmartJoin(tempdir, "checksum/"+test.task))
|
||||
s, err = os.Stat(filepathext.SmartJoin(tempdir.Fingerprint, "checksum/"+test.task))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, time, s.ModTime())
|
||||
})
|
||||
@@ -803,7 +864,8 @@ func TestListDescInterpolation(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Contains(t, buff.String(), "bar")
|
||||
assert.Contains(t, buff.String(), "foo-var")
|
||||
assert.Contains(t, buff.String(), "bar-var")
|
||||
}
|
||||
|
||||
func TestStatusVariables(t *testing.T) {
|
||||
@@ -814,8 +876,11 @@ func TestStatusVariables(t *testing.T) {
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
TempDir: filepathext.SmartJoin(dir, ".task"),
|
||||
Dir: dir,
|
||||
TempDir: task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
},
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: false,
|
||||
@@ -963,11 +1028,14 @@ func TestDryChecksum(t *testing.T) {
|
||||
_ = os.Remove(checksumFile)
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
TempDir: filepathext.SmartJoin(dir, ".task"),
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
Dry: true,
|
||||
Dir: dir,
|
||||
TempDir: task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
},
|
||||
Stdout: io.Discard,
|
||||
Stderr: io.Discard,
|
||||
Dry: true,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "default"}))
|
||||
@@ -1013,6 +1081,107 @@ func TestIncludesMultiLevel(t *testing.T) {
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncludesRemote(t *testing.T) {
|
||||
enableExperimentForTest(t, &experiments.RemoteTaskfiles, "1")
|
||||
|
||||
dir := "testdata/includes_remote"
|
||||
|
||||
srv := httptest.NewServer(http.FileServer(http.Dir(dir)))
|
||||
defer srv.Close()
|
||||
|
||||
tcs := []struct {
|
||||
firstRemote string
|
||||
secondRemote string
|
||||
}{
|
||||
{
|
||||
firstRemote: srv.URL + "/first/Taskfile.yml",
|
||||
secondRemote: srv.URL + "/first/second/Taskfile.yml",
|
||||
},
|
||||
{
|
||||
firstRemote: srv.URL + "/first/Taskfile.yml",
|
||||
secondRemote: "./second/Taskfile.yml",
|
||||
},
|
||||
}
|
||||
|
||||
tasks := []string{
|
||||
"first:write-file",
|
||||
"first:second:write-file",
|
||||
}
|
||||
|
||||
for i, tc := range tcs {
|
||||
t.Run(fmt.Sprint(i), func(t *testing.T) {
|
||||
t.Setenv("FIRST_REMOTE_URL", tc.firstRemote)
|
||||
t.Setenv("SECOND_REMOTE_URL", tc.secondRemote)
|
||||
|
||||
var buff SyncBuffer
|
||||
|
||||
executors := []struct {
|
||||
name string
|
||||
executor *task.Executor
|
||||
}{
|
||||
{
|
||||
name: "online, always download",
|
||||
executor: &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Timeout: time.Minute,
|
||||
Insecure: true,
|
||||
Logger: &logger.Logger{Stdout: &buff, Stderr: &buff, Verbose: true},
|
||||
|
||||
// Without caching
|
||||
AssumeYes: true,
|
||||
Download: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offline, use cache",
|
||||
executor: &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Timeout: time.Minute,
|
||||
Insecure: true,
|
||||
Logger: &logger.Logger{Stdout: &buff, Stderr: &buff, Verbose: true},
|
||||
|
||||
// With caching
|
||||
AssumeYes: false,
|
||||
Download: false,
|
||||
Offline: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for j, e := range executors {
|
||||
t.Run(fmt.Sprint(j), func(t *testing.T) {
|
||||
require.NoError(t, e.executor.Setup())
|
||||
|
||||
for k, task := range tasks {
|
||||
t.Run(task, func(t *testing.T) {
|
||||
expectedContent := fmt.Sprint(rand.Int64())
|
||||
t.Setenv("CONTENT", expectedContent)
|
||||
|
||||
outputFile := fmt.Sprintf("%d.%d.txt", i, k)
|
||||
t.Setenv("OUTPUT_FILE", outputFile)
|
||||
|
||||
path := filepath.Join(dir, outputFile)
|
||||
require.NoError(t, os.RemoveAll(path))
|
||||
|
||||
require.NoError(t, e.executor.Run(context.Background(), &ast.Call{Task: task}))
|
||||
|
||||
actualContent, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedContent, strings.TrimSpace(string(actualContent)))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Log("\noutput:\n", buff.buf.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeCycle(t *testing.T) {
|
||||
const dir = "testdata/includes_cycle"
|
||||
|
||||
@@ -1042,7 +1211,7 @@ func TestIncludesIncorrect(t *testing.T) {
|
||||
|
||||
err := e.Setup()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "task: Failed to parse testdata/includes_incorrect/incomplete.yml:")
|
||||
assert.Contains(t, err.Error(), "Failed to parse testdata/includes_incorrect/incomplete.yml:", err.Error())
|
||||
}
|
||||
|
||||
func TestIncludesEmptyMain(t *testing.T) {
|
||||
@@ -1057,6 +1226,88 @@ func TestIncludesEmptyMain(t *testing.T) {
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncludesHttp(t *testing.T) {
|
||||
enableExperimentForTest(t, &experiments.RemoteTaskfiles, "1")
|
||||
|
||||
dir, err := filepath.Abs("testdata/includes_http")
|
||||
require.NoError(t, err)
|
||||
|
||||
srv := httptest.NewServer(http.FileServer(http.Dir(dir)))
|
||||
defer srv.Close()
|
||||
|
||||
t.Cleanup(func() {
|
||||
// This test fills the .task/remote directory with cache entries because the include URL
|
||||
// is different on every test due to the dynamic nature of the TCP port in srv.URL
|
||||
if err := os.RemoveAll(filepath.Join(dir, ".task")); err != nil {
|
||||
t.Logf("error cleaning up: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
taskfiles, err := fs.Glob(os.DirFS(dir), "root-taskfile-*.yml")
|
||||
require.NoError(t, err)
|
||||
|
||||
remotes := []struct {
|
||||
name string
|
||||
root string
|
||||
}{
|
||||
{
|
||||
name: "local",
|
||||
root: ".",
|
||||
},
|
||||
{
|
||||
name: "http-remote",
|
||||
root: srv.URL,
|
||||
},
|
||||
}
|
||||
|
||||
for _, taskfile := range taskfiles {
|
||||
t.Run(taskfile, func(t *testing.T) {
|
||||
for _, remote := range remotes {
|
||||
t.Run(remote.name, func(t *testing.T) {
|
||||
t.Setenv("INCLUDE_ROOT", remote.root)
|
||||
entrypoint := filepath.Join(dir, taskfile)
|
||||
|
||||
var buff SyncBuffer
|
||||
e := task.Executor{
|
||||
Entrypoint: entrypoint,
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Insecure: true,
|
||||
Download: true,
|
||||
AssumeYes: true,
|
||||
Logger: &logger.Logger{Stdout: &buff, Stderr: &buff, Verbose: true},
|
||||
Timeout: time.Minute,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
defer func() { t.Log("output:", buff.buf.String()) }()
|
||||
|
||||
tcs := []struct {
|
||||
name, dir string
|
||||
}{
|
||||
{
|
||||
name: "second-with-dir-1:third-with-dir-1:default",
|
||||
dir: filepath.Join(dir, "dir-1"),
|
||||
},
|
||||
{
|
||||
name: "second-with-dir-1:third-with-dir-2:default",
|
||||
dir: filepath.Join(dir, "dir-2"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
task, err := e.CompiledTask(&ast.Call{Task: tc.name})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.dir, task.Dir)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludesDependencies(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/includes_deps",
|
||||
@@ -1201,6 +1452,45 @@ func TestIncludesInternal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludesFlatten(t *testing.T) {
|
||||
const dir = "testdata/includes_flatten"
|
||||
tests := []struct {
|
||||
name string
|
||||
taskfile string
|
||||
task string
|
||||
expectedErr bool
|
||||
expectedOutput string
|
||||
}{
|
||||
{name: "included flatten", taskfile: "Taskfile.yml", task: "gen", expectedOutput: "gen from included\n"},
|
||||
{name: "included flatten with default", taskfile: "Taskfile.yml", task: "default", expectedOutput: "default from included flatten\n"},
|
||||
{name: "included flatten can call entrypoint tasks", taskfile: "Taskfile.yml", task: "from_entrypoint", expectedOutput: "from entrypoint\n"},
|
||||
{name: "included flatten with deps", taskfile: "Taskfile.yml", task: "with_deps", expectedOutput: "gen from included\nwith_deps from included\n"},
|
||||
{name: "included flatten nested", taskfile: "Taskfile.yml", task: "from_nested", expectedOutput: "from nested\n"},
|
||||
{name: "included flatten multiple same task", taskfile: "Taskfile.multiple.yml", task: "gen", expectedErr: true, expectedOutput: "task: Found multiple tasks (gen) included by \"included\"\""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Entrypoint: dir + "/" + test.taskfile,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
}
|
||||
err := e.Setup()
|
||||
if test.expectedErr {
|
||||
assert.EqualError(t, err, test.expectedOutput)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
_ = e.Run(context.Background(), &ast.Call{Task: test.task})
|
||||
assert.Equal(t, test.expectedOutput, buff.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludesInterpolation(t *testing.T) {
|
||||
const dir = "testdata/includes_interpolation"
|
||||
tests := []struct {
|
||||
@@ -1589,6 +1879,18 @@ func TestDotenvHasEnvVarInPath(t *testing.T) {
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestTaskDotenvParseErrorMessage(t *testing.T) {
|
||||
e := task.Executor{
|
||||
Dir: "testdata/dotenv/parse_error",
|
||||
}
|
||||
|
||||
path, _ := filepath.Abs(filepath.Join(e.Dir, ".env-with-error"))
|
||||
expected := fmt.Sprintf("error reading env file %s:", path)
|
||||
|
||||
err := e.Setup()
|
||||
require.ErrorContains(t, err, expected)
|
||||
}
|
||||
|
||||
func TestTaskDotenv(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/dotenv_task/default",
|
||||
@@ -1664,6 +1966,26 @@ func TestRunOnlyRunsJobsHashOnce(t *testing.T) {
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestRunOnceSharedDeps(t *testing.T) {
|
||||
const dir = "testdata/run_once_shared_deps"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
ForceAll: true,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "build"}))
|
||||
|
||||
rx := regexp.MustCompile(`task: \[service-[a,b]:library:build\] echo "build library"`)
|
||||
matches := rx.FindAllStringSubmatch(buff.String(), -1)
|
||||
assert.Len(t, matches, 1)
|
||||
assert.Contains(t, buff.String(), `task: [service-a:build] echo "build a"`)
|
||||
assert.Contains(t, buff.String(), `task: [service-b:build] echo "build b"`)
|
||||
}
|
||||
|
||||
func TestDeferredCmds(t *testing.T) {
|
||||
const dir = "testdata/deferred"
|
||||
var buff bytes.Buffer
|
||||
@@ -1689,6 +2011,34 @@ task-1 ran successfully
|
||||
assert.Contains(t, buff.String(), expectedOutputOrder)
|
||||
}
|
||||
|
||||
func TestExitCodeZero(t *testing.T) {
|
||||
const dir = "testdata/exit_code"
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "exit-zero"}))
|
||||
assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=", strings.TrimSpace(buff.String()))
|
||||
}
|
||||
|
||||
func TestExitCodeOne(t *testing.T) {
|
||||
const dir = "testdata/exit_code"
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "exit-one"}))
|
||||
assert.Equal(t, "FOO=bar - DYNAMIC_FOO=bar - EXIT_CODE=1", strings.TrimSpace(buff.String()))
|
||||
}
|
||||
|
||||
func TestIgnoreNilElements(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -2260,6 +2610,10 @@ func TestForCmds(t *testing.T) {
|
||||
name: "loop-explicit",
|
||||
expectedOutput: "a\nb\nc\n",
|
||||
},
|
||||
{
|
||||
name: "loop-matrix",
|
||||
expectedOutput: "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n",
|
||||
},
|
||||
{
|
||||
name: "loop-sources",
|
||||
expectedOutput: "bar\nfoo\n",
|
||||
@@ -2317,6 +2671,17 @@ func TestForDeps(t *testing.T) {
|
||||
name: "loop-explicit",
|
||||
expectedOutputContains: []string{"a\n", "b\n", "c\n"},
|
||||
},
|
||||
{
|
||||
name: "loop-matrix",
|
||||
expectedOutputContains: []string{
|
||||
"windows/amd64\n",
|
||||
"windows/arm64\n",
|
||||
"linux/amd64\n",
|
||||
"linux/arm64\n",
|
||||
"darwin/amd64\n",
|
||||
"darwin/arm64\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "loop-sources",
|
||||
expectedOutputContains: []string{"bar\n", "foo\n"},
|
||||
@@ -2357,6 +2722,8 @@ func TestForDeps(t *testing.T) {
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
Force: true,
|
||||
// Force output of each dep to be grouped together to prevent interleaving
|
||||
OutputStyle: ast.Output{Name: "group"},
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.name}))
|
||||
@@ -2426,3 +2793,63 @@ func TestWildcard(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReference(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
call string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "reference in command",
|
||||
call: "ref-cmd",
|
||||
expectedOutput: "1\n",
|
||||
},
|
||||
{
|
||||
name: "reference in dependency",
|
||||
call: "ref-dep",
|
||||
expectedOutput: "1\n",
|
||||
},
|
||||
{
|
||||
name: "reference using templating resolver",
|
||||
call: "ref-resolver",
|
||||
expectedOutput: "1\n",
|
||||
},
|
||||
{
|
||||
name: "reference using templating resolver and dynamic var",
|
||||
call: "ref-resolver-sh",
|
||||
expectedOutput: "Alice has 3 children called Bob, Charlie, and Diane\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.call, func(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/var_references",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
Force: true,
|
||||
}
|
||||
require.NoError(t, e.Setup())
|
||||
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.call}))
|
||||
assert.Equal(t, test.expectedOutput, buff.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// enableExperimentForTest enables the experiment behind pointer e for the duration of test t and sub-tests,
|
||||
// with the experiment being restored to its previous state when tests complete.
|
||||
//
|
||||
// Typically experiments are controlled via TASK_X_ env vars, but we cannot use those in tests
|
||||
// because the experiment settings are parsed during experiments.init(), before any tests run.
|
||||
func enableExperimentForTest(t *testing.T, e *experiments.Experiment, val string) {
|
||||
prev := *e
|
||||
*e = experiments.Experiment{
|
||||
Name: prev.Name,
|
||||
Enabled: true,
|
||||
Value: val,
|
||||
}
|
||||
t.Cleanup(func() { *e = prev })
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
@@ -46,7 +45,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
c.Cmd = cmd
|
||||
return nil
|
||||
@@ -110,8 +109,8 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line)
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in command")
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
// Dep is a task dependency
|
||||
@@ -32,7 +32,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var task string
|
||||
if err := node.Decode(&task); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
d.Task = task
|
||||
return nil
|
||||
@@ -45,7 +45,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
|
||||
Silent bool
|
||||
}
|
||||
if err := node.Decode(&taskCall); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
d.Task = taskCall.Task
|
||||
d.For = taskCall.For
|
||||
@@ -54,5 +54,5 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot unmarshal %s into dependency", node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("dependency")
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
)
|
||||
|
||||
type For struct {
|
||||
From string
|
||||
List []any
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
From string
|
||||
List []any
|
||||
Matrix omap.OrderedMap[string, []any]
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
}
|
||||
|
||||
func (f *For) UnmarshalYAML(node *yaml.Node) error {
|
||||
@@ -22,7 +23,7 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var from string
|
||||
if err := node.Decode(&from); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
f.From = from
|
||||
return nil
|
||||
@@ -30,28 +31,35 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.SequenceNode:
|
||||
var list []any
|
||||
if err := node.Decode(&list); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
f.List = list
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var forStruct struct {
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
Matrix omap.OrderedMap[string, []any]
|
||||
Var string
|
||||
Split string
|
||||
As string
|
||||
}
|
||||
if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" {
|
||||
f.Var = forStruct.Var
|
||||
f.Split = forStruct.Split
|
||||
f.As = forStruct.As
|
||||
return nil
|
||||
if err := node.Decode(&forStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line)
|
||||
if forStruct.Var == "" && forStruct.Matrix.Len() == 0 {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for")
|
||||
}
|
||||
if forStruct.Var != "" && forStruct.Matrix.Len() != 0 {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("cannot use both var and matrix in for")
|
||||
}
|
||||
f.Matrix = forStruct.Matrix
|
||||
f.Var = forStruct.Var
|
||||
f.Split = forStruct.Split
|
||||
f.As = forStruct.As
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into for", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("for")
|
||||
}
|
||||
|
||||
func (f *For) DeepCopy() *For {
|
||||
@@ -59,10 +67,11 @@ func (f *For) DeepCopy() *For {
|
||||
return nil
|
||||
}
|
||||
return &For{
|
||||
From: f.From,
|
||||
List: deepcopy.Slice(f.List),
|
||||
Var: f.Var,
|
||||
Split: f.Split,
|
||||
As: f.As,
|
||||
From: f.From,
|
||||
List: deepcopy.Slice(f.List),
|
||||
Matrix: f.Matrix.DeepCopy(),
|
||||
Var: f.Var,
|
||||
Split: f.Split,
|
||||
As: f.As,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
type Glob struct {
|
||||
@@ -13,20 +13,22 @@ type Glob struct {
|
||||
|
||||
func (g *Glob) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
g.Glob = node.Value
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var glob struct {
|
||||
Exclude string
|
||||
}
|
||||
if err := node.Decode(&glob); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
g.Glob = glob.Exclude
|
||||
g.Negate = true
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("glob")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
omap "github.com/go-task/task/v3/internal/omap"
|
||||
)
|
||||
|
||||
@@ -18,6 +17,7 @@ type Include struct {
|
||||
Aliases []string
|
||||
AdvancedImport bool
|
||||
Vars *Vars
|
||||
Flatten bool
|
||||
}
|
||||
|
||||
// Includes represents information about included tasksfiles
|
||||
@@ -38,7 +38,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
|
||||
|
||||
var v Include
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Namespace = keyNode.Value
|
||||
includes.Set(keyNode.Value, &v)
|
||||
@@ -46,7 +46,7 @@ func (includes *Includes) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("includes")
|
||||
}
|
||||
|
||||
// Len returns the length of the map
|
||||
@@ -71,7 +71,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var str string
|
||||
if err := node.Decode(&str); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
include.Taskfile = str
|
||||
return nil
|
||||
@@ -82,11 +82,12 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
Dir string
|
||||
Optional bool
|
||||
Internal bool
|
||||
Flatten bool
|
||||
Aliases []string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := node.Decode(&includedTaskfile); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
include.Taskfile = includedTaskfile.Taskfile
|
||||
include.Dir = includedTaskfile.Dir
|
||||
@@ -95,10 +96,11 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
include.Aliases = includedTaskfile.Aliases
|
||||
include.AdvancedImport = true
|
||||
include.Vars = includedTaskfile.Vars
|
||||
include.Flatten = includedTaskfile.Flatten
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("include")
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of IncludedTaskfile and copies
|
||||
@@ -115,5 +117,6 @@ func (include *Include) DeepCopy() *Include {
|
||||
Internal: include.Internal,
|
||||
AdvancedImport: include.AdvancedImport,
|
||||
Vars: include.Vars.DeepCopy(),
|
||||
Flatten: include.Flatten,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
// Output of the Task output
|
||||
@@ -25,7 +25,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var name string
|
||||
if err := node.Decode(&name); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
s.Name = name
|
||||
return nil
|
||||
@@ -35,10 +35,10 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
|
||||
Group *OutputGroup
|
||||
}
|
||||
if err := node.Decode(&tmp); err != nil {
|
||||
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if tmp.Group == nil {
|
||||
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`output style must have the "group" key when in mapping form`)
|
||||
}
|
||||
*s = Output{
|
||||
Name: "group",
|
||||
@@ -47,7 +47,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("output")
|
||||
}
|
||||
|
||||
// OutputGroup is the style options specific to the Group style.
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/goext"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,7 @@ type ErrInvalidPlatform struct {
|
||||
}
|
||||
|
||||
func (err *ErrInvalidPlatform) Error() string {
|
||||
return fmt.Sprintf(`task: Invalid platform "%s"`, err.Platform)
|
||||
return fmt.Sprintf(`invalid platform "%s"`, err.Platform)
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
@@ -39,14 +40,14 @@ func (p *Platform) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var platform string
|
||||
if err := node.Decode(&platform); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if err := p.parsePlatform(platform); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into platform", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("platform")
|
||||
}
|
||||
|
||||
// parsePlatform takes a string representing an OS/Arch combination (or either on their own)
|
||||
|
||||
@@ -26,10 +26,10 @@ func TestPlatformParsing(t *testing.T) {
|
||||
{Input: "windows/amd64", ExpectedOS: "windows", ExpectedArch: "amd64"},
|
||||
{Input: "windows/arm64", ExpectedOS: "windows", ExpectedArch: "arm64"},
|
||||
|
||||
{Input: "invalid", Error: `task: Invalid platform "invalid"`},
|
||||
{Input: "invalid/invalid", Error: `task: Invalid platform "invalid/invalid"`},
|
||||
{Input: "windows/invalid", Error: `task: Invalid platform "windows/invalid"`},
|
||||
{Input: "invalid/amd64", Error: `task: Invalid platform "invalid/amd64"`},
|
||||
{Input: "invalid", Error: `invalid platform "invalid"`},
|
||||
{Input: "invalid/invalid", Error: `invalid platform "invalid/invalid"`},
|
||||
{Input: "windows/invalid", Error: `invalid platform "windows/invalid"`},
|
||||
{Input: "invalid/amd64", Error: `invalid platform "invalid/amd64"`},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
|
||||
var ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value")
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
// Precondition represents a precondition necessary for a task to run
|
||||
type Precondition struct {
|
||||
@@ -33,7 +31,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
p.Sh = cmd
|
||||
p.Msg = fmt.Sprintf("`%s` failed", cmd)
|
||||
@@ -45,7 +43,7 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
|
||||
Msg string
|
||||
}
|
||||
if err := node.Decode(&sh); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
p.Sh = sh.Sh
|
||||
p.Msg = sh.Msg
|
||||
@@ -55,5 +53,5 @@ func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into precondition", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("precondition")
|
||||
}
|
||||
|
||||
29
taskfile/ast/prompt.go
Normal file
29
taskfile/ast/prompt.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
type Prompt []string
|
||||
|
||||
func (p *Prompt) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var str string
|
||||
if err := node.Decode(&str); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
*p = []string{str}
|
||||
return nil
|
||||
case yaml.SequenceNode:
|
||||
var list []string
|
||||
if err := node.Decode(&list); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
*p = list
|
||||
return nil
|
||||
}
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("prompt")
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package ast
|
||||
|
||||
import "github.com/go-task/task/v3/internal/deepcopy"
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Requires represents a set of required variables necessary for a task to run
|
||||
type Requires struct {
|
||||
Vars []string
|
||||
Vars []*VarsWithValidation
|
||||
}
|
||||
|
||||
func (r *Requires) DeepCopy() *Requires {
|
||||
@@ -16,3 +21,47 @@ func (r *Requires) DeepCopy() *Requires {
|
||||
Vars: deepcopy.Slice(r.Vars),
|
||||
}
|
||||
}
|
||||
|
||||
type VarsWithValidation struct {
|
||||
Name string
|
||||
Enum []string
|
||||
}
|
||||
|
||||
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return &VarsWithValidation{
|
||||
Name: v.Name,
|
||||
Enum: v.Enum,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Name = cmd
|
||||
v.Enum = nil
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var vv struct {
|
||||
Name string
|
||||
Enum []string
|
||||
}
|
||||
if err := node.Decode(&vv); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Name = vv.Name
|
||||
v.Enum = vv.Enum
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("requires")
|
||||
}
|
||||
|
||||
@@ -7,42 +7,45 @@ import (
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Task represents a task
|
||||
type Task struct {
|
||||
Task string
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Prompt string
|
||||
Summary string
|
||||
Requires *Requires
|
||||
Aliases []string
|
||||
Sources []*Glob
|
||||
Generates []*Glob
|
||||
Status []string
|
||||
Preconditions []*Precondition
|
||||
Dir string
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
Silent bool
|
||||
Interactive bool
|
||||
Internal bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool
|
||||
Run string
|
||||
Task string
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Prompt Prompt
|
||||
Summary string
|
||||
Requires *Requires
|
||||
Aliases []string
|
||||
Sources []*Glob
|
||||
Generates []*Glob
|
||||
Status []string
|
||||
Preconditions []*Precondition
|
||||
Dir string
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
Silent bool
|
||||
Interactive bool
|
||||
Internal bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
Watch bool
|
||||
Location *Location
|
||||
// Populated during merging
|
||||
Namespace string
|
||||
IncludeVars *Vars
|
||||
IncludedTaskfileVars *Vars
|
||||
Platforms []*Platform
|
||||
Location *Location
|
||||
Watch bool
|
||||
}
|
||||
|
||||
func (t *Task) Name() string {
|
||||
@@ -52,6 +55,13 @@ func (t *Task) Name() string {
|
||||
return t.Task
|
||||
}
|
||||
|
||||
func (t *Task) LocalName() string {
|
||||
name := t.Task
|
||||
name = strings.TrimPrefix(name, t.Namespace)
|
||||
name = strings.TrimPrefix(name, ":")
|
||||
return name
|
||||
}
|
||||
|
||||
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
|
||||
func (t *Task) WildcardMatch(name string) (bool, []string) {
|
||||
// Convert the name into a regex string
|
||||
@@ -83,7 +93,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.ScalarNode:
|
||||
var cmd Cmd
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
t.Cmds = append(t.Cmds, &cmd)
|
||||
return nil
|
||||
@@ -92,7 +102,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.SequenceNode:
|
||||
var cmds []*Cmd
|
||||
if err := node.Decode(&cmds); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
t.Cmds = cmds
|
||||
return nil
|
||||
@@ -105,7 +115,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Prompt string
|
||||
Prompt Prompt
|
||||
Summary string
|
||||
Aliases []string
|
||||
Sources []*Glob
|
||||
@@ -130,11 +140,11 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
Watch bool
|
||||
}
|
||||
if err := node.Decode(&task); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if task.Cmd != nil {
|
||||
if task.Cmds != nil {
|
||||
return fmt.Errorf("yaml: line %d: task cannot have both cmd and cmds", node.Line)
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("task cannot have both cmd and cmds")
|
||||
}
|
||||
t.Cmds = []*Cmd{task.Cmd}
|
||||
} else {
|
||||
@@ -169,7 +179,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("task")
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of Task and copies
|
||||
@@ -209,6 +219,7 @@ func (t *Task) DeepCopy() *Task {
|
||||
Platforms: deepcopy.Slice(t.Platforms),
|
||||
Location: t.Location.DeepCopy(),
|
||||
Requires: t.Requires.DeepCopy(),
|
||||
Namespace: t.Namespace,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
// NamespaceSeparator contains the character that separates namespaces
|
||||
@@ -54,8 +55,7 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error {
|
||||
}
|
||||
t1.Vars.Merge(t2.Vars, include)
|
||||
t1.Env.Merge(t2.Env, include)
|
||||
t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
return nil
|
||||
return t1.Tasks.Merge(t2.Tasks, include, t1.Vars)
|
||||
}
|
||||
|
||||
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
@@ -77,7 +77,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
Interval time.Duration
|
||||
}
|
||||
if err := node.Decode(&taskfile); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
tf.Version = taskfile.Version
|
||||
tf.Output = taskfile.Output
|
||||
@@ -101,5 +101,5 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("taskfile")
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
)
|
||||
@@ -45,43 +47,48 @@ func (t *Tasks) FindMatchingTasks(call *Call) []*MatchingTask {
|
||||
return matchingTasks
|
||||
}
|
||||
|
||||
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
|
||||
_ = t2.Range(func(k string, v *Task) error {
|
||||
func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) error {
|
||||
err := t2.Range(func(name string, v *Task) error {
|
||||
// We do a deep copy of the task struct here to ensure that no data can
|
||||
// be changed elsewhere once the taskfile is merged.
|
||||
task := v.DeepCopy()
|
||||
|
||||
// Set the task to internal if EITHER the included task or the included
|
||||
// taskfile are marked as internal
|
||||
task.Internal = task.Internal || (include != nil && include.Internal)
|
||||
|
||||
// Add namespaces to task dependencies
|
||||
for _, dep := range task.Deps {
|
||||
if dep != nil && dep.Task != "" {
|
||||
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task commands
|
||||
for _, cmd := range task.Cmds {
|
||||
if cmd != nil && cmd.Task != "" {
|
||||
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task aliases
|
||||
for i, alias := range task.Aliases {
|
||||
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
|
||||
}
|
||||
|
||||
// Add namespace aliases
|
||||
if include != nil {
|
||||
for _, namespaceAlias := range include.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
|
||||
for _, alias := range v.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
|
||||
taskName := name
|
||||
if !include.Flatten {
|
||||
// Add namespaces to task dependencies
|
||||
for _, dep := range task.Deps {
|
||||
if dep != nil && dep.Task != "" {
|
||||
dep.Task = taskNameWithNamespace(dep.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task commands
|
||||
for _, cmd := range task.Cmds {
|
||||
if cmd != nil && cmd.Task != "" {
|
||||
cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespaces to task aliases
|
||||
for i, alias := range task.Aliases {
|
||||
task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace)
|
||||
}
|
||||
|
||||
// Add namespace aliases
|
||||
if include != nil {
|
||||
for _, namespaceAlias := range include.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(task.Task, namespaceAlias))
|
||||
for _, alias := range v.Aliases {
|
||||
task.Aliases = append(task.Aliases, taskNameWithNamespace(alias, namespaceAlias))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskName = taskNameWithNamespace(name, include.Namespace)
|
||||
task.Namespace = include.Namespace
|
||||
task.Task = taskName
|
||||
}
|
||||
|
||||
if include.AdvancedImport {
|
||||
@@ -93,24 +100,29 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include, includedTaskfileVars *Vars) {
|
||||
task.IncludedTaskfileVars = includedTaskfileVars.DeepCopy()
|
||||
}
|
||||
|
||||
if t1.Get(taskName) != nil {
|
||||
return &errors.TaskNameFlattenConflictError{
|
||||
TaskName: taskName,
|
||||
Include: include.Namespace,
|
||||
}
|
||||
}
|
||||
// Add the task to the merged taskfile
|
||||
taskNameWithNamespace := taskNameWithNamespace(k, include.Namespace)
|
||||
task.Task = taskNameWithNamespace
|
||||
t1.Set(taskNameWithNamespace, task)
|
||||
t1.Set(taskName, task)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// If the included Taskfile has a default task and the parent namespace has
|
||||
// If the included Taskfile has a default task, being not flattened and the parent namespace has
|
||||
// no task with a matching name, we can add an alias so that the user can
|
||||
// run the included Taskfile's default task without specifying its full
|
||||
// name. If the parent namespace has aliases, we add another alias for each
|
||||
// of them.
|
||||
if t2.Get("default") != nil && t1.Get(include.Namespace) == nil {
|
||||
if t2.Get("default") != nil && t1.Get(include.Namespace) == nil && !include.Flatten {
|
||||
defaultTaskName := fmt.Sprintf("%s:default", include.Namespace)
|
||||
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Namespace)
|
||||
t1.Get(defaultTaskName).Aliases = append(t1.Get(defaultTaskName).Aliases, include.Aliases...)
|
||||
t1.Get(defaultTaskName).Aliases = slices.Concat(t1.Get(defaultTaskName).Aliases, include.Aliases)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
|
||||
@@ -118,7 +130,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.MappingNode:
|
||||
tasks := omap.New[string, *Task]()
|
||||
if err := node.Decode(&tasks); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
@@ -150,7 +162,7 @@ func (t *Tasks) UnmarshalYAML(node *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into tasks", node.Line, node.ShortTag())
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("tasks")
|
||||
}
|
||||
|
||||
func taskNameWithNamespace(taskName string, namespace string) string {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/omap"
|
||||
)
|
||||
@@ -20,7 +20,7 @@ type Vars struct {
|
||||
func (vs *Vars) ToCacheMap() (m map[string]any) {
|
||||
m = make(map[string]any, vs.Len())
|
||||
_ = vs.Range(func(k string, v Var) error {
|
||||
if v.Sh != "" {
|
||||
if v.Sh != nil && *v.Sh != "" {
|
||||
// Dynamic variable is not yet resolved; trigger
|
||||
// <no value> to be used in templates.
|
||||
return nil
|
||||
@@ -81,10 +81,8 @@ func (vs *Vars) DeepCopy() *Vars {
|
||||
type Var struct {
|
||||
Value any
|
||||
Live any
|
||||
Sh string
|
||||
Sh *string
|
||||
Ref string
|
||||
Json string
|
||||
Yaml string
|
||||
Dir string
|
||||
}
|
||||
|
||||
@@ -95,12 +93,16 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
if experiments.MapVariables.Value == "1" {
|
||||
var value any
|
||||
if err := node.Decode(&value); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
// If the value is a string and it starts with $, then it's a shell command
|
||||
if str, ok := value.(string); ok {
|
||||
if str, ok = strings.CutPrefix(str, "$"); ok {
|
||||
v.Sh = str
|
||||
v.Sh = &str
|
||||
return nil
|
||||
}
|
||||
if str, ok = strings.CutPrefix(str, "#"); ok {
|
||||
v.Ref = str
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -114,30 +116,26 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
case yaml.MappingNode:
|
||||
key := node.Content[0].Value
|
||||
switch key {
|
||||
case "sh", "ref", "map", "json", "yaml":
|
||||
case "sh", "ref", "map":
|
||||
var m struct {
|
||||
Sh string
|
||||
Ref string
|
||||
Map any
|
||||
Json string
|
||||
Yaml string
|
||||
Sh *string
|
||||
Ref string
|
||||
Map any
|
||||
}
|
||||
if err := node.Decode(&m); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Sh = m.Sh
|
||||
v.Ref = m.Ref
|
||||
v.Value = m.Map
|
||||
v.Json = m.Json
|
||||
v.Yaml = m.Yaml
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf(`yaml: line %d: %q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, node.Line, key)
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
|
||||
}
|
||||
default:
|
||||
var value any
|
||||
if err := node.Decode(&value); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Value = value
|
||||
return nil
|
||||
@@ -148,22 +146,27 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.MappingNode:
|
||||
if len(node.Content) > 2 || node.Content[0].Value != "sh" {
|
||||
return fmt.Errorf(`task: line %d: maps cannot be assigned to variables`, node.Line)
|
||||
key := node.Content[0].Value
|
||||
switch key {
|
||||
case "sh", "ref":
|
||||
var m struct {
|
||||
Sh *string
|
||||
Ref string
|
||||
}
|
||||
if err := node.Decode(&m); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Sh = m.Sh
|
||||
v.Ref = m.Ref
|
||||
return nil
|
||||
default:
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables")
|
||||
}
|
||||
var sh struct {
|
||||
Sh string
|
||||
}
|
||||
if err := node.Decode(&sh); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Sh = sh.Sh
|
||||
return nil
|
||||
|
||||
default:
|
||||
var value any
|
||||
if err := node.Decode(&value); err != nil {
|
||||
return err
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
v.Value = value
|
||||
return nil
|
||||
|
||||
@@ -50,9 +50,23 @@ func (c *Cache) key(node Node) string {
|
||||
}
|
||||
|
||||
func (c *Cache) cacheFilePath(node Node) string {
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%s.yaml", c.key(node)))
|
||||
return c.filePath(node, "yaml")
|
||||
}
|
||||
|
||||
func (c *Cache) checksumFilePath(node Node) string {
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%s.checksum", c.key(node)))
|
||||
return c.filePath(node, "checksum")
|
||||
}
|
||||
|
||||
func (c *Cache) filePath(node Node, suffix string) string {
|
||||
lastDir, filename := node.FilenameAndLastDir()
|
||||
prefix := filename
|
||||
// Means it's not "", nor "." nor "/", so it's a valid directory
|
||||
if len(lastDir) > 1 {
|
||||
prefix = fmt.Sprintf("%s-%s", lastDir, filename)
|
||||
}
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%s.%s.%s", prefix, c.key(node), suffix))
|
||||
}
|
||||
|
||||
func (c *Cache) Clear() error {
|
||||
return os.RemoveAll(c.dir)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
@@ -37,7 +38,7 @@ func Dotenv(c *compiler.Compiler, tf *ast.Taskfile, dir string) (*ast.Vars, erro
|
||||
|
||||
envs, err := godotenv.Read(dotEnvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error reading env file %s: %w", dotEnvPath, err)
|
||||
}
|
||||
for key, value := range envs {
|
||||
if ok := env.Exists(key); !ok {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
giturls "github.com/whilp/git-urls"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/experiments"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
@@ -20,6 +22,7 @@ type Node interface {
|
||||
Remote() bool
|
||||
ResolveEntrypoint(entrypoint string) (string, error)
|
||||
ResolveDir(dir string) (string, error)
|
||||
FilenameAndLastDir() (string, string)
|
||||
}
|
||||
|
||||
func NewRootNode(
|
||||
@@ -47,24 +50,39 @@ func NewNode(
|
||||
) (Node, error) {
|
||||
var node Node
|
||||
var err error
|
||||
switch getScheme(entrypoint) {
|
||||
scheme, err := getScheme(entrypoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch scheme {
|
||||
case "git":
|
||||
node, err = NewGitNode(entrypoint, dir, insecure, opts...)
|
||||
case "http", "https":
|
||||
node, err = NewHTTPNode(l, entrypoint, dir, insecure, timeout, opts...)
|
||||
default:
|
||||
// If no other scheme matches, we assume it's a file
|
||||
node, err = NewFileNode(l, entrypoint, dir, opts...)
|
||||
|
||||
}
|
||||
|
||||
if node.Remote() && !experiments.RemoteTaskfiles.Enabled {
|
||||
return nil, errors.New("task: Remote taskfiles are not enabled. You can read more about this experiment and how to enable it at https://taskfile.dev/experiments/remote-taskfiles")
|
||||
}
|
||||
return node, err
|
||||
}
|
||||
|
||||
func getScheme(uri string) string {
|
||||
if i := strings.Index(uri, "://"); i != -1 {
|
||||
return uri[:i]
|
||||
func getScheme(uri string) (string, error) {
|
||||
u, err := giturls.Parse(uri)
|
||||
if u == nil {
|
||||
return "", err
|
||||
}
|
||||
return ""
|
||||
if strings.HasSuffix(strings.Split(u.Path, "//")[0], ".git") && (u.Scheme == "git" || u.Scheme == "ssh" || u.Scheme == "https" || u.Scheme == "http") {
|
||||
return "git", nil
|
||||
}
|
||||
|
||||
if i := strings.Index(uri, "://"); i != -1 {
|
||||
return uri[:i], nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func getDefaultDir(entrypoint, dir string) string {
|
||||
|
||||
@@ -81,6 +81,9 @@ func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||
if strings.Contains(entrypoint, "://") {
|
||||
return entrypoint, nil
|
||||
}
|
||||
if strings.HasPrefix(entrypoint, "git") {
|
||||
return entrypoint, nil
|
||||
}
|
||||
|
||||
path, err := execext.Expand(entrypoint)
|
||||
if err != nil {
|
||||
@@ -112,3 +115,7 @@ func (node *FileNode) ResolveDir(dir string) (string, error) {
|
||||
entrypointDir := filepath.Dir(node.Entrypoint)
|
||||
return filepathext.SmartJoin(entrypointDir, path), nil
|
||||
}
|
||||
|
||||
func (node *FileNode) FilenameAndLastDir() (string, string) {
|
||||
return "", filepath.Base(node.Entrypoint)
|
||||
}
|
||||
|
||||
126
taskfile/node_git.go
Normal file
126
taskfile/node_git.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
giturls "github.com/whilp/git-urls"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
)
|
||||
|
||||
// An GitNode is a node that reads a Taskfile from a remote location via Git.
|
||||
type GitNode struct {
|
||||
*BaseNode
|
||||
URL *url.URL
|
||||
rawUrl string
|
||||
ref string
|
||||
path string
|
||||
}
|
||||
|
||||
func NewGitNode(
|
||||
entrypoint string,
|
||||
dir string,
|
||||
insecure bool,
|
||||
opts ...NodeOption,
|
||||
) (*GitNode, error) {
|
||||
base := NewBaseNode(dir, opts...)
|
||||
u, err := giturls.Parse(entrypoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basePath, path := func() (string, string) {
|
||||
x := strings.Split(u.Path, "//")
|
||||
return x[0], x[1]
|
||||
}()
|
||||
ref := u.Query().Get("ref")
|
||||
|
||||
rawUrl := u.String()
|
||||
|
||||
u.RawQuery = ""
|
||||
u.Path = basePath
|
||||
|
||||
if u.Scheme == "http" && !insecure {
|
||||
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
|
||||
}
|
||||
return &GitNode{
|
||||
BaseNode: base,
|
||||
URL: u,
|
||||
rawUrl: rawUrl,
|
||||
ref: ref,
|
||||
path: path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (node *GitNode) Location() string {
|
||||
return node.rawUrl
|
||||
}
|
||||
|
||||
func (node *GitNode) Remote() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (node *GitNode) Read(_ context.Context) ([]byte, error) {
|
||||
fs := memfs.New()
|
||||
storer := memory.NewStorage()
|
||||
_, err := git.Clone(storer, fs, &git.CloneOptions{
|
||||
URL: node.URL.String(),
|
||||
ReferenceName: plumbing.ReferenceName(node.ref),
|
||||
SingleBranch: true,
|
||||
Depth: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := fs.Open(node.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Read the entire response body
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||
dir, _ := filepath.Split(node.path)
|
||||
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
|
||||
if node.ref != "" {
|
||||
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
|
||||
}
|
||||
return resolvedEntrypoint, nil
|
||||
}
|
||||
|
||||
func (node *GitNode) ResolveDir(dir string) (string, error) {
|
||||
path, err := execext.Expand(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if filepathext.IsAbs(path) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
|
||||
// This means that files are included relative to one another
|
||||
entrypointDir := filepath.Dir(node.Dir())
|
||||
return filepathext.SmartJoin(entrypointDir, path), nil
|
||||
}
|
||||
|
||||
func (node *GitNode) FilenameAndLastDir() (string, string) {
|
||||
return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path))
|
||||
}
|
||||
75
taskfile/node_git_test.go
Normal file
75
taskfile/node_git_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitNode_ssh(t *testing.T) {
|
||||
node, err := NewGitNode("git@github.com:foo/bar.git//Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "main", node.ref)
|
||||
assert.Equal(t, "Taskfile.yml", node.path)
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
|
||||
entrypoint, err := node.ResolveEntrypoint("common.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
|
||||
}
|
||||
|
||||
func TestGitNode_sshWithDir(t *testing.T) {
|
||||
node, err := NewGitNode("git@github.com:foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "main", node.ref)
|
||||
assert.Equal(t, "directory/Taskfile.yml", node.path)
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
|
||||
entrypoint, err := node.ResolveEntrypoint("common.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
|
||||
}
|
||||
|
||||
func TestGitNode_https(t *testing.T) {
|
||||
node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "main", node.ref)
|
||||
assert.Equal(t, "Taskfile.yml", node.path)
|
||||
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
|
||||
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
|
||||
entrypoint, err := node.ResolveEntrypoint("common.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
|
||||
}
|
||||
|
||||
func TestGitNode_httpsWithDir(t *testing.T) {
|
||||
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "main", node.ref)
|
||||
assert.Equal(t, "directory/Taskfile.yml", node.path)
|
||||
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
|
||||
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
|
||||
entrypoint, err := node.ResolveEntrypoint("common.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
|
||||
}
|
||||
|
||||
func TestGitNode_FilenameAndDir(t *testing.T) {
|
||||
node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
filename, dir := node.FilenameAndLastDir()
|
||||
assert.Equal(t, "Taskfile.yml", filename)
|
||||
assert.Equal(t, "directory", dir)
|
||||
|
||||
node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
filename, dir = node.FilenameAndLastDir()
|
||||
assert.Equal(t, "Taskfile.yml", filename)
|
||||
assert.Equal(t, ".", dir)
|
||||
|
||||
node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
|
||||
assert.NoError(t, err)
|
||||
filename, dir = node.FilenameAndLastDir()
|
||||
assert.Equal(t, "Taskfile.yml", filename)
|
||||
assert.Equal(t, "directory", dir)
|
||||
}
|
||||
@@ -17,7 +17,9 @@ import (
|
||||
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
|
||||
type HTTPNode struct {
|
||||
*BaseNode
|
||||
URL *url.URL
|
||||
URL *url.URL
|
||||
logger *logger.Logger
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewHTTPNode(
|
||||
@@ -36,18 +38,12 @@ func NewHTTPNode(
|
||||
if url.Scheme == "http" && !insecure {
|
||||
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
|
||||
}
|
||||
ctx, cf := context.WithTimeout(context.Background(), timeout)
|
||||
defer cf()
|
||||
url, err = RemoteExists(ctx, l, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: url.String(), Timeout: timeout}
|
||||
}
|
||||
|
||||
return &HTTPNode{
|
||||
BaseNode: base,
|
||||
URL: url,
|
||||
timeout: timeout,
|
||||
logger: l,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -60,6 +56,11 @@ func (node *HTTPNode) Remote() bool {
|
||||
}
|
||||
|
||||
func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
|
||||
url, err := RemoteExists(ctx, node.logger, node.URL, node.timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.URL = url
|
||||
req, err := http.NewRequest("GET", node.URL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
|
||||
@@ -67,10 +68,12 @@ func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.URL.String(), Timeout: node.timeout}
|
||||
}
|
||||
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.TaskfileFetchFailedError{
|
||||
URI: node.URL.String(),
|
||||
@@ -107,6 +110,15 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {
|
||||
|
||||
// NOTE: Uses the directory of the entrypoint (Taskfile), not the current working directory
|
||||
// This means that files are included relative to one another
|
||||
entrypointDir := filepath.Dir(node.Dir())
|
||||
return filepathext.SmartJoin(entrypointDir, path), nil
|
||||
parent := node.Dir()
|
||||
if node.Parent() != nil {
|
||||
parent = node.Parent().Dir()
|
||||
}
|
||||
|
||||
return filepathext.SmartJoin(parent, path), nil
|
||||
}
|
||||
|
||||
func (node *HTTPNode) FilenameAndLastDir() (string, string) {
|
||||
dir, filename := filepath.Split(node.URL.Path)
|
||||
return filepath.Base(dir), filename
|
||||
}
|
||||
|
||||
@@ -72,3 +72,7 @@ func (node *StdinNode) ResolveDir(dir string) (string, error) {
|
||||
|
||||
return filepathext.SmartJoin(node.Dir(), path), nil
|
||||
}
|
||||
|
||||
func (node *StdinNode) FilenameAndLastDir() (string, string) {
|
||||
return "", "__stdin__"
|
||||
}
|
||||
|
||||
22
taskfile/node_test.go
Normal file
22
taskfile/node_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScheme(t *testing.T) {
|
||||
scheme, err := getScheme("https://github.com/foo/bar.git")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "git", scheme)
|
||||
scheme, err = getScheme("https://github.com/foo/bar.git?ref=v1//taskfile/common.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "git", scheme)
|
||||
scheme, err = getScheme("git@github.com:foo/bar.git?ref=main//Taskfile.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "git", scheme)
|
||||
scheme, err = getScheme("https://github.com/foo/common.yml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https", scheme)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dominikbraun/graph"
|
||||
@@ -30,14 +31,15 @@ Continue?`
|
||||
// A Reader will recursively read Taskfiles from a given source using a directed
|
||||
// acyclic graph (DAG).
|
||||
type Reader struct {
|
||||
graph *ast.TaskfileGraph
|
||||
node Node
|
||||
insecure bool
|
||||
download bool
|
||||
offline bool
|
||||
timeout time.Duration
|
||||
tempDir string
|
||||
logger *logger.Logger
|
||||
graph *ast.TaskfileGraph
|
||||
node Node
|
||||
insecure bool
|
||||
download bool
|
||||
offline bool
|
||||
timeout time.Duration
|
||||
tempDir string
|
||||
logger *logger.Logger
|
||||
promptMutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewReader(
|
||||
@@ -50,14 +52,15 @@ func NewReader(
|
||||
logger *logger.Logger,
|
||||
) *Reader {
|
||||
return &Reader{
|
||||
graph: ast.NewTaskfileGraph(),
|
||||
node: node,
|
||||
insecure: insecure,
|
||||
download: download,
|
||||
offline: offline,
|
||||
timeout: timeout,
|
||||
tempDir: tempDir,
|
||||
logger: logger,
|
||||
graph: ast.NewTaskfileGraph(),
|
||||
node: node,
|
||||
insecure: insecure,
|
||||
download: download,
|
||||
offline: offline,
|
||||
timeout: timeout,
|
||||
tempDir: tempDir,
|
||||
logger: logger,
|
||||
promptMutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +112,7 @@ func (r *Reader) include(node Node) error {
|
||||
Dir: templater.Replace(include.Dir, cache),
|
||||
Optional: include.Optional,
|
||||
Internal: include.Internal,
|
||||
Flatten: include.Flatten,
|
||||
Aliases: include.Aliases,
|
||||
AdvancedImport: include.AdvancedImport,
|
||||
Vars: include.Vars,
|
||||
@@ -180,91 +184,18 @@ func (r *Reader) include(node Node) error {
|
||||
}
|
||||
|
||||
func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
|
||||
var b []byte
|
||||
var err error
|
||||
var cache *Cache
|
||||
|
||||
if node.Remote() {
|
||||
cache, err = NewCache(r.tempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If the file is remote and we're in offline mode, check if we have a cached copy
|
||||
if node.Remote() && r.offline {
|
||||
if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location())
|
||||
} else {
|
||||
|
||||
downloaded := false
|
||||
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
|
||||
defer cf()
|
||||
|
||||
// Read the file
|
||||
b, err = node.Read(ctx)
|
||||
// If we timed out then we likely have a network issue
|
||||
if node.Remote() && errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
// If a download was requested, then we can't use a cached copy
|
||||
if r.download {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
|
||||
}
|
||||
// Search for any cached copies
|
||||
if b, err = cache.read(node); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout, CheckedCache: true}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location())
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
downloaded = true
|
||||
}
|
||||
|
||||
// If the node was remote, we need to check the checksum
|
||||
if node.Remote() && downloaded {
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location())
|
||||
|
||||
// Get the checksums
|
||||
checksum := checksum(b)
|
||||
cachedChecksum := cache.readChecksum(node)
|
||||
|
||||
var prompt string
|
||||
if cachedChecksum == "" {
|
||||
// If the checksum doesn't exist, prompt the user to continue
|
||||
prompt = fmt.Sprintf(taskfileUntrustedPrompt, node.Location())
|
||||
} else if checksum != cachedChecksum {
|
||||
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
|
||||
prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location())
|
||||
}
|
||||
if prompt != "" {
|
||||
if err := r.logger.Prompt(logger.Yellow, prompt, "n", "y", "yes"); err != nil {
|
||||
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
|
||||
}
|
||||
}
|
||||
|
||||
// If the hash has changed (or is new)
|
||||
if checksum != cachedChecksum {
|
||||
// Store the checksum
|
||||
if err := cache.writeChecksum(node, checksum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Cache the file
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location())
|
||||
if err = cache.write(node, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
b, err := r.loadNodeContent(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tf ast.Taskfile
|
||||
if err := yaml.Unmarshal(b, &tf); err != nil {
|
||||
// Decode the taskfile and add the file info the any errors
|
||||
taskfileInvalidErr := &errors.TaskfileDecodeError{}
|
||||
if errors.As(err, &taskfileInvalidErr) {
|
||||
return nil, taskfileInvalidErr.WithFileInfo(node.Location(), b, 2)
|
||||
}
|
||||
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
|
||||
}
|
||||
|
||||
@@ -288,3 +219,93 @@ func (r *Reader) readNode(node Node) (*ast.Taskfile, error) {
|
||||
|
||||
return &tf, nil
|
||||
}
|
||||
|
||||
func (r *Reader) loadNodeContent(node Node) ([]byte, error) {
|
||||
if !node.Remote() {
|
||||
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
|
||||
defer cf()
|
||||
return node.Read(ctx)
|
||||
}
|
||||
|
||||
cache, err := NewCache(r.tempDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.offline {
|
||||
// In offline mode try to use cached copy
|
||||
cached, err := cache.read(node)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, &errors.TaskfileCacheNotFoundError{URI: node.Location()}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched cached copy\n", node.Location())
|
||||
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
ctx, cf := context.WithTimeout(context.Background(), r.timeout)
|
||||
defer cf()
|
||||
|
||||
b, err := node.Read(ctx)
|
||||
if errors.Is(err, &errors.TaskfileNetworkTimeoutError{}) {
|
||||
// If we timed out then we likely have a network issue
|
||||
|
||||
// If a download was requested, then we can't use a cached copy
|
||||
if r.download {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout}
|
||||
}
|
||||
|
||||
// Search for any cached copies
|
||||
cached, err := cache.read(node)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.Location(), Timeout: r.timeout, CheckedCache: true}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Network timeout. Fetched cached copy\n", node.Location())
|
||||
|
||||
return cached, nil
|
||||
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Fetched remote copy\n", node.Location())
|
||||
|
||||
// Get the checksums
|
||||
checksum := checksum(b)
|
||||
cachedChecksum := cache.readChecksum(node)
|
||||
|
||||
var prompt string
|
||||
if cachedChecksum == "" {
|
||||
// If the checksum doesn't exist, prompt the user to continue
|
||||
prompt = fmt.Sprintf(taskfileUntrustedPrompt, node.Location())
|
||||
} else if checksum != cachedChecksum {
|
||||
// If there is a cached hash, but it doesn't match the expected hash, prompt the user to continue
|
||||
prompt = fmt.Sprintf(taskfileChangedPrompt, node.Location())
|
||||
}
|
||||
|
||||
if prompt != "" {
|
||||
if err := func() error {
|
||||
r.promptMutex.Lock()
|
||||
defer r.promptMutex.Unlock()
|
||||
return r.logger.Prompt(logger.Yellow, prompt, "n", "y", "yes")
|
||||
}(); err != nil {
|
||||
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
|
||||
}
|
||||
|
||||
// Store the checksum
|
||||
if err := cache.writeChecksum(node, checksum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache the file
|
||||
r.logger.VerboseOutf(logger.Magenta, "task: [%s] Caching downloaded file\n", node.Location())
|
||||
if err = cache.write(node, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
@@ -40,7 +41,7 @@ var (
|
||||
// at the given URL with any of the default Taskfile files names. If any of
|
||||
// these match a file, the first matching path will be returned. If no files are
|
||||
// found, an error will be returned.
|
||||
func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL) (*url.URL, error) {
|
||||
func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL, timeout time.Duration) (*url.URL, error) {
|
||||
// Create a new HEAD request for the given URL to check if the resource exists
|
||||
req, err := http.NewRequest("HEAD", u.String(), nil)
|
||||
if err != nil {
|
||||
@@ -50,6 +51,9 @@ func RemoteExists(ctx context.Context, l *logger.Logger, u *url.URL) (*url.URL,
|
||||
// Request the given URL
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, &errors.TaskfileNetworkTimeoutError{URI: u.String(), Timeout: timeout}
|
||||
}
|
||||
return nil, errors.TaskfileFetchFailedError{URI: u.String()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
13
testdata/desc/Taskfile.yml
vendored
Normal file
13
testdata/desc/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 3
|
||||
tasks:
|
||||
build:
|
||||
aliases:
|
||||
- b
|
||||
desc: |
|
||||
Multi-line escription with alias which is super long long long long long long
|
||||
another line
|
||||
third line long long long long long long long long
|
||||
test:
|
||||
aliases:
|
||||
- t
|
||||
desc: Single line description with alias
|
||||
2
testdata/dotenv/parse_error/.env-with-error
vendored
Normal file
2
testdata/dotenv/parse_error/.env-with-error
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
#intentional parse error
|
||||
SOME_VAR
|
||||
8
testdata/dotenv/parse_error/Taskfile.yml
vendored
Normal file
8
testdata/dotenv/parse_error/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
dotenv: ['.env-with-error']
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmd: "true"
|
||||
|
||||
10
testdata/env/Taskfile.yml
vendored
10
testdata/env/Taskfile.yml
vendored
@@ -8,12 +8,14 @@ env:
|
||||
FOO: foo
|
||||
BAR: bar
|
||||
BAZ: "{{.BAZ}}"
|
||||
QUX: from_taskfile
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: local
|
||||
- task: global
|
||||
- task: not-overriden
|
||||
- task: multiple_type
|
||||
|
||||
local:
|
||||
@@ -40,3 +42,11 @@ tasks:
|
||||
BAZ: 1.1
|
||||
cmds:
|
||||
- echo "FOO='$FOO' BAR='$BAR' BAZ='$BAZ'" > multiple_type.txt
|
||||
|
||||
not-overriden:
|
||||
cmds:
|
||||
- echo "QUX='$QUX'" > not-overriden.txt
|
||||
|
||||
overriden:
|
||||
cmds:
|
||||
- echo "QUX='$QUX'" > overriden.txt
|
||||
|
||||
25
testdata/exit_code/Taskfile.yml
vendored
Normal file
25
testdata/exit_code/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
vars:
|
||||
PREFIX: EXIT_CODE=
|
||||
|
||||
tasks:
|
||||
exit-zero:
|
||||
vars:
|
||||
FOO: bar
|
||||
DYNAMIC_FOO:
|
||||
sh: echo 'bar'
|
||||
cmds:
|
||||
- defer: echo FOO={{.FOO}} - DYNAMIC_FOO={{.DYNAMIC_FOO}} - {{.PREFIX}}{{.EXIT_CODE}}
|
||||
- exit 0
|
||||
|
||||
exit-one:
|
||||
vars:
|
||||
FOO: bar
|
||||
DYNAMIC_FOO:
|
||||
sh: echo 'bar'
|
||||
cmds:
|
||||
- defer: echo FOO={{.FOO}} - DYNAMIC_FOO={{.DYNAMIC_FOO}} - {{.PREFIX}}{{.EXIT_CODE}}
|
||||
- exit 1
|
||||
8
testdata/for/cmds/Taskfile.yml
vendored
8
testdata/for/cmds/Taskfile.yml
vendored
@@ -7,6 +7,14 @@ tasks:
|
||||
- for: ["a", "b", "c"]
|
||||
cmd: echo "{{.ITEM}}"
|
||||
|
||||
loop-matrix:
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
OS: ["windows", "linux", "darwin"]
|
||||
ARCH: ["amd64", "arm64"]
|
||||
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
|
||||
|
||||
# Loop over the task's sources
|
||||
loop-sources:
|
||||
sources:
|
||||
|
||||
10
testdata/for/deps/Taskfile.yml
vendored
10
testdata/for/deps/Taskfile.yml
vendored
@@ -9,6 +9,16 @@ tasks:
|
||||
vars:
|
||||
TEXT: "{{.ITEM}}"
|
||||
|
||||
loop-matrix:
|
||||
deps:
|
||||
- for:
|
||||
matrix:
|
||||
OS: ["windows", "linux", "darwin"]
|
||||
ARCH: ["amd64", "arm64"]
|
||||
task: echo
|
||||
vars:
|
||||
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
|
||||
|
||||
# Loop over the task's sources
|
||||
loop-sources:
|
||||
sources:
|
||||
|
||||
1
testdata/includes_flatten/.gitignore
vendored
Normal file
1
testdata/includes_flatten/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.txt
|
||||
12
testdata/includes_flatten/Taskfile.multiple.yml
vendored
Normal file
12
testdata/includes_flatten/Taskfile.multiple.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included:
|
||||
taskfile: ./included
|
||||
flatten: true
|
||||
|
||||
tasks:
|
||||
gen:
|
||||
cmds:
|
||||
- echo "gen multiple"
|
||||
|
||||
3
testdata/includes_flatten/Taskfile.with_default.yml
vendored
Normal file
3
testdata/includes_flatten/Taskfile.with_default.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
version: '3'
|
||||
tasks:
|
||||
default: echo "default from included flatten"
|
||||
15
testdata/includes_flatten/Taskfile.yml
vendored
Normal file
15
testdata/includes_flatten/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included:
|
||||
taskfile: ./included
|
||||
dir: ./included
|
||||
flatten: true
|
||||
with_default:
|
||||
taskfile: ./Taskfile.with_default.yml
|
||||
flatten: true
|
||||
|
||||
tasks:
|
||||
from_entrypoint: echo "from entrypoint"
|
||||
|
||||
|
||||
23
testdata/includes_flatten/included/Taskfile.yml
vendored
Normal file
23
testdata/includes_flatten/included/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
nested:
|
||||
taskfile: ../nested
|
||||
flatten: true
|
||||
|
||||
tasks:
|
||||
gen:
|
||||
cmds:
|
||||
- echo "gen from included"
|
||||
|
||||
with_deps:
|
||||
deps:
|
||||
- gen
|
||||
cmds:
|
||||
- echo "with_deps from included"
|
||||
|
||||
|
||||
pwd:
|
||||
desc: Print working directory
|
||||
cmds:
|
||||
- pwd
|
||||
6
testdata/includes_flatten/nested/Taskfile.yml
vendored
Normal file
6
testdata/includes_flatten/nested/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
from_nested:
|
||||
cmds:
|
||||
- echo "from nested"
|
||||
9
testdata/includes_http/child-taskfile2.yml
vendored
Normal file
9
testdata/includes_http/child-taskfile2.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
third-with-dir-1:
|
||||
taskfile: "{{.INCLUDE_ROOT}}/child-taskfile3.yml"
|
||||
dir: ./dir-1
|
||||
third-with-dir-2:
|
||||
taskfile: "{{.INCLUDE_ROOT}}/child-taskfile3.yml"
|
||||
dir: ./dir-2
|
||||
4
testdata/includes_http/child-taskfile3.yml
vendored
Normal file
4
testdata/includes_http/child-taskfile3.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default: "true"
|
||||
8
testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml
vendored
Normal file
8
testdata/includes_http/root-taskfile-remotefile-empty-dir-1st.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
second-no-dir:
|
||||
taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml"
|
||||
second-with-dir-1:
|
||||
taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml"
|
||||
dir: ./dir-1
|
||||
8
testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml
vendored
Normal file
8
testdata/includes_http/root-taskfile-remotefile-empty-dir-2nd.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
second-with-dir-1:
|
||||
taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml"
|
||||
dir: ./dir-1
|
||||
second-no-dir:
|
||||
taskfile: "{{.INCLUDE_ROOT}}/child-taskfile2.yml"
|
||||
1
testdata/includes_remote/.gitignore
vendored
Normal file
1
testdata/includes_remote/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.txt
|
||||
4
testdata/includes_remote/Taskfile.yml
vendored
Normal file
4
testdata/includes_remote/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
first: "{{.FIRST_REMOTE_URL}}"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user