mirror of
https://github.com/go-task/task.git
synced 2026-06-14 19:31:39 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eb4c9eae8 | ||
|
|
0838d48ee3 | ||
|
|
c64f8818be | ||
|
|
97ffd84d0e | ||
|
|
f2114f09f7 | ||
|
|
9c844850e4 | ||
|
|
68aef2ef0d | ||
|
|
88d644a7e9 | ||
|
|
4b97d4f7f5 | ||
|
|
bc14c633ae | ||
|
|
a29e5d39ca | ||
|
|
f1506ee500 | ||
|
|
6e346de9fb | ||
|
|
99ab2a4d62 | ||
|
|
d4ed7c3cfc | ||
|
|
bc0554575a | ||
|
|
1f4906244b | ||
|
|
52756ab83e | ||
|
|
97dcbe6932 | ||
|
|
e35bf22dd3 | ||
|
|
a36b1b9cec | ||
|
|
1920ee38c3 | ||
|
|
ec2110e58f | ||
|
|
12a1cd6f62 | ||
|
|
9af056e746 | ||
|
|
c8fe450623 | ||
|
|
ab1fe742f3 | ||
|
|
8b72c86ba5 | ||
|
|
28c5f4a635 | ||
|
|
74f69a21cd | ||
|
|
e23dacd6d4 | ||
|
|
58d582941b | ||
|
|
3bbc51949c | ||
|
|
69e0254a99 | ||
|
|
1091a914bd | ||
|
|
ecc65a218e | ||
|
|
426ed7eff6 | ||
|
|
73aba36309 | ||
|
|
cb393ccd3a | ||
|
|
347fcf9f67 | ||
|
|
fce7575b03 | ||
|
|
2da7ddc399 | ||
|
|
1c1be683ab | ||
|
|
4be1050234 | ||
|
|
2efb3533ec | ||
|
|
aa6c7e4b94 | ||
|
|
63c50d13ee | ||
|
|
c1e127e42f | ||
|
|
9e38e8a4db | ||
|
|
b4c95d6b0b | ||
|
|
c4766e2611 | ||
|
|
796097e3ab | ||
|
|
c7d9efebf9 | ||
|
|
8f4306d321 | ||
|
|
435f086cb7 | ||
|
|
01c9158120 | ||
|
|
e235d77d64 | ||
|
|
dbe8131b75 | ||
|
|
0a9d76515e | ||
|
|
0ce1af9ee0 | ||
|
|
c4452d2698 | ||
|
|
491888f6c0 | ||
|
|
e4158dc5e4 | ||
|
|
0307ca8ac6 | ||
|
|
156a273351 | ||
|
|
d6d51a2f8b | ||
|
|
a98b41d657 | ||
|
|
87ec78fbaa | ||
|
|
957bff4b89 | ||
|
|
321f7b59d8 | ||
|
|
41a9316523 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,3 +1,3 @@
|
||||
github: andreynering
|
||||
github: [andreynering, pd93]
|
||||
open_collective: task
|
||||
custom: 'https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=GSVDU63RKG45A¤cy_code=USD&source=url'
|
||||
custom: https://taskfile.dev/donate/
|
||||
|
||||
@@ -10,6 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
|
||||
29
.github/workflows/issue-closed.yml
vendored
Normal file
29
.github/workflows/issue-closed.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: issue closed
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
issue-closed:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
const labels = await github.paginate(
|
||||
github.rest.issues.listLabelsOnIssue, {
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
}
|
||||
)
|
||||
if (labels.find(label => label.name === 'needs triage')) {
|
||||
github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'needs triage'
|
||||
})
|
||||
}
|
||||
3
.github/workflows/issue-needs-triage.yml
vendored
3
.github/workflows/issue-needs-triage.yml
vendored
@@ -5,11 +5,12 @@ on:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
needs-triage:
|
||||
issue-needs-triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{secrets.GH_PAT}}
|
||||
script: |
|
||||
const labels = await github.paginate(
|
||||
github.rest.issues.listLabelsOnIssue, {
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -14,11 +14,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.46.1
|
||||
version: v1.51.1
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19.x
|
||||
go-version: 1.20.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.18.x, 1.19.x]
|
||||
go-version: [1.19.x, 1.20.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
|
||||
12
.golangci.yml
Normal file
12
.golangci.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
# NOTE(@andreynering): The linters listed here are additions on top of
|
||||
# those enabled by default:
|
||||
#
|
||||
# https://golangci-lint.run/usage/linters/#enabled-by-default
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- goimports
|
||||
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/go-task/task
|
||||
@@ -6,15 +6,15 @@ build:
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- '386'
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 6
|
||||
- '6'
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
goarch: '386'
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,5 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## v3.22.0 - 2023-03-10
|
||||
|
||||
- Add a brand new `--global` (`-g`) flag that will run a Taskfile from your
|
||||
`$HOME` directory. This is useful to have automation that you can run from
|
||||
anywhere in your system!
|
||||
([Documentation](https://taskfile.dev/usage/#running-a-global-taskfile), [#1029](https://github.com/go-task/task/pull/1029) by @andreynering).
|
||||
- Add ability to set `error_only: true` on the `group` output mode. This will
|
||||
instruct Task to only print a command output if it returned with a non-zero
|
||||
exit code
|
||||
([#664](https://github.com/go-task/task/issues/664), [#1022](https://github.com/go-task/task/pull/1022) by @jaedle).
|
||||
- Fixed bug where `.task/checksum` file was sometimes not being created when
|
||||
task also declares a `status:`
|
||||
([#840](https://github.com/go-task/task/issues/840), [#1035](https://github.com/go-task/task/pull/1035) by @harelwa, [#1037](https://github.com/go-task/task/pull/1037) by @pd93).
|
||||
- Refactored and decoupled fingerprinting from the main Task executor ([#1039](https://github.com/go-task/task/issues/1039) by @pd93).
|
||||
- Fixed deadlock issue when using `run: once`
|
||||
([#715](https://github.com/go-task/task/issues/715), [#1025](https://github.com/go-task/task/pull/1025) by @theunrepentantgeek).
|
||||
|
||||
## v3.21.0 - 2023-02-22
|
||||
|
||||
- Added new `TASK_VERSION` special variable
|
||||
([#990](https://github.com/go-task/task/issues/990), [#1014](https://github.com/go-task/task/pull/1014) by @ja1code).
|
||||
- Fixed a bug where tasks were sometimes incorrectly marked as internal ([#1007](https://github.com/go-task/task/pull/1007) by @pd93).
|
||||
- Update to Go 1.20 (bump minimum version to 1.19) ([#1010](https://github.com/go-task/task/pull/1010) by @pd93)
|
||||
- Added environment variable `FORCE_COLOR` support to force color output. Usefull for environments without TTY ([#1003](https://github.com/go-task/task/pull/1003) by @automation-stack)
|
||||
|
||||
## v3.20.0 - 2023-01-14
|
||||
|
||||
- Improve behavior and performance of status checking when using the
|
||||
`timestamp` mode
|
||||
([#976](https://github.com/go-task/task/issues/976), [#977](https://github.com/go-task/task/pull/977) by @aminya).
|
||||
- Performance optimizations were made for large Taskfiles
|
||||
([#982](https://github.com/go-task/task/pull/982) by @pd93).
|
||||
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
|
||||
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
|
||||
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
|
||||
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
|
||||
choose in which platforms that given task or command will be run on. Possible
|
||||
values are operating system (GOOS), architecture (GOARCH) or a combination of
|
||||
the two. Example: `platforms: [linux]`, `platforms: [amd64]` or
|
||||
`platforms: [linux/amd64]`. Other platforms will be skipped
|
||||
([#978](https://github.com/go-task/task/issues/978), [#980](https://github.com/go-task/task/pull/980) by @leaanthony).
|
||||
|
||||
## v3.19.1 - 2022-12-31
|
||||
|
||||
- Small bug fix: closing `Taskfile.yml` once we're done reading it
|
||||
([#963](https://github.com/go-task/task/issues/963), [#964](https://github.com/go-task/task/pull/964) by @HeCorr).
|
||||
- Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file
|
||||
([#961](https://github.com/go-task/task/issues/961), [#971](https://github.com/go-task/task/pull/971) by @pd93).
|
||||
- Fixed a bug where watch intervals set in the Taskfile were not being respected ([#969](https://github.com/go-task/task/pull/969), [#970](https://github.com/go-task/task/pull/970) by @pd93)
|
||||
- Add `--json` flag (alias `-j`) with the intent to improve support for code
|
||||
editors and add room to other possible integrations. This is basic for now,
|
||||
but we plan to add more info in the near future
|
||||
([#936](https://github.com/go-task/task/pull/936) by @davidalpert, [#764](https://github.com/go-task/task/issues/764)).
|
||||
|
||||
## v3.19.0 - 2022-12-05
|
||||
|
||||
- Installation via npm now supports [pnpm](https://pnpm.io/) as well
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
|
||||
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://fosstodon.org/@task">Mastodon</a> | <a href="https://discord.gg/6TY36E39UK">Discord</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
49
Taskfile.yml
49
Taskfile.yml
@@ -6,13 +6,6 @@ includes:
|
||||
taskfile: ./docs
|
||||
dir: ./docs
|
||||
|
||||
vars:
|
||||
GIT_COMMIT:
|
||||
sh: git log -n 1 --format=%h
|
||||
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
|
||||
@@ -28,7 +21,29 @@ tasks:
|
||||
sources:
|
||||
- './**/*.go'
|
||||
cmds:
|
||||
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
|
||||
- go install -v -ldflags="-w -s -X '{{.VERSION_VAR}}={{.GIT_COMMIT}}'" ./cmd/task
|
||||
vars:
|
||||
VERSION_VAR: github.com/go-task/task/v3/internal/version.version
|
||||
GIT_COMMIT:
|
||||
sh: git log -n 1 --format=%h
|
||||
|
||||
generate:
|
||||
desc: Runs Go generate to create mocks
|
||||
aliases: [gen, g]
|
||||
deps: [install:mockgen]
|
||||
sources:
|
||||
- "internal/fingerprint/checker.go"
|
||||
generates:
|
||||
- "internal/fingerprint/checker_mock.go"
|
||||
cmds:
|
||||
- mockgen -source=internal/fingerprint/checker.go -destination=internal/fingerprint/checker_mock.go -package=fingerprint
|
||||
|
||||
install:mockgen:
|
||||
desc: Installs mockgen; a tool to generate mock files
|
||||
status:
|
||||
- command -v mockgen &>/dev/null
|
||||
cmds:
|
||||
- go install github.com/golang/mock/mockgen@latest
|
||||
|
||||
mod:
|
||||
desc: Downloads and tidy Go modules
|
||||
@@ -47,6 +62,7 @@ tasks:
|
||||
aliases: [l]
|
||||
sources:
|
||||
- './**/*.go'
|
||||
- .golangci.yml
|
||||
cmds:
|
||||
- golangci-lint run
|
||||
|
||||
@@ -72,12 +88,18 @@ tasks:
|
||||
deps: [install]
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}}
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
|
||||
test:signals:
|
||||
desc: Runs test suite with signals tests included
|
||||
test:all:
|
||||
desc: Runs test suite with signals and watch tests included
|
||||
deps: [install, sleepit:build]
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}} -tags signals
|
||||
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
|
||||
test-release:
|
||||
desc: Tests release process without publishing
|
||||
@@ -92,7 +114,7 @@ tasks:
|
||||
- rm {{.FILE}}
|
||||
- 'echo "---" >> {{.FILE}}'
|
||||
- 'echo "slug: /changelog/" >> {{.FILE}}'
|
||||
- 'echo "sidebar_position: 6" >> {{.FILE}}'
|
||||
- 'echo "sidebar_position: 7" >> {{.FILE}}'
|
||||
- 'echo "---" >> {{.FILE}}'
|
||||
- 'echo "" >> {{.FILE}}'
|
||||
- 'cat CHANGELOG.md >> {{.FILE}}'
|
||||
@@ -105,4 +127,7 @@ tasks:
|
||||
packages:
|
||||
cmds:
|
||||
- echo '{{.GO_PACKAGES}}'
|
||||
vars:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
silent: true
|
||||
|
||||
@@ -130,11 +130,11 @@ func supervisor(
|
||||
// The goroutine will prepend its prints with the prefix `name`.
|
||||
// The goroutine will simulate some work and will terminate when one of the following
|
||||
// conditions happens:
|
||||
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
|
||||
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
|
||||
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
|
||||
// it should send a message; if instead it wants an asynchronous cancel, it should
|
||||
// close the channel.
|
||||
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
|
||||
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
|
||||
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
|
||||
// it should send a message; if instead it wants an asynchronous cancel, it should
|
||||
// close the channel.
|
||||
func worker(
|
||||
canceled <-chan struct{},
|
||||
howlong time.Duration,
|
||||
|
||||
122
cmd/task/task.go
122
cmd/task/task.go
@@ -6,8 +6,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
@@ -15,13 +15,10 @@ import (
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/args"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
ver "github.com/go-task/task/v3/internal/version"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
var (
|
||||
version = ""
|
||||
)
|
||||
|
||||
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--taskfile] [--dry] [--summary] [task...]
|
||||
|
||||
Runs the specified task(s). Falls back to the "default" task if no task name
|
||||
@@ -59,6 +56,7 @@ func main() {
|
||||
init bool
|
||||
list bool
|
||||
listAll bool
|
||||
listJson bool
|
||||
status bool
|
||||
force bool
|
||||
watch bool
|
||||
@@ -73,35 +71,39 @@ func main() {
|
||||
entrypoint string
|
||||
output taskfile.Output
|
||||
color bool
|
||||
interval string
|
||||
interval time.Duration
|
||||
global bool
|
||||
)
|
||||
|
||||
pflag.BoolVar(&versionFlag, "version", false, "show Task version")
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "shows Task usage")
|
||||
pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yaml in the current folder")
|
||||
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.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date")
|
||||
pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date")
|
||||
pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task")
|
||||
pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode")
|
||||
pflag.BoolVarP(&silent, "silent", "s", false, "disables echoing")
|
||||
pflag.BoolVarP(¶llel, "parallel", "p", false, "executes tasks provided on command line in parallel")
|
||||
pflag.BoolVarP(&dry, "dry", "n", false, "compiles and prints tasks in the order that they would be run, without executing them")
|
||||
pflag.BoolVar(&summary, "summary", false, "show summary about a task")
|
||||
pflag.BoolVarP(&exitCode, "exit-code", "x", false, "pass-through the exit code of the task command")
|
||||
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
|
||||
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
|
||||
pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
|
||||
pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "message template to print before a task's grouped output")
|
||||
pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output")
|
||||
pflag.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.StringVarP(&interval, "interval", "I", "5s", "interval to watch for changes")
|
||||
pflag.BoolVar(&versionFlag, "version", false, "Show Task version.")
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "Shows Task usage.")
|
||||
pflag.BoolVarP(&init, "init", "i", false, "Creates a new Taskfile.yaml in the current folder.")
|
||||
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.")
|
||||
pflag.BoolVar(&status, "status", false, "Exits with non-zero exit code if any of the given tasks is not up-to-date.")
|
||||
pflag.BoolVarP(&force, "force", "f", false, "Forces execution even when the task is up-to-date.")
|
||||
pflag.BoolVarP(&watch, "watch", "w", false, "Enables watch of the given task.")
|
||||
pflag.BoolVarP(&verbose, "verbose", "v", false, "Enables verbose mode.")
|
||||
pflag.BoolVarP(&silent, "silent", "s", false, "Disables echoing.")
|
||||
pflag.BoolVarP(¶llel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
|
||||
pflag.BoolVarP(&dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
|
||||
pflag.BoolVar(&summary, "summary", false, "Show summary about a task.")
|
||||
pflag.BoolVarP(&exitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
|
||||
pflag.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
|
||||
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
pflag.StringVarP(&output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
|
||||
pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
|
||||
pflag.StringVar(&output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
|
||||
pflag.BoolVar(&output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
|
||||
pflag.BoolVarP(&color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
|
||||
pflag.IntVarP(&concurrency, "concurrency", "C", 0, "Limit number 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/Taskfile.{yml,yaml}.")
|
||||
pflag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
fmt.Printf("Task version: %s\n", getVersion())
|
||||
fmt.Printf("Task version: %s\n", ver.GetVersion())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,6 +123,19 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if global && dir != "" {
|
||||
log.Fatal("task: You can't set both --global and --dir")
|
||||
return
|
||||
}
|
||||
if global {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatal("task: Failed to get user home directory: %w", err)
|
||||
return
|
||||
}
|
||||
dir = home
|
||||
}
|
||||
|
||||
if dir != "" && entrypoint != "" {
|
||||
log.Fatal("task: You can't set both --dir and --taskfile")
|
||||
return
|
||||
@@ -139,6 +154,10 @@ func main() {
|
||||
log.Fatal("task: You can't set --output-group-end without --output=group")
|
||||
return
|
||||
}
|
||||
if output.Group.ErrorOnly {
|
||||
log.Fatal("task: You can't set --output-group-error-only without --output=group")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
e := task.Executor{
|
||||
@@ -162,7 +181,12 @@ func main() {
|
||||
OutputStyle: output,
|
||||
}
|
||||
|
||||
if (list || listAll) && silent {
|
||||
var listOptions = task.NewListOptions(list, listAll, listJson)
|
||||
if err := listOptions.Validate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if (listOptions.ShouldListTasks()) && silent {
|
||||
e.ListTaskNames(listAll)
|
||||
return
|
||||
}
|
||||
@@ -170,22 +194,10 @@ func main() {
|
||||
if err := e.Setup(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if list {
|
||||
if ok := e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()); !ok {
|
||||
e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if listAll {
|
||||
if ok := e.ListTasks(task.FilterOutInternal()); !ok {
|
||||
e.Logger.Outf(logger.Yellow, "task: No tasks available")
|
||||
if listOptions.ShouldListTasks() {
|
||||
if foundTasks, err := e.ListTasks(listOptions); !foundTasks || err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -200,7 +212,7 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if v >= 3.0 {
|
||||
if e.Taskfile.Version.Compare(taskfile.V3) >= 0 {
|
||||
calls, globals = args.ParseV3(tasksAndVars...)
|
||||
} else {
|
||||
calls, globals = args.ParseV2(tasksAndVars...)
|
||||
@@ -254,21 +266,3 @@ func getArgs() ([]string, string, error) {
|
||||
}
|
||||
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
|
||||
}
|
||||
|
||||
func getVersion() string {
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info.Main.Version == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
version = info.Main.Version
|
||||
if info.Main.Sum != "" {
|
||||
version += fmt.Sprintf(" (%s)", info.Main.Sum)
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
@@ -35,3 +35,8 @@ tasks:
|
||||
summary: Requires GIT_USER and GIT_PASS envs to be previous set
|
||||
cmds:
|
||||
- npx docusaurus deploy
|
||||
|
||||
upgrade:
|
||||
desc: Upgrade Docusaurus
|
||||
cmds:
|
||||
- yarn upgrade @docusaurus/core@latest @docusaurus/preset-classic@latest
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const GITHUB_URL = 'https://github.com/go-task/task';
|
||||
const TWITTER_URL = 'https://twitter.com/taskfiledev';
|
||||
const MASTODON_URL = 'https://fosstodon.org/@task';
|
||||
const DISCORD_URL = 'https://discord.gg/6TY36E39UK';
|
||||
const CHINESE_URL = 'https://task-zh.readthedocs.io/zh_CN/latest/';
|
||||
|
||||
module.exports = {
|
||||
GITHUB_URL,
|
||||
TWITTER_URL,
|
||||
CHINESE_URL,
|
||||
DISCORD_URL,
|
||||
CHINESE_URL
|
||||
GITHUB_URL,
|
||||
MASTODON_URL,
|
||||
TWITTER_URL
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ variable
|
||||
| `-n` | `--dry` | `bool` | `false` | Compiles and prints tasks in the order that they would be run, without executing them. |
|
||||
| `-x` | `--exit-code` | `bool` | `false` | Pass-through the exit code of the task command. |
|
||||
| `-f` | `--force` | `bool` | `false` | Forces execution even when the task is up-to-date. |
|
||||
| `-g` | `--global` | `bool` | `false` | Runs global Taskfile, from `$HOME/Taskfile.{yml,yaml}`. |
|
||||
| `-h` | `--help` | `bool` | `false` | Shows Task usage. |
|
||||
| `-i` | `--init` | `bool` | `false` | Creates a new Taskfile.yaml in the current folder. |
|
||||
| `-I` | `--interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
|
||||
@@ -36,6 +37,7 @@ variable
|
||||
| `-o` | `--output` | `string` | Default set in the Taskfile or `intervealed` | Sets output style: [`interleaved`/`group`/`prefixed`]. |
|
||||
| | `--output-group-begin` | `string` | | Message template to print before a task's grouped output. |
|
||||
| | `--output-group-end` | `string` | | Message template to print after a task's grouped output. |
|
||||
| | `--output-group-error-only` | `bool` | `false` | Swallow command output on zero exit code. |
|
||||
| `-p` | `--parallel` | `bool` | `false` | Executes tasks provided on command line in parallel. |
|
||||
| `-s` | `--silent` | `bool` | `false` | Disables echoing. |
|
||||
| | `--status` | `bool` | `false` | Exits with non-zero exit code if any of the given tasks is not up-to-date. |
|
||||
@@ -58,6 +60,7 @@ There are some special variables that is available on the templating system:
|
||||
| `USER_WORKING_DIR` | The absolute path of the directory `task` was called from. |
|
||||
| `CHECKSUM` | The checksum of the files listed in `sources`. Only available within the `status` prop and if method is set to `checksum`. |
|
||||
| `TIMESTAMP` | The date object of the greatest timestamp of the files listes in `sources`. Only available within the `status` prop and if method is set to `timestamp`. |
|
||||
| `TASK_VERSION` | The current version of task. |
|
||||
|
||||
## ENV
|
||||
|
||||
@@ -73,6 +76,7 @@ Some environment variables can be overriden to adjust Task behavior.
|
||||
| `TASK_COLOR_YELLOW` | `33` | Color used for yellow. |
|
||||
| `TASK_COLOR_MAGENTA` | `35` | Color used for magenta. |
|
||||
| `TASK_COLOR_RED` | `31` | Color used for red. |
|
||||
| `FORCE_COLOR` | | Force color output usage. |
|
||||
|
||||
## Schema
|
||||
|
||||
@@ -91,6 +95,8 @@ Some environment variables can be overriden to adjust Task behavior.
|
||||
| `dotenv` | `[]string` | | A list of `.env` file paths to be parsed. |
|
||||
| `run` | `string` | `always` | Default 'run' option for this Taskfile. Available options: `always`, `once` and `when_changed`. |
|
||||
| `interval` | `string` | `5s` | Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid [Go Duration](https://pkg.go.dev/time#ParseDuration). |
|
||||
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
|
||||
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
|
||||
|
||||
### Include
|
||||
|
||||
@@ -139,6 +145,9 @@ includes:
|
||||
| `prefix` | `string` | | Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`. |
|
||||
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing commands. |
|
||||
| `run` | `string` | The one declared globally in the Taskfile or `always` | Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`. |
|
||||
| `platforms` | `[]string` | All platforms | Specifies which platforms the task should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Task will be skipped otherwise. |
|
||||
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
|
||||
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
|
||||
|
||||
:::info
|
||||
|
||||
@@ -189,6 +198,9 @@ tasks:
|
||||
| `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. |
|
||||
| `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. |
|
||||
| `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. |
|
||||
| `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/go/build/syslist.go). Command will be skipped otherwise. |
|
||||
| `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). |
|
||||
| `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). |
|
||||
|
||||
:::info
|
||||
|
||||
|
||||
@@ -1,10 +1,64 @@
|
||||
---
|
||||
slug: /changelog/
|
||||
sidebar_position: 6
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Changelog
|
||||
|
||||
## v3.22.0 - 2023-03-10
|
||||
|
||||
- Add a brand new `--global` (`-g`) flag that will run a Taskfile from your
|
||||
`$HOME` directory. This is useful to have automation that you can run from
|
||||
anywhere in your system!
|
||||
([Documentation](https://taskfile.dev/usage/#running-a-global-taskfile), [#1029](https://github.com/go-task/task/pull/1029) by @andreynering).
|
||||
- Add ability to set `error_only: true` on the `group` output mode. This will
|
||||
instruct Task to only print a command output if it returned with a non-zero
|
||||
exit code
|
||||
([#664](https://github.com/go-task/task/issues/664), [#1022](https://github.com/go-task/task/pull/1022) by @jaedle).
|
||||
- Fixed bug where `.task/checksum` file was sometimes not being created when
|
||||
task also declares a `status:`
|
||||
([#840](https://github.com/go-task/task/issues/840), [#1035](https://github.com/go-task/task/pull/1035) by @harelwa, [#1037](https://github.com/go-task/task/pull/1037) by @pd93).
|
||||
- Refactored and decoupled fingerprinting from the main Task executor ([#1039](https://github.com/go-task/task/issues/1039) by @pd93).
|
||||
- Fixed deadlock issue when using `run: once`
|
||||
([#715](https://github.com/go-task/task/issues/715), [#1025](https://github.com/go-task/task/pull/1025) by @theunrepentantgeek).
|
||||
|
||||
## v3.21.0 - 2023-02-22
|
||||
|
||||
- Added new `TASK_VERSION` special variable
|
||||
([#990](https://github.com/go-task/task/issues/990), [#1014](https://github.com/go-task/task/pull/1014) by @ja1code).
|
||||
- Fixed a bug where tasks were sometimes incorrectly marked as internal ([#1007](https://github.com/go-task/task/pull/1007) by @pd93).
|
||||
- Update to Go 1.20 (bump minimum version to 1.19) ([#1010](https://github.com/go-task/task/pull/1010) by @pd93)
|
||||
- Added environment variable `FORCE_COLOR` support to force color output. Usefull for environments without TTY ([#1003](https://github.com/go-task/task/pull/1003) by @automation-stack)
|
||||
|
||||
## v3.20.0 - 2023-01-14
|
||||
|
||||
- Improve behavior and performance of status checking when using the
|
||||
`timestamp` mode
|
||||
([#976](https://github.com/go-task/task/issues/976), [#977](https://github.com/go-task/task/pull/977) by @aminya).
|
||||
- Performance optimizations were made for large Taskfiles
|
||||
([#982](https://github.com/go-task/task/pull/982) by @pd93).
|
||||
- Add ability to configure options for the [`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
|
||||
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) builtins
|
||||
([#908](https://github.com/go-task/task/issues/908), [#929](https://github.com/go-task/task/pull/929) by @pd93, [Documentation](http://taskfile.dev/usage/#set-and-shopt)).
|
||||
- Add new `platforms:` attribute to `task` and `cmd`, so it's now possible to
|
||||
choose in which platforms that given task or command will be run on. Possible
|
||||
values are operating system (GOOS), architecture (GOARCH) or a combination of
|
||||
the two. Example: `platforms: [linux]`, `platforms: [amd64]` or
|
||||
`platforms: [linux/amd64]`. Other platforms will be skipped
|
||||
([#978](https://github.com/go-task/task/issues/978), [#980](https://github.com/go-task/task/pull/980) by @leaanthony).
|
||||
|
||||
## v3.19.1 - 2022-12-31
|
||||
|
||||
- Small bug fix: closing `Taskfile.yml` once we're done reading it
|
||||
([#963](https://github.com/go-task/task/issues/963), [#964](https://github.com/go-task/task/pull/964) by @HeCorr).
|
||||
- Fixes a bug in v2 that caused a panic when using a `Taskfile_{{OS}}.yml` file
|
||||
([#961](https://github.com/go-task/task/issues/961), [#971](https://github.com/go-task/task/pull/971) by @pd93).
|
||||
- Fixed a bug where watch intervals set in the Taskfile were not being respected ([#969](https://github.com/go-task/task/pull/969), [#970](https://github.com/go-task/task/pull/970) by @pd93)
|
||||
- Add `--json` flag (alias `-j`) with the intent to improve support for code
|
||||
editors and add room to other possible integrations. This is basic for now,
|
||||
but we plan to add more info in the near future
|
||||
([#936](https://github.com/go-task/task/pull/936) by @davidalpert, [#764](https://github.com/go-task/task/issues/764)).
|
||||
|
||||
## v3.19.0 - 2022-12-05
|
||||
|
||||
- Installation via npm now supports [pnpm](https://pnpm.io/) as well
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
slug: /community/
|
||||
sidebar_position: 6
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Community
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
slug: /contributing/
|
||||
sidebar_position: 7
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
slug: /donate/
|
||||
sidebar_position: 10
|
||||
sidebar_position: 12
|
||||
---
|
||||
|
||||
# Donate
|
||||
@@ -11,10 +11,23 @@ channels listed below.
|
||||
This is just a way of saying "thank you", it won't give you any benefits like
|
||||
higher priority on issues or something similar.
|
||||
|
||||
Companies who donate at least $50/month will be featured as a "Gold Sponsor"
|
||||
in the website homepage and on the GitHub repository README. Make contact with
|
||||
[@andreynering] with the logo you want to be shown.
|
||||
Suspect businesses (gambling, casinos, etc) won't be allowed, though.
|
||||
|
||||
## GitHub Sponsors
|
||||
|
||||
The preferred way to donate to the maintainers is via GitHub Sponsors.
|
||||
Just use the following links to do your donation:
|
||||
|
||||
- [@andreynering](https://github.com/sponsors/andreynering)
|
||||
- [@pd93](https://github.com/sponsors/pd93)
|
||||
|
||||
## Open Collective
|
||||
|
||||
Task is on [Open Collective](https://opencollective.com/task) and you have
|
||||
these options to donate:
|
||||
If you prefer [Open Collective](https://opencollective.com/task) you can donate
|
||||
by using these links:
|
||||
|
||||
- [$2 per month](https://opencollective.com/task/contribute/backer-4034/checkout)
|
||||
- [$5 per month](https://opencollective.com/task/contribute/supporter-8404/checkout)
|
||||
@@ -22,15 +35,15 @@ these options to donate:
|
||||
- [$50 per month](https://opencollective.com/task/contribute/sponsor-28775/checkout)
|
||||
- [Custom value - One-time donation option supported](https://opencollective.com/task/donate)
|
||||
|
||||
## GitHub Sponsors
|
||||
|
||||
- [@andreynering](https://github.com/sponsors/andreynering)
|
||||
|
||||
## PayPal
|
||||
|
||||
You can donate to [@andreynering] via PayPal as well:
|
||||
|
||||
- [Any value - One-time donation](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=GSVDU63RKG45A¤cy_code=USD&source=url)
|
||||
|
||||
## PIX (Brazil only)
|
||||
|
||||
If you're Brazilian, you can donate any value by
|
||||
And if you're Brazilian, you can also donate to [@andreynering] via PIX by
|
||||
[using this QR Code](/img/pix.png).
|
||||
|
||||
[@andreynering]: https://github.com/andreynering
|
||||
|
||||
54
docs/docs/faq.md
Normal file
54
docs/docs/faq.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
slug: /faq/
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# FAQ
|
||||
|
||||
This page contains a list of frequently asked questions about Task.
|
||||
|
||||
- [Why won't my task update my shell environment?](#why-wont-my-task-update-my-shell-environment)
|
||||
- ['x' builtin command doesn't work on Windows](#x-builtin-command-doesnt-work-on-windows)
|
||||
|
||||
## Why won't my task update my shell environment?
|
||||
|
||||
This is a limitation of how shells work. Task runs as a subprocess of your
|
||||
current shell, so it can't change the environment of the shell that started it.
|
||||
This limitation is shared by other task runners and build tools too.
|
||||
|
||||
A common way to work around this is to create a task that will generate output
|
||||
that can be parsed by your shell. For example, to set an environment variable on
|
||||
your shell you can write a task like this:
|
||||
|
||||
```yaml
|
||||
my-shell-env:
|
||||
cmds:
|
||||
- echo "export FOO=foo"
|
||||
- echo "export BAR=bar"
|
||||
```
|
||||
|
||||
Now run `eval $(task my-shell-env)` and the variables `$FOO` and `$BAR` will be
|
||||
available in your shell.
|
||||
|
||||
## 'x' builtin command doesn't work on Windows
|
||||
|
||||
The default shell on Windows (`cmd` and `powershell`) do not have commands like
|
||||
`rm` and `cp` available as builtins. This means that these commands won't work.
|
||||
If you want to make your Taskfile fully cross-platform, you'll need to work
|
||||
around this limitation using one of the following methods:
|
||||
|
||||
- Use the `{{OS}}` function to run an OS-specific script.
|
||||
- Use something like `{{if eq OS "windows"}}powershell {{end}}<my_cmd>` to
|
||||
detect windows and run the command in Powershell directly.
|
||||
- Use a shell on Windows that supports these commands as builtins, such as [Git
|
||||
Bash] or [WSL].
|
||||
|
||||
We want to make improvements to this part of Task and the issues below track
|
||||
this work. Constructive comments and contributions are very welcome!
|
||||
|
||||
- [#197](https://github.com/go-task/task/issues/197)
|
||||
- [mvdan/sh#93](https://github.com/mvdan/sh/issues/93)
|
||||
- [mvdan/sh#97](https://github.com/mvdan/sh/issues/97)
|
||||
|
||||
[Git Bash]: https://gitforwindows.org/
|
||||
[WSL]: https://learn.microsoft.com/en-us/windows/wsl/install
|
||||
@@ -18,6 +18,15 @@ Task is as simple as running:
|
||||
brew install go-task/tap/go-task
|
||||
```
|
||||
|
||||
The above Formula is [maintained by ourselves](https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb).
|
||||
|
||||
Recently, Task was also made available [on the official Homebrew repository](https://formulae.brew.sh/formula/go-task),
|
||||
so you also have that option if you prefer:
|
||||
|
||||
```bash
|
||||
brew install go-task
|
||||
```
|
||||
|
||||
### Snap
|
||||
|
||||
Task is available in [Snapcraft][snapcraft], but keep in mind that your
|
||||
@@ -161,9 +170,10 @@ This installation method is community owned.
|
||||
|
||||
### Go Modules
|
||||
|
||||
First, make sure you have [Go][go] properly installed and setup.
|
||||
Ensure that you have a supported version of [Go][go] properly installed and setup. You can find
|
||||
the minimum required version of Go in the [go.mod](https://github.com/go-task/task/blob/master/go.mod#L3) file.
|
||||
|
||||
You can easily install the latest release globally by running:
|
||||
You can then install the latest release globally by running:
|
||||
|
||||
```bash
|
||||
go install github.com/go-task/task/v3/cmd/task@latest
|
||||
@@ -175,12 +185,6 @@ Or you can install into another directory:
|
||||
env GOBIN=/bin go install github.com/go-task/task/v3/cmd/task@latest
|
||||
```
|
||||
|
||||
If using Go 1.15 or earlier, instead use:
|
||||
|
||||
```bash
|
||||
env GO111MODULE=on go get -u github.com/go-task/task/v3/cmd/task@latest
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
For CI environments we recommend using the [install script](#get-the-binary)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
slug: /releasing/
|
||||
sidebar_position: 8
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
# Releasing
|
||||
@@ -41,7 +41,7 @@ the [Snapcraft dashboard][snapcraftdashboard].
|
||||
|
||||
Scoop is a command-line package manager for the Windows operating system.
|
||||
Scoop package manifests are maintained by the community.
|
||||
Scoop owners usually take care of updating versions there by editing [this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json).
|
||||
Scoop owners usually take care of updating versions there by editing [this file](https://github.com/ScoopInstaller/Main/blob/master/bucket/task.json).
|
||||
If you think its Task version is outdated, open an issue to let us know.
|
||||
|
||||
# Nix
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
slug: /styleguide/
|
||||
sidebar_position: 5
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Styleguide
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
slug: /taskfile-versions/
|
||||
sidebar_position: 9
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
# Taskfile Versions
|
||||
|
||||
@@ -9,7 +9,7 @@ sidebar_position: 3
|
||||
|
||||
Create a file called `Taskfile.yml` in the root of your project.
|
||||
The `cmds` attribute should contain the commands of a task.
|
||||
The example below allows compiling a Go app and uses [Minify][minify] to concat
|
||||
The example below allows compiling a Go app and uses [esbuild](https://esbuild.github.io/) to concat
|
||||
and minify multiple CSS files into a single one.
|
||||
|
||||
```yaml
|
||||
@@ -22,7 +22,7 @@ tasks:
|
||||
|
||||
assets:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
- esbuild --bundle --minify css/index.css > public/bundle.css
|
||||
```
|
||||
|
||||
Running the tasks is as simple as running:
|
||||
@@ -81,6 +81,40 @@ In this example, we can run `cd <service>` and `task up` and as long as the
|
||||
`<service>` directory contains a `docker-compose.yml`, the Docker composition will be
|
||||
brought up.
|
||||
|
||||
### Running a global Taskfile
|
||||
|
||||
If you call Task with the `--global` (alias `-g`) flag, it will look for your
|
||||
home directory instead of your working directory. In short, Task will look for
|
||||
a Taskfile on either `$HOME/Taskfile.yml` or `$HOME/Taskfile.yaml` paths.
|
||||
|
||||
This is useful to have automation that you can run from anywhere in your
|
||||
system!
|
||||
|
||||
:::info
|
||||
|
||||
When running your global Taskfile with `-g`, tasks will run on `$HOME` by
|
||||
default, and not on your working directory!
|
||||
|
||||
As mentioned in the previous section, the `{{.USER_WORKING_DIR}}` special
|
||||
variable can be very handy here to run stuff on the directory you're calling
|
||||
`task -g` from.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
from-home:
|
||||
cmds:
|
||||
- pwd
|
||||
|
||||
from-working-directory:
|
||||
dir: '{{.USER_WORKING_DIR}}'
|
||||
cmds:
|
||||
- pwd
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Environment variables
|
||||
|
||||
### Task
|
||||
@@ -384,7 +418,7 @@ tasks:
|
||||
|
||||
assets:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
- esbuild --bundle --minify css/index.css > public/bundle.css
|
||||
```
|
||||
|
||||
In the above example, `assets` will always run right before `build` if you run
|
||||
@@ -401,11 +435,11 @@ tasks:
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- minify -o public/script.js src/js
|
||||
- esbuild --bundle --minify js/index.js > public/bundle.js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
- esbuild --bundle --minify css/index.css > public/bundle.css
|
||||
```
|
||||
|
||||
If there is more than one dependency, they always run in parallel for better
|
||||
@@ -439,6 +473,78 @@ tasks:
|
||||
- echo {{.TEXT}}
|
||||
```
|
||||
|
||||
## Platform specific tasks and commands
|
||||
|
||||
If you want to restrict the running of tasks to explicit platforms, this can be achieved
|
||||
using the `platforms:` key. Tasks can be restricted to a specific OS, architecture or a
|
||||
combination of both.
|
||||
On a mismatch, the task or command will be skipped, and no error will be thrown.
|
||||
|
||||
The values allowed as OS or Arch are valid `GOOS` and `GOARCH` values, as
|
||||
defined by the Go language
|
||||
[here](https://github.com/golang/go/blob/master/src/go/build/syslist.go).
|
||||
|
||||
The `build-windows` task below will run only on Windows, and on any architecture:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build-windows:
|
||||
platforms: [windows]
|
||||
cmds:
|
||||
- echo 'Running command on Windows'
|
||||
```
|
||||
|
||||
This can be restricted to a specific architecture as follows:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build-windows-amd64:
|
||||
platforms: [windows/amd64]
|
||||
cmds:
|
||||
- echo 'Running command on Windows (amd64)'
|
||||
```
|
||||
|
||||
It is also possible to restrict the task to specific architectures:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build-amd64:
|
||||
platforms: [amd64]
|
||||
cmds:
|
||||
- echo 'Running command on amd64'
|
||||
```
|
||||
|
||||
Multiple platforms can be specified as follows:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
platforms: [windows/amd64, darwin]
|
||||
cmds:
|
||||
- echo 'Running command on Windows (amd64) and macOS'
|
||||
```
|
||||
|
||||
Individual commands can also be restricted to specific platforms:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cmd: echo 'Running command on Windows (amd64) and macOS'
|
||||
platforms: [windows/amd64, darwin]
|
||||
- cmd: echo 'Running on all platforms'
|
||||
```
|
||||
|
||||
## Calling another task
|
||||
|
||||
When a task has many dependencies, they are executed concurrently. This will
|
||||
@@ -511,19 +617,19 @@ tasks:
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- minify -o public/script.js src/js
|
||||
- esbuild --bundle --minify js/index.js > public/bundle.js
|
||||
sources:
|
||||
- src/js/**/*.js
|
||||
generates:
|
||||
- public/script.js
|
||||
- public/bundle.js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
- esbuild --bundle --minify css/index.css > public/bundle.css
|
||||
sources:
|
||||
- src/css/**/*.css
|
||||
generates:
|
||||
- public/style.css
|
||||
- public/bundle.css
|
||||
```
|
||||
|
||||
`sources` and `generates` can be files or file patterns. When given,
|
||||
@@ -595,9 +701,9 @@ The method `none` skips any validation and always run the task.
|
||||
|
||||
:::info
|
||||
|
||||
For the `checksum` (default) method to work, it is only necessary to
|
||||
inform the source files, but if you want to use the `timestamp` method, you
|
||||
also need to inform the generated files with `generates`.
|
||||
For the `checksum` (default) or `timestamp` method to work, it is only necessary to
|
||||
inform the source files.
|
||||
When the `timestamp` method is used, the last time of the running the task is considered as a generate.
|
||||
|
||||
:::
|
||||
|
||||
@@ -987,11 +1093,11 @@ tasks:
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- minify -o public/script.js src/js
|
||||
- esbuild --bundle --minify js/index.js > public/bundle.js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
- esbuild --bundle --minify css/index.css > public/bundle.css
|
||||
```
|
||||
|
||||
would print the following output:
|
||||
@@ -1270,6 +1376,30 @@ Hello, World!
|
||||
::endgroup::
|
||||
```
|
||||
|
||||
When using the `group` output, you may swallow the output of the executed command
|
||||
on standard output and standard error if it does not fail (zero exit code).
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
output:
|
||||
group:
|
||||
error_only: true
|
||||
|
||||
tasks:
|
||||
passes: echo 'output-of-passes'
|
||||
errors: echo 'output-of-errors' && exit 1
|
||||
```
|
||||
|
||||
```bash
|
||||
$ task passes
|
||||
$ task errors
|
||||
output-of-errors
|
||||
task: Failed to run task "errors": exit status 1
|
||||
```
|
||||
|
||||
The `prefix` output will prefix every line printed by a command with
|
||||
`[task-name] ` as the prefix, but you can customize the prefix for a command
|
||||
with the `prefix:` attribute:
|
||||
@@ -1348,6 +1478,31 @@ tasks:
|
||||
- ./app{{exeExt}} -h localhost -p 8080
|
||||
```
|
||||
|
||||
## `set` and `shopt`
|
||||
|
||||
It's possible to specify options to the
|
||||
[`set`](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html)
|
||||
and [`shopt`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html)
|
||||
builtins. This can be added at global, task or command level.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
set: [pipefail]
|
||||
shopt: [globstar]
|
||||
|
||||
tasks:
|
||||
# `globstar` required for double star globs to work
|
||||
default: echo **/*.go
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
Keep in mind that not all options are available in the
|
||||
[shell interpreter library](https://github.com/mvdan/sh) that Task uses.
|
||||
|
||||
:::
|
||||
|
||||
## Watch tasks
|
||||
|
||||
With the flags `--watch` or `-w` task will watch for file changes
|
||||
@@ -1359,4 +1514,3 @@ either setting `interval: '500ms'` in the root of the Taskfile passing it
|
||||
as an argument like `--interval=500ms`.
|
||||
|
||||
[gotemplate]: https://golang.org/pkg/text/template/
|
||||
[minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const {
|
||||
GITHUB_URL,
|
||||
TWITTER_URL,
|
||||
CHINESE_URL,
|
||||
DISCORD_URL,
|
||||
CHINESE_URL
|
||||
GITHUB_URL,
|
||||
MASTODON_URL,
|
||||
TWITTER_URL
|
||||
} = require('./constants');
|
||||
const lightCodeTheme = require('./src/themes/prismLight');
|
||||
const darkCodeTheme = require('./src/themes/prismDark');
|
||||
@@ -40,10 +41,7 @@ const config = {
|
||||
},
|
||||
blog: false,
|
||||
theme: {
|
||||
customCss: [
|
||||
require.resolve('./src/css/custom.css'),
|
||||
require.resolve('./src/css/carbon.css')
|
||||
]
|
||||
customCss: [require.resolve('./src/css/custom.css')]
|
||||
},
|
||||
gtag: {
|
||||
trackingID: 'G-4RT25NXQ7N',
|
||||
@@ -108,6 +106,11 @@ const config = {
|
||||
label: 'Twitter',
|
||||
position: 'right'
|
||||
},
|
||||
{
|
||||
href: MASTODON_URL,
|
||||
label: 'Mastodon',
|
||||
position: 'right'
|
||||
},
|
||||
{
|
||||
href: DISCORD_URL,
|
||||
label: 'Discord',
|
||||
@@ -146,6 +149,10 @@ const config = {
|
||||
label: 'Twitter',
|
||||
href: TWITTER_URL
|
||||
},
|
||||
{
|
||||
label: 'Mastodon',
|
||||
href: MASTODON_URL
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: DISCORD_URL
|
||||
@@ -177,14 +184,7 @@ const config = {
|
||||
apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc',
|
||||
indexName: 'taskfile'
|
||||
}
|
||||
}),
|
||||
|
||||
scripts: [
|
||||
{
|
||||
src: '/js/carbon.js',
|
||||
async: true
|
||||
}
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-beta.20",
|
||||
"@docusaurus/preset-classic": "2.0.0-beta.20",
|
||||
"@docusaurus/core": "^2.2.0",
|
||||
"@docusaurus/preset-classic": "^2.2.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.1.1",
|
||||
"prism-react-renderer": "^1.3.1",
|
||||
|
||||
@@ -13,10 +13,6 @@ const sidebars = {
|
||||
type: 'link',
|
||||
label: 'Chinese | 中国人',
|
||||
href: CHINESE_URL
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
value: '<div id="sidebar-ads"></div>'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
#carbonads * {
|
||||
margin: initial;
|
||||
padding: initial;
|
||||
}
|
||||
#carbonads {
|
||||
display: flex;
|
||||
max-width: 330px;
|
||||
background-color: hsl(0, 0%, 98%);
|
||||
box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
#carbonads a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
#carbonads a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
#carbonads span {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
#carbonads .carbon-wrap {
|
||||
display: flex;
|
||||
}
|
||||
#carbonads .carbon-img {
|
||||
display: block;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
#carbonads .carbon-img img {
|
||||
display: block;
|
||||
}
|
||||
#carbonads .carbon-text {
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
}
|
||||
#carbonads .carbon-poweredby {
|
||||
display: block;
|
||||
padding: 6px 8px;
|
||||
background: #f1f1f2;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
border-top-left-radius: 3px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[data-theme='dark'] #carbonads {
|
||||
background-color: hsl(0, 0%, 35%);
|
||||
box-shadow: 0 1px 4px 1px hsl(0, 0%, 55%);
|
||||
}
|
||||
|
||||
[data-theme='dark'] #carbonads .carbon-poweredby {
|
||||
background-color: hsl(0, 0%, 55%);
|
||||
}
|
||||
@@ -23,11 +23,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#carbonads {
|
||||
margin-top: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.gold-sponsors {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
0
docs/static/js/.keep
vendored
Normal file
0
docs/static/js/.keep
vendored
Normal file
29
docs/static/js/carbon.js
vendored
29
docs/static/js/carbon.js
vendored
@@ -1,29 +0,0 @@
|
||||
(function () {
|
||||
function attachAd() {
|
||||
var el = document.createElement('script');
|
||||
el.setAttribute('type', 'text/javascript');
|
||||
el.setAttribute('id', '_carbonads_js');
|
||||
el.setAttribute(
|
||||
'src',
|
||||
'//cdn.carbonads.com/carbon.js?serve=CESI65QJ&placement=taskfiledev'
|
||||
);
|
||||
el.setAttribute('async', 'async');
|
||||
|
||||
var wrapper = document.getElementById('sidebar-ads');
|
||||
wrapper.innerHTML = '';
|
||||
wrapper.appendChild(el);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
attachAd();
|
||||
|
||||
var currentPath = window.location.pathname;
|
||||
|
||||
setInterval(function () {
|
||||
if (currentPath !== window.location.pathname) {
|
||||
currentPath = window.location.pathname;
|
||||
attachAd();
|
||||
}
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
})();
|
||||
100
docs/static/schema.json
vendored
100
docs/static/schema.json
vendored
@@ -108,6 +108,20 @@
|
||||
"description": "The directory in which this task should run. Defaults to the current working directory.",
|
||||
"type": "string"
|
||||
},
|
||||
"set": {
|
||||
"description": "Enables POSIX shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/3/set"
|
||||
}
|
||||
},
|
||||
"shopt": {
|
||||
"description": "Enables Bash shell options for all of a task's commands. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/3/shopt"
|
||||
}
|
||||
},
|
||||
"vars": {
|
||||
"description": "A set of variables that can be used in the task.",
|
||||
"$ref": "#/definitions/3/vars"
|
||||
@@ -155,6 +169,13 @@
|
||||
"run": {
|
||||
"description": "Specifies whether the task should run again or not if called more than once. Available options: `always`, `once` and `when_changed`.",
|
||||
"$ref": "#/definitions/3/run"
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Specifies which platforms the task should be run on.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -177,6 +198,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"type": "string",
|
||||
"enum": ["allexport", "a", "errexit", "e", "noexec", "n", "noglob", "f", "nounset", "u", "xtrace", "x", "pipefail"]
|
||||
},
|
||||
"shopt": {
|
||||
"type": "string",
|
||||
"enum": ["expand_aliases", "globstar", "nullglob"]
|
||||
},
|
||||
"vars": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
@@ -226,6 +255,20 @@
|
||||
"description": "Silent mode disables echoing of command before Task runs it",
|
||||
"type": "boolean"
|
||||
},
|
||||
"set": {
|
||||
"description": "Enables POSIX shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/3/set"
|
||||
}
|
||||
},
|
||||
"shopt": {
|
||||
"description": "Enables Bash shell options for this command. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/3/shopt"
|
||||
}
|
||||
},
|
||||
"ignore_error": {
|
||||
"description": "Prevent command from aborting the execution of task even after receiving a status code of 1",
|
||||
"type": "boolean"
|
||||
@@ -233,6 +276,13 @@
|
||||
"defer": {
|
||||
"description": "",
|
||||
"type": "boolean"
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Specifies which platforms the command should be run on.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -264,6 +314,32 @@
|
||||
"run": {
|
||||
"type": "string",
|
||||
"enum": ["always", "once", "when_changed"]
|
||||
},
|
||||
"outputString": {
|
||||
"type": "string",
|
||||
"enum": ["interleaved", "prefixed", "group"],
|
||||
"default": "interleaved"
|
||||
},
|
||||
"outputObject": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"begin": {
|
||||
"type": "string"
|
||||
},
|
||||
"end": {
|
||||
"type": "string"
|
||||
},
|
||||
"error_only": {
|
||||
"description": "Swallows command output on zero exit code",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -286,9 +362,10 @@
|
||||
},
|
||||
"output": {
|
||||
"description": "Defines how the STDOUT and STDERR are printed when running tasks in parallel. The interleaved output prints lines in real time (default). The group output will print the entire output of a command once, after it finishes, so you won't have live feedback for commands that take a long time to run. The prefix output will prefix every line printed by a command with [task-name] as the prefix, but you can customize the prefix for a command with the prefix: attribute.",
|
||||
"type": "string",
|
||||
"enum": ["interleaved", "group", "prefixed"],
|
||||
"default": "interleaved"
|
||||
"anyOf": [
|
||||
{"$ref": "#/definitions/3/outputString"},
|
||||
{"$ref": "#/definitions/3/outputObject"}
|
||||
]
|
||||
},
|
||||
"method": {
|
||||
"description": "Defines which method is used to check the task is up-to-date. (default: checksum)",
|
||||
@@ -357,6 +434,20 @@
|
||||
"description": "Default 'silent' options for this Taskfile. If `false`, can be overidden with `true` in a task by task basis.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"set": {
|
||||
"description": "Enables POSIX shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/3/set"
|
||||
}
|
||||
},
|
||||
"shopt": {
|
||||
"description": "Enables Bash shell options for all commands in the Taskfile. See https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/3/shopt"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"type": "array",
|
||||
"description": "A list of `.env` file paths to be parsed.",
|
||||
@@ -370,7 +461,8 @@
|
||||
},
|
||||
"interval": {
|
||||
"description": "Sets a different watch interval when using `--watch`, the default being 5 seconds. This string should be a valid Go duration: https://pkg.go.dev/time#ParseDuration.",
|
||||
"$ref": "#/definitions/3/run"
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+(?:m|s|ms)$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
2505
docs/yarn.lock
2505
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
24
go.mod
24
go.mod
@@ -1,29 +1,31 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/fatih/color v1.14.1
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
|
||||
github.com/joho/godotenv v1.4.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-zglob v0.0.4
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2
|
||||
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.8.1
|
||||
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6
|
||||
golang.org/x/sync v0.1.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899
|
||||
mvdan.cc/sh/v3 v3.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
||||
go 1.18
|
||||
|
||||
77
go.sum
77
go.sum
@@ -1,22 +1,26 @@
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
|
||||
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
@@ -25,7 +29,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/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.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
|
||||
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
@@ -38,17 +42,38 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
|
||||
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6 h1:Ic9KukPQ7PegFzHckNiMTQXGgEszA7mY2Fn4ZMtnMbw=
|
||||
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
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/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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
|
||||
@@ -56,5 +81,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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.6.0-0.dev.0.20220704111049-a6e3029cd899 h1:nwm4t5PtLlFd/H342GP50CtYf7vyMCOZkPx3g9shO0c=
|
||||
mvdan.cc/sh/v3 v3.6.0-0.dev.0.20220704111049-a6e3029cd899/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E=
|
||||
mvdan.cc/sh/v3 v3.6.0 h1:gtva4EXJ0dFNvl5bHjcUEvws+KRcDslT8VKheTYkbGU=
|
||||
mvdan.cc/sh/v3 v3.6.0/go.mod h1:U4mhtBLZ32iWhif5/lD+ygy1zrgaQhUu+XFy7C8+TTA=
|
||||
|
||||
117
help.go
117
help.go
@@ -1,6 +1,8 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -9,16 +11,86 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/go-task/task/v3/internal/editors"
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// ListOptions collects list-related options
|
||||
type ListOptions struct {
|
||||
ListOnlyTasksWithDescriptions bool
|
||||
ListAllTasks bool
|
||||
FormatTaskListAsJSON bool
|
||||
}
|
||||
|
||||
// NewListOptions creates a new ListOptions instance
|
||||
func NewListOptions(list, listAll, listAsJson bool) ListOptions {
|
||||
return ListOptions{
|
||||
ListOnlyTasksWithDescriptions: list,
|
||||
ListAllTasks: listAll,
|
||||
FormatTaskListAsJSON: listAsJson,
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldListTasks returns true if one of the options to list tasks has been set to true
|
||||
func (o ListOptions) ShouldListTasks() bool {
|
||||
return o.ListOnlyTasksWithDescriptions || o.ListAllTasks
|
||||
}
|
||||
|
||||
// Validate validates that the collection of list-related options are in a valid configuration
|
||||
func (o ListOptions) Validate() error {
|
||||
if o.ListOnlyTasksWithDescriptions && o.ListAllTasks {
|
||||
return fmt.Errorf("task: cannot use --list and --list-all at the same time")
|
||||
}
|
||||
if o.FormatTaskListAsJSON && !o.ShouldListTasks() {
|
||||
return fmt.Errorf("task: --json only applies to --list or --list-all")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filters returns the slice of FilterFunc which filters a list
|
||||
// of taskfile.Task according to the given ListOptions
|
||||
func (o ListOptions) Filters() []FilterFunc {
|
||||
filters := []FilterFunc{FilterOutInternal}
|
||||
|
||||
if o.ListOnlyTasksWithDescriptions {
|
||||
filters = append(filters, FilterOutNoDesc)
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
// ListTasks prints a list of tasks.
|
||||
// Tasks that match the given filters will be excluded from the list.
|
||||
// The function returns a boolean indicating whether or not tasks were found.
|
||||
func (e *Executor) ListTasks(filters ...FilterFunc) bool {
|
||||
tasks := e.GetTaskList(filters...)
|
||||
// The function returns a boolean indicating whether tasks were found
|
||||
// and an error if one was encountered while preparing the output.
|
||||
func (e *Executor) ListTasks(o ListOptions) (bool, error) {
|
||||
tasks, err := e.GetTaskList(o.Filters()...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if o.FormatTaskListAsJSON {
|
||||
output, err := e.ToEditorOutput(tasks)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(e.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(output); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(tasks) > 0, nil
|
||||
}
|
||||
if len(tasks) == 0 {
|
||||
return false
|
||||
if o.ListOnlyTasksWithDescriptions {
|
||||
e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks")
|
||||
} else if o.ListAllTasks {
|
||||
e.Logger.Outf(logger.Yellow, "task: No tasks available")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
e.Logger.Outf(logger.Default, "task: Available tasks for this project:")
|
||||
|
||||
@@ -31,10 +103,12 @@ func (e *Executor) ListTasks(filters ...FilterFunc) bool {
|
||||
if len(task.Aliases) > 0 {
|
||||
e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", "))
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
_, _ = fmt.Fprint(w, "\n")
|
||||
}
|
||||
w.Flush()
|
||||
return true
|
||||
if err := w.Flush(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ListTaskNames prints only the task names in a Taskfile.
|
||||
@@ -69,3 +143,32 @@ func (e *Executor) ListTaskNames(allTasks bool) {
|
||||
fmt.Fprintln(w, t)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Output, error) {
|
||||
o := &editors.Output{
|
||||
Tasks: make([]editors.Task, len(tasks)),
|
||||
}
|
||||
for i, t := range tasks {
|
||||
// Get the fingerprinting method to use
|
||||
method := e.Taskfile.Method
|
||||
if t.Method != "" {
|
||||
method = t.Method
|
||||
}
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(context.Background(), t,
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Tasks[i] = editors.Task{
|
||||
Name: t.Name(),
|
||||
Desc: t.Desc,
|
||||
Summary: t.Summary,
|
||||
UpToDate: upToDate,
|
||||
}
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/internal/version"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
@@ -184,6 +185,7 @@ func (c *CompilerV3) getSpecialVars(t *taskfile.Task) (map[string]string, error)
|
||||
"ROOT_DIR": c.Dir,
|
||||
"TASKFILE_DIR": taskfileDir,
|
||||
"USER_WORKING_DIR": c.UserWorkingDir,
|
||||
"TASK_VERSION": version.GetVersion(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
14
internal/editors/output.go
Normal file
14
internal/editors/output.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package editors
|
||||
|
||||
// Output wraps task list output for use in editor integrations (e.g. VSCode, etc)
|
||||
type Output struct {
|
||||
Tasks []Task `json:"tasks"`
|
||||
}
|
||||
|
||||
// Task describes a single task
|
||||
type Task struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
Summary string `json:"summary"`
|
||||
UpToDate bool `json:"up_to_date"`
|
||||
}
|
||||
31
internal/env/env.go
vendored
Normal file
31
internal/env/env.go
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
func Get(t *taskfile.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
environ := os.Environ()
|
||||
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
str, isString := v.(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
|
||||
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
|
||||
}
|
||||
|
||||
return environ
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package execext
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -17,12 +18,14 @@ import (
|
||||
|
||||
// RunCommandOptions is the options for the RunCommand func
|
||||
type RunCommandOptions struct {
|
||||
Command string
|
||||
Dir string
|
||||
Env []string
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Command string
|
||||
Dir string
|
||||
Env []string
|
||||
PosixOpts []string
|
||||
BashOpts []string
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -36,9 +39,18 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
return ErrNilOptions
|
||||
}
|
||||
|
||||
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "")
|
||||
if err != nil {
|
||||
return err
|
||||
// Set "-e" or "errexit" by default
|
||||
opts.PosixOpts = append(opts.PosixOpts, "e")
|
||||
|
||||
// Format POSIX options into a slice that mvdan/sh understands
|
||||
var params []string
|
||||
for _, opt := range opts.PosixOpts {
|
||||
if len(opt) == 1 {
|
||||
params = append(params, fmt.Sprintf("-%s", opt))
|
||||
} else {
|
||||
params = append(params, "-o")
|
||||
params = append(params, opt)
|
||||
}
|
||||
}
|
||||
|
||||
environ := opts.Env
|
||||
@@ -47,7 +59,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
}
|
||||
|
||||
r, err := interp.New(
|
||||
interp.Params("-e"),
|
||||
interp.Params(params...),
|
||||
interp.Env(expand.ListEnviron(environ...)),
|
||||
interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)),
|
||||
interp.OpenHandler(openHandler),
|
||||
@@ -58,6 +70,25 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
parser := syntax.NewParser()
|
||||
|
||||
// Run any shopt commands
|
||||
if len(opts.BashOpts) > 0 {
|
||||
shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " "))
|
||||
shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.Run(ctx, shoptCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Run the user-defined command
|
||||
p, err := parser.Parse(strings.NewReader(opts.Command), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Run(ctx, p)
|
||||
}
|
||||
|
||||
|
||||
20
internal/fingerprint/checker.go
Normal file
20
internal/fingerprint/checker.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// StatusCheckable defines any type that can check if the status of a task is up-to-date.
|
||||
type StatusCheckable interface {
|
||||
IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error)
|
||||
}
|
||||
|
||||
// SourcesCheckable defines any type that can check if the sources of a task are up-to-date.
|
||||
type SourcesCheckable interface {
|
||||
IsUpToDate(t *taskfile.Task) (bool, error)
|
||||
Value(t *taskfile.Task) (interface{}, error)
|
||||
OnError(t *taskfile.Task) error
|
||||
Kind() string
|
||||
}
|
||||
132
internal/fingerprint/checker_mock.go
Normal file
132
internal/fingerprint/checker_mock.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: checker.go
|
||||
|
||||
// Package fingerprint is a generated GoMock package.
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
taskfile "github.com/go-task/task/v3/taskfile"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockStatusCheckable is a mock of StatusCheckable interface.
|
||||
type MockStatusCheckable struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStatusCheckableMockRecorder
|
||||
}
|
||||
|
||||
// MockStatusCheckableMockRecorder is the mock recorder for MockStatusCheckable.
|
||||
type MockStatusCheckableMockRecorder struct {
|
||||
mock *MockStatusCheckable
|
||||
}
|
||||
|
||||
// NewMockStatusCheckable creates a new mock instance.
|
||||
func NewMockStatusCheckable(ctrl *gomock.Controller) *MockStatusCheckable {
|
||||
mock := &MockStatusCheckable{ctrl: ctrl}
|
||||
mock.recorder = &MockStatusCheckableMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockStatusCheckable) EXPECT() *MockStatusCheckableMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// IsUpToDate mocks base method.
|
||||
func (m *MockStatusCheckable) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsUpToDate", ctx, t)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsUpToDate indicates an expected call of IsUpToDate.
|
||||
func (mr *MockStatusCheckableMockRecorder) IsUpToDate(ctx, t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStatusCheckable)(nil).IsUpToDate), ctx, t)
|
||||
}
|
||||
|
||||
// MockSourcesCheckable is a mock of SourcesCheckable interface.
|
||||
type MockSourcesCheckable struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSourcesCheckableMockRecorder
|
||||
}
|
||||
|
||||
// MockSourcesCheckableMockRecorder is the mock recorder for MockSourcesCheckable.
|
||||
type MockSourcesCheckableMockRecorder struct {
|
||||
mock *MockSourcesCheckable
|
||||
}
|
||||
|
||||
// NewMockSourcesCheckable creates a new mock instance.
|
||||
func NewMockSourcesCheckable(ctrl *gomock.Controller) *MockSourcesCheckable {
|
||||
mock := &MockSourcesCheckable{ctrl: ctrl}
|
||||
mock.recorder = &MockSourcesCheckableMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSourcesCheckable) EXPECT() *MockSourcesCheckableMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// IsUpToDate mocks base method.
|
||||
func (m *MockSourcesCheckable) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsUpToDate", t)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsUpToDate indicates an expected call of IsUpToDate.
|
||||
func (mr *MockSourcesCheckableMockRecorder) IsUpToDate(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockSourcesCheckable)(nil).IsUpToDate), t)
|
||||
}
|
||||
|
||||
// Kind mocks base method.
|
||||
func (m *MockSourcesCheckable) Kind() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Kind")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Kind indicates an expected call of Kind.
|
||||
func (mr *MockSourcesCheckableMockRecorder) Kind() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kind", reflect.TypeOf((*MockSourcesCheckable)(nil).Kind))
|
||||
}
|
||||
|
||||
// OnError mocks base method.
|
||||
func (m *MockSourcesCheckable) OnError(t *taskfile.Task) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OnError", t)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OnError indicates an expected call of OnError.
|
||||
func (mr *MockSourcesCheckableMockRecorder) OnError(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnError", reflect.TypeOf((*MockSourcesCheckable)(nil).OnError), t)
|
||||
}
|
||||
|
||||
// Value mocks base method.
|
||||
func (m *MockSourcesCheckable) Value(t *taskfile.Task) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Value", t)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Value indicates an expected call of Value.
|
||||
func (mr *MockSourcesCheckableMockRecorder) Value(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockSourcesCheckable)(nil).Value), t)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
16
internal/fingerprint/sources.go
Normal file
16
internal/fingerprint/sources.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package fingerprint
|
||||
|
||||
import "fmt"
|
||||
|
||||
func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) {
|
||||
switch method {
|
||||
case "timestamp":
|
||||
return NewTimestampChecker(tempDir, dry), nil
|
||||
case "checksum":
|
||||
return NewChecksumChecker(tempDir, dry), nil
|
||||
case "none":
|
||||
return NoneChecker{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: invalid method "%s"`, method)
|
||||
}
|
||||
}
|
||||
121
internal/fingerprint/sources_checksum.go
Normal file
121
internal/fingerprint/sources_checksum.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// ChecksumChecker validates if a task is up to date by calculating its source
|
||||
// files checksum
|
||||
type ChecksumChecker struct {
|
||||
tempDir string
|
||||
dry bool
|
||||
}
|
||||
|
||||
func NewChecksumChecker(tempDir string, dry bool) *ChecksumChecker {
|
||||
return &ChecksumChecker{
|
||||
tempDir: tempDir,
|
||||
dry: dry,
|
||||
}
|
||||
}
|
||||
|
||||
func (checker *ChecksumChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
if len(t.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
checksumFile := checker.checksumFilePath(t)
|
||||
|
||||
data, _ := os.ReadFile(checksumFile)
|
||||
oldMd5 := strings.TrimSpace(string(data))
|
||||
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newMd5, err := checker.checksum(sources...)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !checker.dry {
|
||||
_ = os.MkdirAll(filepathext.SmartJoin(checker.tempDir, "checksum"), 0o755)
|
||||
if err = os.WriteFile(checksumFile, []byte(newMd5+"\n"), 0o644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Generates) > 0 {
|
||||
// For each specified 'generates' field, check whether the files actually exist
|
||||
for _, g := range t.Generates {
|
||||
generates, err := Glob(t.Dir, g)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(generates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return oldMd5 == newMd5, nil
|
||||
}
|
||||
|
||||
func (checker *ChecksumChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
return checker.checksum()
|
||||
}
|
||||
|
||||
func (checker *ChecksumChecker) OnError(t *taskfile.Task) error {
|
||||
if len(t.Sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(checker.checksumFilePath(t))
|
||||
}
|
||||
|
||||
func (*ChecksumChecker) Kind() string {
|
||||
return "checksum"
|
||||
}
|
||||
|
||||
func (c *ChecksumChecker) checksum(files ...string) (string, error) {
|
||||
h := md5.New()
|
||||
|
||||
for _, f := range files {
|
||||
// also sum the filename, so checksum changes for renaming a file
|
||||
if _, err := io.Copy(h, strings.NewReader(filepath.Base(f))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.Open(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (checker *ChecksumChecker) checksumFilePath(t *taskfile.Task) string {
|
||||
return filepath.Join(checker.tempDir, "checksum", normalizeFilename(t.Name()))
|
||||
}
|
||||
|
||||
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
|
||||
|
||||
// replaces invalid characters on filenames with "-"
|
||||
func normalizeFilename(f string) string {
|
||||
return checksumFilenameRegexp.ReplaceAllString(f, "-")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -16,6 +16,6 @@ func TestNormalizeFilename(t *testing.T) {
|
||||
{"foo1bar2baz3", "foo1bar2baz3"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.Out, (&Checksum{}).normalizeFilename(test.In))
|
||||
assert.Equal(t, test.Out, normalizeFilename(test.In))
|
||||
}
|
||||
}
|
||||
23
internal/fingerprint/sources_none.go
Normal file
23
internal/fingerprint/sources_none.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package fingerprint
|
||||
|
||||
import "github.com/go-task/task/v3/taskfile"
|
||||
|
||||
// NoneChecker is a no-op Checker.
|
||||
// It will always report that the task is not up-to-date.
|
||||
type NoneChecker struct{}
|
||||
|
||||
func (NoneChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (NoneChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (NoneChecker) OnError(t *taskfile.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NoneChecker) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
151
internal/fingerprint/sources_timestamp.go
Normal file
151
internal/fingerprint/sources_timestamp.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// TimestampChecker checks if any source change compared with the generated files,
|
||||
// using file modifications timestamps.
|
||||
type TimestampChecker struct {
|
||||
tempDir string
|
||||
dry bool
|
||||
}
|
||||
|
||||
func NewTimestampChecker(tempDir string, dry bool) *TimestampChecker {
|
||||
return &TimestampChecker{
|
||||
tempDir: tempDir,
|
||||
dry: dry,
|
||||
}
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (checker *TimestampChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
if len(t.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
generates, err := globs(t.Dir, t.Generates)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
timestampFile := checker.timestampFilePath(t)
|
||||
|
||||
// If the file exists, add the file path to the generates.
|
||||
// If the generate file is old, the task will be executed.
|
||||
_, err = os.Stat(timestampFile)
|
||||
if err == nil {
|
||||
generates = append(generates, timestampFile)
|
||||
} else {
|
||||
// Create the timestamp file for the next execution when the file does not exist.
|
||||
if !checker.dry {
|
||||
if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
f, err := os.Create(timestampFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
taskTime := time.Now()
|
||||
|
||||
// Compare the time of the generates and sources. If the generates are old, the task will be executed.
|
||||
|
||||
// Get the max time of the generates.
|
||||
generateMaxTime, err := getMaxTime(generates...)
|
||||
if err != nil || generateMaxTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if any of the source files is newer than the max time of the generates.
|
||||
shouldUpdate, err := anyFileNewerThan(sources, generateMaxTime)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Modify the metadata of the file to the the current time.
|
||||
if !checker.dry {
|
||||
if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return !shouldUpdate, nil
|
||||
}
|
||||
|
||||
func (checker *TimestampChecker) Kind() string {
|
||||
return "timestamp"
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (checker *TimestampChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
sourcesMaxTime, err := getMaxTime(sources...)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
if sourcesMaxTime.IsZero() {
|
||||
return time.Unix(0, 0), nil
|
||||
}
|
||||
|
||||
return sourcesMaxTime, nil
|
||||
}
|
||||
|
||||
func getMaxTime(files ...string) (time.Time, error) {
|
||||
var t time.Time
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
t = maxTime(t, info.ModTime())
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func maxTime(a, b time.Time) time.Time {
|
||||
if a.After(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// If the modification time of any of the files is newer than the the given time, returns true.
|
||||
// This function is lazy, as it stops when it finds a file newer than the given time.
|
||||
func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) {
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if info.ModTime().After(givenTime) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (*TimestampChecker) OnError(t *taskfile.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (checker *TimestampChecker) timestampFilePath(t *taskfile.Task) string {
|
||||
return filepath.Join(checker.tempDir, "timestamp", normalizeFilename(t.Task))
|
||||
}
|
||||
36
internal/fingerprint/status.go
Normal file
36
internal/fingerprint/status.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
type StatusChecker struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewStatusChecker(logger *logger.Logger) StatusCheckable {
|
||||
return &StatusChecker{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (checker *StatusChecker) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
for _, s := range t.Status {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: s,
|
||||
Dir: t.Dir,
|
||||
Env: env.Get(t),
|
||||
})
|
||||
if err != nil {
|
||||
checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err)
|
||||
return false, nil
|
||||
}
|
||||
checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
132
internal/fingerprint/task.go
Normal file
132
internal/fingerprint/task.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
type (
|
||||
CheckerOption func(*CheckerConfig)
|
||||
CheckerConfig struct {
|
||||
method string
|
||||
dry bool
|
||||
tempDir string
|
||||
logger *logger.Logger
|
||||
statusChecker StatusCheckable
|
||||
sourcesChecker SourcesCheckable
|
||||
}
|
||||
)
|
||||
|
||||
func WithMethod(method string) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.method = method
|
||||
}
|
||||
}
|
||||
|
||||
func WithDry(dry bool) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.dry = dry
|
||||
}
|
||||
}
|
||||
|
||||
func WithTempDir(tempDir string) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.tempDir = tempDir
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(logger *logger.Logger) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func WithStatusChecker(checker StatusCheckable) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.statusChecker = checker
|
||||
}
|
||||
}
|
||||
|
||||
func WithSourcesChecker(checker SourcesCheckable) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.sourcesChecker = checker
|
||||
}
|
||||
}
|
||||
|
||||
func IsTaskUpToDate(
|
||||
ctx context.Context,
|
||||
t *taskfile.Task,
|
||||
opts ...CheckerOption,
|
||||
) (bool, error) {
|
||||
var statusUpToDate bool
|
||||
var sourcesUpToDate bool
|
||||
var err error
|
||||
|
||||
// Default config
|
||||
config := &CheckerConfig{
|
||||
method: "none",
|
||||
tempDir: "",
|
||||
dry: false,
|
||||
logger: nil,
|
||||
statusChecker: nil,
|
||||
sourcesChecker: nil,
|
||||
}
|
||||
|
||||
// Apply functional options
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
// If no status checker was given, set up the default one
|
||||
if config.statusChecker == nil {
|
||||
config.statusChecker = NewStatusChecker(config.logger)
|
||||
}
|
||||
|
||||
// If no sources checker was given, set up the default one
|
||||
if config.sourcesChecker == nil {
|
||||
config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
statusIsSet := len(t.Status) != 0
|
||||
sourcesIsSet := len(t.Sources) != 0
|
||||
|
||||
// If status is set, check if it is up-to-date
|
||||
if statusIsSet {
|
||||
statusUpToDate, err = config.statusChecker.IsUpToDate(ctx, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// If sources is set, check if they are up-to-date
|
||||
if sourcesIsSet {
|
||||
sourcesUpToDate, err = config.sourcesChecker.IsUpToDate(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// If both status and sources are set, the task is up-to-date if both are up-to-date
|
||||
if statusIsSet && sourcesIsSet {
|
||||
return statusUpToDate && sourcesUpToDate, nil
|
||||
}
|
||||
|
||||
// If only status is set, the task is up-to-date if the status is up-to-date
|
||||
if statusIsSet {
|
||||
return statusUpToDate, nil
|
||||
}
|
||||
|
||||
// If only sources is set, the task is up-to-date if the sources are up-to-date
|
||||
if sourcesIsSet {
|
||||
return sourcesUpToDate, nil
|
||||
}
|
||||
|
||||
// If no status or sources are set, the task should always run
|
||||
// i.e. it is never considered "up-to-date"
|
||||
return false, nil
|
||||
}
|
||||
174
internal/fingerprint/task_test.go
Normal file
174
internal/fingerprint/task_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// TruthTable
|
||||
//
|
||||
// | Status up-to-date | Sources up-to-date | Task is up-to-date |
|
||||
// | ----------------- | ------------------ | ------------------ |
|
||||
// | not set | not set | false |
|
||||
// | not set | true | true |
|
||||
// | not set | false | false |
|
||||
// | true | not set | true |
|
||||
// | true | true | true |
|
||||
// | true | false | false |
|
||||
// | false | not set | false |
|
||||
// | false | true | false |
|
||||
// | false | false | false |
|
||||
func TestIsTaskUpToDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task *taskfile.Task
|
||||
setupMockStatusChecker func(m *MockStatusCheckable)
|
||||
setupMockSourcesChecker func(m *MockSourcesCheckable)
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "expect FALSE when no status or sources are defined",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when no status is defined and sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when no status is defined and sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when status is up-to-date and sources are not defined",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when status and sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is NOT up-to-date and sources are not defined",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status and sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockStatusChecker := NewMockStatusCheckable(ctrl)
|
||||
if tt.setupMockStatusChecker != nil {
|
||||
tt.setupMockStatusChecker(mockStatusChecker)
|
||||
}
|
||||
|
||||
mockSourcesChecker := NewMockSourcesCheckable(ctrl)
|
||||
if tt.setupMockSourcesChecker != nil {
|
||||
tt.setupMockSourcesChecker(mockSourcesChecker)
|
||||
}
|
||||
|
||||
result, err := IsTaskUpToDate(
|
||||
context.Background(),
|
||||
tt.task,
|
||||
WithStatusChecker(mockStatusChecker),
|
||||
WithSourcesChecker(mockSourcesChecker),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
62
internal/goext/meta.go
Normal file
62
internal/goext/meta.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package goext
|
||||
|
||||
// NOTE(@andreynering): The lists in this file were copied from:
|
||||
//
|
||||
// https://github.com/golang/go/blob/master/src/go/build/syslist.go
|
||||
|
||||
func IsKnownOS(str string) bool {
|
||||
_, known := knownOS[str]
|
||||
return known
|
||||
}
|
||||
|
||||
func IsKnownArch(str string) bool {
|
||||
_, known := knownArch[str]
|
||||
return known
|
||||
}
|
||||
|
||||
var knownOS = map[string]struct{}{
|
||||
"aix": {},
|
||||
"android": {},
|
||||
"darwin": {},
|
||||
"dragonfly": {},
|
||||
"freebsd": {},
|
||||
"hurd": {},
|
||||
"illumos": {},
|
||||
"ios": {},
|
||||
"js": {},
|
||||
"linux": {},
|
||||
"nacl": {},
|
||||
"netbsd": {},
|
||||
"openbsd": {},
|
||||
"plan9": {},
|
||||
"solaris": {},
|
||||
"windows": {},
|
||||
"zos": {},
|
||||
}
|
||||
|
||||
var knownArch = map[string]struct{}{
|
||||
"386": {},
|
||||
"amd64": {},
|
||||
"amd64p32": {},
|
||||
"arm": {},
|
||||
"armbe": {},
|
||||
"arm64": {},
|
||||
"arm64be": {},
|
||||
"loong64": {},
|
||||
"mips": {},
|
||||
"mipsle": {},
|
||||
"mips64": {},
|
||||
"mips64le": {},
|
||||
"mips64p32": {},
|
||||
"mips64p32le": {},
|
||||
"ppc": {},
|
||||
"ppc64": {},
|
||||
"ppc64le": {},
|
||||
"riscv": {},
|
||||
"riscv64": {},
|
||||
"s390": {},
|
||||
"s390x": {},
|
||||
"sparc": {},
|
||||
"sparc64": {},
|
||||
"wasm": {},
|
||||
}
|
||||
@@ -34,6 +34,10 @@ func Red() PrintFunc {
|
||||
}
|
||||
|
||||
func envColor(env string, defaultColor color.Attribute) color.Attribute {
|
||||
if os.Getenv("FORCE_COLOR") != "" {
|
||||
color.NoColor = false
|
||||
}
|
||||
|
||||
override, err := strconv.Atoi(os.Getenv(env))
|
||||
if err == nil {
|
||||
return color.Attribute(override)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type Group struct {
|
||||
Begin, End string
|
||||
ErrorOnly bool
|
||||
}
|
||||
|
||||
func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Writer, io.Writer, CloseFunc) {
|
||||
@@ -17,7 +18,13 @@ func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Wri
|
||||
if g.End != "" {
|
||||
gw.end = tmpl.Replace(g.End) + "\n"
|
||||
}
|
||||
return gw, gw, func() error { return gw.close() }
|
||||
return gw, gw, func(err error) error {
|
||||
if g.ErrorOnly && err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gw.close()
|
||||
}
|
||||
}
|
||||
|
||||
type groupWriter struct {
|
||||
|
||||
@@ -7,5 +7,5 @@ import (
|
||||
type Interleaved struct{}
|
||||
|
||||
func (Interleaved) WrapWriter(stdOut, stdErr io.Writer, _ string, _ Templater) (io.Writer, io.Writer, CloseFunc) {
|
||||
return stdOut, stdErr, func() error { return nil }
|
||||
return stdOut, stdErr, func(error) error { return nil }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type Output interface {
|
||||
WrapWriter(stdOut, stdErr io.Writer, prefix string, tmpl Templater) (io.Writer, io.Writer, CloseFunc)
|
||||
}
|
||||
|
||||
type CloseFunc func() error
|
||||
type CloseFunc func(err error) error
|
||||
|
||||
// Build the Output for the requested taskfile.Output.
|
||||
func BuildFor(o *taskfile.Output) (Output, error) {
|
||||
@@ -30,8 +30,9 @@ func BuildFor(o *taskfile.Output) (Output, error) {
|
||||
return Interleaved{}, nil
|
||||
case "group":
|
||||
return Group{
|
||||
Begin: o.Group.Begin,
|
||||
End: o.Group.End,
|
||||
Begin: o.Group.Begin,
|
||||
End: o.Group.End,
|
||||
ErrorOnly: o.Group.ErrorOnly,
|
||||
}, nil
|
||||
case "prefixed":
|
||||
if err := checkOutputGroupUnset(o); err != nil {
|
||||
|
||||
@@ -2,15 +2,16 @@ package output_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
func TestInterleaved(t *testing.T) {
|
||||
@@ -38,7 +39,7 @@ func TestGroup(t *testing.T) {
|
||||
fmt.Fprintln(stdErr, "err")
|
||||
assert.Equal(t, "", b.String())
|
||||
|
||||
assert.NoError(t, cleanup())
|
||||
assert.NoError(t, cleanup(nil))
|
||||
assert.Equal(t, "out\nout\nerr\nerr\nout\nerr\n", b.String())
|
||||
}
|
||||
|
||||
@@ -64,17 +65,44 @@ func TestGroupWithBeginEnd(t *testing.T) {
|
||||
assert.Equal(t, "", b.String())
|
||||
fmt.Fprintln(w, "baz")
|
||||
assert.Equal(t, "", b.String())
|
||||
assert.NoError(t, cleanup())
|
||||
assert.NoError(t, cleanup(nil))
|
||||
assert.Equal(t, "::group::example-value\nfoo\nbar\nbaz\n::endgroup::\n", b.String())
|
||||
})
|
||||
t.Run("no output", func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var _, _, cleanup = o.WrapWriter(&b, io.Discard, "", &tmpl)
|
||||
assert.NoError(t, cleanup())
|
||||
assert.NoError(t, cleanup(nil))
|
||||
assert.Equal(t, "", b.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroupErrorOnlySwallowsOutputOnNoError(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Group{
|
||||
ErrorOnly: true,
|
||||
}
|
||||
var stdOut, stdErr, cleanup = o.WrapWriter(&b, io.Discard, "", nil)
|
||||
|
||||
_, _ = fmt.Fprintln(stdOut, "std-out")
|
||||
_, _ = fmt.Fprintln(stdErr, "std-err")
|
||||
|
||||
assert.NoError(t, cleanup(nil))
|
||||
assert.Empty(t, b.String())
|
||||
}
|
||||
func TestGroupErrorOnlyShowsOutputOnError(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Group{
|
||||
ErrorOnly: true,
|
||||
}
|
||||
var stdOut, stdErr, cleanup = o.WrapWriter(&b, io.Discard, "", nil)
|
||||
|
||||
_, _ = fmt.Fprintln(stdOut, "std-out")
|
||||
_, _ = fmt.Fprintln(stdErr, "std-err")
|
||||
|
||||
assert.NoError(t, cleanup(errors.New("any-error")))
|
||||
assert.Equal(t, "std-out\nstd-err\n", b.String())
|
||||
}
|
||||
|
||||
func TestPrefixed(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Prefixed{}
|
||||
@@ -87,7 +115,7 @@ func TestPrefixed(t *testing.T) {
|
||||
assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String())
|
||||
fmt.Fprintln(w, "baz")
|
||||
assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String())
|
||||
assert.NoError(t, cleanup())
|
||||
assert.NoError(t, cleanup(nil))
|
||||
})
|
||||
|
||||
t.Run("multiple writes for a single line", func(t *testing.T) {
|
||||
@@ -98,7 +126,7 @@ func TestPrefixed(t *testing.T) {
|
||||
assert.Equal(t, "", b.String())
|
||||
}
|
||||
|
||||
assert.NoError(t, cleanup())
|
||||
assert.NoError(t, cleanup(nil))
|
||||
assert.Equal(t, "[prefix] Test!\n", b.String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type Prefixed struct{}
|
||||
|
||||
func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ Templater) (io.Writer, io.Writer, CloseFunc) {
|
||||
pw := &prefixWriter{writer: stdOut, prefix: prefix}
|
||||
return pw, pw, func() error { return pw.close() }
|
||||
return pw, pw, func(error) error { return pw.close() }
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
// This code is released under the MIT License
|
||||
// Copyright (c) 2020 Marco Molteni and the timeit contributors.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
)
|
||||
|
||||
const usage = `sleepit: sleep for the specified duration, optionally handling signals
|
||||
When the line "sleepit: ready" is printed, it means that it is safe to send signals to it
|
||||
|
||||
Usage: sleepit <command> [<args>]
|
||||
|
||||
Commands
|
||||
|
||||
default Use default action: on reception of SIGINT terminate abruptly
|
||||
handle Handle signals: on reception of SIGINT perform cleanup before exiting
|
||||
version Show the sleepit version`
|
||||
|
||||
var (
|
||||
// Filled by the linker.
|
||||
fullVersion = "unknown" // example: v0.0.9-8-g941583d027-dirty
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:]))
|
||||
}
|
||||
|
||||
func run(args []string) int {
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, usage)
|
||||
return 2
|
||||
}
|
||||
|
||||
defaultCmd := flag.NewFlagSet("default", flag.ExitOnError)
|
||||
defaultSleep := defaultCmd.Duration("sleep", 5*time.Second, "Sleep duration")
|
||||
|
||||
handleCmd := flag.NewFlagSet("handle", flag.ExitOnError)
|
||||
handleSleep := handleCmd.Duration("sleep", 5*time.Second, "Sleep duration")
|
||||
handleCleanup := handleCmd.Duration("cleanup", 5*time.Second, "Cleanup duration")
|
||||
handleTermAfter := handleCmd.Int("term-after", 0,
|
||||
"Terminate immediately after `N` signals.\n"+
|
||||
"Default is to terminate only when the cleanup phase has completed.")
|
||||
|
||||
versionCmd := flag.NewFlagSet("version", flag.ExitOnError)
|
||||
|
||||
switch args[0] {
|
||||
|
||||
case "default":
|
||||
_ = defaultCmd.Parse(args[1:])
|
||||
if len(defaultCmd.Args()) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "default: unexpected arguments: %v\n", defaultCmd.Args())
|
||||
return 2
|
||||
}
|
||||
return supervisor(*defaultSleep, 0, 0, nil)
|
||||
|
||||
case "handle":
|
||||
_ = handleCmd.Parse(args[1:])
|
||||
if *handleTermAfter == 1 {
|
||||
fmt.Fprintf(os.Stderr, "handle: term-after cannot be 1\n")
|
||||
return 2
|
||||
}
|
||||
if len(handleCmd.Args()) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "handle: unexpected arguments: %v\n", handleCmd.Args())
|
||||
return 2
|
||||
}
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT
|
||||
return supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh)
|
||||
|
||||
case "version":
|
||||
_ = versionCmd.Parse(args[1:])
|
||||
if len(versionCmd.Args()) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "version: unexpected arguments: %v\n", versionCmd.Args())
|
||||
return 2
|
||||
}
|
||||
fmt.Printf("sleepit version %s\n", fullVersion)
|
||||
return 0
|
||||
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, usage)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func supervisor(
|
||||
sleep time.Duration,
|
||||
cleanup time.Duration,
|
||||
termAfter int,
|
||||
sigCh <-chan os.Signal,
|
||||
) int {
|
||||
fmt.Printf("sleepit: ready\n")
|
||||
fmt.Printf("sleepit: PID=%d sleep=%v cleanup=%v\n",
|
||||
os.Getpid(), sleep, cleanup)
|
||||
|
||||
cancelWork := make(chan struct{})
|
||||
workerDone := worker(cancelWork, sleep, "work")
|
||||
|
||||
cancelCleaner := make(chan struct{})
|
||||
var cleanerDone <-chan struct{}
|
||||
|
||||
sigCount := 0
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
sigCount++
|
||||
fmt.Printf("sleepit: got signal=%s count=%d\n", sig, sigCount)
|
||||
if sigCount == 1 {
|
||||
// since `cancelWork` is unbuffered, sending will be synchronous:
|
||||
// we are ensured that the worker has terminated before starting cleanup.
|
||||
// This is important in some real-life situations.
|
||||
cancelWork <- struct{}{}
|
||||
cleanerDone = worker(cancelCleaner, cleanup, "cleanup")
|
||||
}
|
||||
if sigCount == termAfter {
|
||||
cancelCleaner <- struct{}{}
|
||||
return 4
|
||||
}
|
||||
case <-workerDone:
|
||||
return 0
|
||||
case <-cleanerDone:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start a worker goroutine and return immediately a `workerDone` channel.
|
||||
// The goroutine will prepend its prints with the prefix `name`.
|
||||
// The goroutine will simulate some work and will terminate when one of the following
|
||||
// conditions happens:
|
||||
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
|
||||
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
|
||||
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
|
||||
// it should send a message; if instead it wants an asynchronous cancel, it should
|
||||
// close the channel.
|
||||
func worker(
|
||||
canceled <-chan struct{},
|
||||
howlong time.Duration,
|
||||
name string,
|
||||
) <-chan struct{} {
|
||||
workerDone := make(chan struct{})
|
||||
deadline := time.Now().Add(howlong)
|
||||
go func() {
|
||||
fmt.Printf("sleepit: %s started\n", name)
|
||||
for {
|
||||
select {
|
||||
case <-canceled:
|
||||
fmt.Printf("sleepit: %s canceled\n", name)
|
||||
return
|
||||
default:
|
||||
if doSomeWork(deadline) {
|
||||
fmt.Printf("sleepit: %s done\n", name) // <== NOTE THIS LINE
|
||||
workerDone <- struct{}{}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return workerDone
|
||||
}
|
||||
|
||||
// Do some work and then return, so that the caller can decide wether to continue or not.
|
||||
// Return true when all work is done.
|
||||
func doSomeWork(deadline time.Time) bool {
|
||||
if time.Now().After(deadline) {
|
||||
return true
|
||||
}
|
||||
timeout := 100 * time.Millisecond
|
||||
time.Sleep(timeout)
|
||||
return false
|
||||
}
|
||||
20
internal/slicesext/slicesext.go
Normal file
20
internal/slicesext/slicesext.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package slicesext
|
||||
|
||||
import (
|
||||
"golang.org/x/exp/constraints"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func UniqueJoin[T constraints.Ordered](ss ...[]T) []T {
|
||||
var length int
|
||||
for _, s := range ss {
|
||||
length += len(s)
|
||||
}
|
||||
r := make([]T, length)
|
||||
var i int
|
||||
for _, s := range ss {
|
||||
i += copy(r[i:], s)
|
||||
}
|
||||
slices.Sort(r)
|
||||
return slices.Compact(r)
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
)
|
||||
|
||||
// Checksum validades if a task is up to date by calculating its source
|
||||
// files checksum
|
||||
type Checksum struct {
|
||||
TempDir string
|
||||
TaskDir string
|
||||
Task string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Dry bool
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (c *Checksum) IsUpToDate() (bool, error) {
|
||||
if len(c.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
checksumFile := c.checksumFilePath()
|
||||
|
||||
data, _ := os.ReadFile(checksumFile)
|
||||
oldMd5 := strings.TrimSpace(string(data))
|
||||
|
||||
sources, err := globs(c.TaskDir, c.Sources)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newMd5, err := c.checksum(sources...)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !c.Dry {
|
||||
_ = os.MkdirAll(filepathext.SmartJoin(c.TempDir, "checksum"), 0o755)
|
||||
if err = os.WriteFile(checksumFile, []byte(newMd5+"\n"), 0o644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Generates) > 0 {
|
||||
// For each specified 'generates' field, check whether the files actually exist
|
||||
for _, g := range c.Generates {
|
||||
generates, err := Glob(c.TaskDir, g)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(generates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return oldMd5 == newMd5, nil
|
||||
}
|
||||
|
||||
func (c *Checksum) checksum(files ...string) (string, error) {
|
||||
h := md5.New()
|
||||
|
||||
for _, f := range files {
|
||||
// also sum the filename, so checksum changes for renaming a file
|
||||
if _, err := io.Copy(h, strings.NewReader(filepath.Base(f))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.Open(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (c *Checksum) Value() (interface{}, error) {
|
||||
return c.checksum()
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (c *Checksum) OnError() error {
|
||||
if len(c.Sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(c.checksumFilePath())
|
||||
}
|
||||
|
||||
// Kind implements the Checker Interface
|
||||
func (*Checksum) Kind() string {
|
||||
return "checksum"
|
||||
}
|
||||
|
||||
func (c *Checksum) checksumFilePath() string {
|
||||
return filepath.Join(c.TempDir, "checksum", c.normalizeFilename(c.Task))
|
||||
}
|
||||
|
||||
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
|
||||
|
||||
// replaces invalid caracters on filenames with "-"
|
||||
func (*Checksum) normalizeFilename(f string) string {
|
||||
return checksumFilenameRegexp.ReplaceAllString(f, "-")
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package status
|
||||
|
||||
// None is a no-op Checker
|
||||
type None struct{}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (None) IsUpToDate() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Value implements the Checker interface
|
||||
func (None) Value() (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (None) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (None) OnError() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package status
|
||||
|
||||
var (
|
||||
_ Checker = &Timestamp{}
|
||||
_ Checker = &Checksum{}
|
||||
_ Checker = None{}
|
||||
)
|
||||
|
||||
// Checker is an interface that checks if the status is up-to-date
|
||||
type Checker interface {
|
||||
IsUpToDate() (bool, error)
|
||||
Value() (interface{}, error)
|
||||
OnError() error
|
||||
Kind() string
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Timestamp checks if any source change compared with the generated files,
|
||||
// using file modifications timestamps.
|
||||
type Timestamp struct {
|
||||
Dir string
|
||||
Sources []string
|
||||
Generates []string
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
if len(t.Sources) == 0 || len(t.Generates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
generates, err := globs(t.Dir, t.Generates)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sourcesMaxTime, err := getMaxTime(sources...)
|
||||
if err != nil || sourcesMaxTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
generatesMinTime, err := getMinTime(generates...)
|
||||
if err != nil || generatesMinTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return !generatesMinTime.Before(sourcesMaxTime), nil
|
||||
}
|
||||
|
||||
func (t *Timestamp) Kind() string {
|
||||
return "timestamp"
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (t *Timestamp) Value() (interface{}, error) {
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
sourcesMaxTime, err := getMaxTime(sources...)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
if sourcesMaxTime.IsZero() {
|
||||
return time.Unix(0, 0), nil
|
||||
}
|
||||
|
||||
return sourcesMaxTime, nil
|
||||
}
|
||||
|
||||
func getMinTime(files ...string) (time.Time, error) {
|
||||
var t time.Time
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
t = minTime(t, info.ModTime())
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func getMaxTime(files ...string) (time.Time, error) {
|
||||
var t time.Time
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
t = maxTime(t, info.ModTime())
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func minTime(a, b time.Time) time.Time {
|
||||
if !a.IsZero() && a.Before(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxTime(a, b time.Time) time.Time {
|
||||
if a.After(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (*Timestamp) OnError() error {
|
||||
return nil
|
||||
}
|
||||
25
internal/version/version.go
Normal file
25
internal/version/version.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var version = ""
|
||||
|
||||
func GetVersion() string {
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info.Main.Version == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
ver := info.Main.Version
|
||||
if info.Main.Sum != "" {
|
||||
ver += fmt.Sprintf(" (%s)", info.Main.Sum)
|
||||
}
|
||||
return ver
|
||||
}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.19.0",
|
||||
"version": "3.22.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.19.0",
|
||||
"version": "3.22.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.19.0",
|
||||
"version": "3.22.0",
|
||||
"description": "A task runner / simpler Make alternative written in Go",
|
||||
"scripts": {
|
||||
"postinstall": "go-npm install",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
@@ -19,7 +20,7 @@ func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: p.Sh,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
Env: env.Get(t),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
66
setup.go
66
setup.go
@@ -9,6 +9,9 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/sajari/fuzzy"
|
||||
|
||||
compilerv2 "github.com/go-task/task/v3/internal/compiler/v2"
|
||||
compilerv3 "github.com/go-task/task/v3/internal/compiler/v3"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
@@ -17,8 +20,6 @@ import (
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/go-task/task/v3/taskfile/read"
|
||||
|
||||
"github.com/sajari/fuzzy"
|
||||
)
|
||||
|
||||
func (e *Executor) Setup() error {
|
||||
@@ -32,11 +33,6 @@ func (e *Executor) Setup() error {
|
||||
|
||||
e.setupFuzzyModel()
|
||||
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.setupTempDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,17 +41,17 @@ func (e *Executor) Setup() error {
|
||||
if err := e.setupOutput(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.setupCompiler(v); err != nil {
|
||||
if err := e.setupCompiler(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.readDotEnvFiles(v); err != nil {
|
||||
if err := e.readDotEnvFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.doVersionChecks(v); err != nil {
|
||||
if err := e.doVersionChecks(); err != nil {
|
||||
return err
|
||||
}
|
||||
e.setupDefaults(v)
|
||||
e.setupDefaults()
|
||||
e.setupConcurrencyState()
|
||||
|
||||
return nil
|
||||
@@ -163,8 +159,8 @@ func (e *Executor) setupOutput() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Executor) setupCompiler(v float64) error {
|
||||
if v < 3 {
|
||||
func (e *Executor) setupCompiler() error {
|
||||
if e.Taskfile.Version.LessThan(taskfile.V3) {
|
||||
var err error
|
||||
e.taskvars, err = read.Taskvars(e.Dir)
|
||||
if err != nil {
|
||||
@@ -195,8 +191,8 @@ func (e *Executor) setupCompiler(v float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) readDotEnvFiles(v float64) error {
|
||||
if v < 3.0 {
|
||||
func (e *Executor) readDotEnvFiles() error {
|
||||
if e.Taskfile.Version.LessThan(taskfile.V3) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -214,14 +210,14 @@ func (e *Executor) readDotEnvFiles(v float64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Executor) setupDefaults(v float64) {
|
||||
func (e *Executor) setupDefaults() {
|
||||
// Color available only on v3
|
||||
if v < 3 {
|
||||
if e.Taskfile.Version.LessThan(taskfile.V3) {
|
||||
e.Logger.Color = false
|
||||
}
|
||||
|
||||
if e.Taskfile.Method == "" {
|
||||
if v >= 3 {
|
||||
if e.Taskfile.Version.Compare(taskfile.V3) >= 0 {
|
||||
e.Taskfile.Method = "checksum"
|
||||
} else {
|
||||
e.Taskfile.Method = "timestamp"
|
||||
@@ -248,37 +244,41 @@ func (e *Executor) setupConcurrencyState() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) doVersionChecks(v float64) error {
|
||||
if v < 2 {
|
||||
func (e *Executor) doVersionChecks() error {
|
||||
// Copy the version to avoid modifying the original
|
||||
v := &semver.Version{}
|
||||
*v = *e.Taskfile.Version
|
||||
|
||||
if v.LessThan(taskfile.V2) {
|
||||
return fmt.Errorf(`task: Taskfile versions prior to v2 are not supported anymore`)
|
||||
}
|
||||
|
||||
// consider as equal to the greater version if round
|
||||
if v == 2.0 {
|
||||
v = 2.6
|
||||
if v.Equal(taskfile.V2) {
|
||||
v = semver.MustParse("2.6")
|
||||
}
|
||||
if v == 3.0 {
|
||||
v = 3.8
|
||||
if v.Equal(taskfile.V3) {
|
||||
v = semver.MustParse("3.8")
|
||||
}
|
||||
|
||||
if v > 3.8 {
|
||||
if v.GreaterThan(semver.MustParse("3.8")) {
|
||||
return fmt.Errorf(`task: Taskfile versions greater than v3.8 not implemented in the version of Task`)
|
||||
}
|
||||
|
||||
if v < 2.1 && !e.Taskfile.Output.IsSet() {
|
||||
if v.LessThan(semver.MustParse("2.1")) && !e.Taskfile.Output.IsSet() {
|
||||
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
|
||||
}
|
||||
if v < 2.2 && e.Taskfile.Includes.Len() > 0 {
|
||||
if v.LessThan(semver.MustParse("2.2")) && e.Taskfile.Includes.Len() > 0 {
|
||||
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
|
||||
}
|
||||
if v >= 3.0 && e.Taskfile.Expansions > 2 {
|
||||
if v.Compare(taskfile.V3) >= 0 && e.Taskfile.Expansions > 2 {
|
||||
return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`)
|
||||
}
|
||||
if v < 3.8 && e.Taskfile.Output.Group.IsSet() {
|
||||
if v.LessThan(semver.MustParse("3.8")) && e.Taskfile.Output.Group.IsSet() {
|
||||
return fmt.Errorf(`task: Taskfile option "output.group" is only available starting on Taskfile version v3.8`)
|
||||
}
|
||||
|
||||
if v <= 2.1 {
|
||||
if v.Compare(semver.MustParse("2.1")) <= 0 {
|
||||
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
|
||||
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
@@ -293,7 +293,7 @@ func (e *Executor) doVersionChecks(v float64) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v < 2.6 {
|
||||
if v.LessThan(semver.MustParse("2.6")) {
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
if len(task.Preconditions) > 0 {
|
||||
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
|
||||
@@ -301,7 +301,7 @@ func (e *Executor) doVersionChecks(v float64) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v < 3 {
|
||||
if v.LessThan(taskfile.V3) {
|
||||
err := e.Taskfile.Includes.Range(func(_ string, taskfile taskfile.IncludedTaskfile) error {
|
||||
if taskfile.AdvancedImport {
|
||||
return errors.New(`task: Import with additional parameters is only available starting on Taskfile version v3`)
|
||||
@@ -313,7 +313,7 @@ func (e *Executor) doVersionChecks(v float64) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v < 3.7 {
|
||||
if v.LessThan(semver.MustParse("3.7")) {
|
||||
if e.Taskfile.Run != "" {
|
||||
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)
|
||||
}
|
||||
|
||||
109
status.go
109
status.go
@@ -4,20 +4,33 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/status"
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// Status returns an error if any the of given tasks is not up-to-date
|
||||
func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error {
|
||||
for _, call := range calls {
|
||||
|
||||
// Compile the task
|
||||
t, err := e.CompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isUpToDate, err := e.isTaskUpToDate(ctx, t)
|
||||
|
||||
// Get the fingerprinting method to use
|
||||
method := e.Taskfile.Method
|
||||
if t.Method != "" {
|
||||
method = t.Method
|
||||
}
|
||||
|
||||
// Check if the task is up-to-date
|
||||
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -28,94 +41,14 @@ func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
if len(t.Status) == 0 && len(t.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(t.Status) > 0 {
|
||||
isUpToDate, err := e.isTaskUpToDateStatus(ctx, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !isUpToDate {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Sources) > 0 {
|
||||
checker, err := e.getStatusChecker(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
isUpToDate, err := checker.IsUpToDate()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !isUpToDate {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (e *Executor) statusOnError(t *taskfile.Task) error {
|
||||
checker, err := e.getStatusChecker(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checker.OnError()
|
||||
}
|
||||
|
||||
func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) {
|
||||
method := t.Method
|
||||
if method == "" {
|
||||
method = e.Taskfile.Method
|
||||
}
|
||||
switch method {
|
||||
case "timestamp":
|
||||
return e.timestampChecker(t), nil
|
||||
case "checksum":
|
||||
return e.checksumChecker(t), nil
|
||||
case "none":
|
||||
return status.None{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: invalid method "%s"`, method)
|
||||
checker, err := fingerprint.NewSourcesChecker(method, e.TempDir, e.Dry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) timestampChecker(t *taskfile.Task) status.Checker {
|
||||
return &status.Timestamp{
|
||||
Dir: t.Dir,
|
||||
Sources: t.Sources,
|
||||
Generates: t.Generates,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) checksumChecker(t *taskfile.Task) status.Checker {
|
||||
return &status.Checksum{
|
||||
TempDir: e.TempDir,
|
||||
TaskDir: t.Dir,
|
||||
Task: t.Name(),
|
||||
Sources: t.Sources,
|
||||
Generates: t.Generates,
|
||||
Dry: e.Dry,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
for _, s := range t.Status {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: s,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
})
|
||||
if err != nil {
|
||||
e.Logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err)
|
||||
return false, nil
|
||||
}
|
||||
e.Logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s)
|
||||
}
|
||||
return true, nil
|
||||
return checker.OnError(t)
|
||||
}
|
||||
|
||||
180
task.go
180
task.go
@@ -5,15 +5,20 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/slicesext"
|
||||
"github.com/go-task/task/v3/internal/summary"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
@@ -45,7 +50,7 @@ type Executor struct {
|
||||
Parallel bool
|
||||
Color bool
|
||||
Concurrency int
|
||||
Interval string
|
||||
Interval time.Duration
|
||||
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
@@ -72,12 +77,20 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
|
||||
for _, call := range calls {
|
||||
task, err := e.GetTask(call)
|
||||
if err != nil {
|
||||
e.ListTasks(FilterOutInternal(), FilterOutNoDesc())
|
||||
if _, ok := err.(*taskNotFoundError); ok {
|
||||
if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if task.Internal {
|
||||
e.ListTasks(FilterOutInternal(), FilterOutNoDesc())
|
||||
if _, ok := err.(*taskNotFoundError); ok {
|
||||
if _, err := e.ListTasks(ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return &taskInternalError{taskName: call.Task}
|
||||
}
|
||||
}
|
||||
@@ -126,6 +139,11 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
|
||||
defer release()
|
||||
|
||||
return e.startExecution(ctx, t, func(ctx context.Context) error {
|
||||
if !shouldRunOnCurrentPlatform(t.Platforms) {
|
||||
e.Logger.VerboseOutf(logger.Yellow, `task: "%s" not for current platform - ignored`, call.Task)
|
||||
return nil
|
||||
}
|
||||
|
||||
e.Logger.VerboseErrf(logger.Magenta, `task: "%s" started`, call.Task)
|
||||
if err := e.runDeps(ctx, t); err != nil {
|
||||
return err
|
||||
@@ -141,7 +159,18 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
|
||||
return err
|
||||
}
|
||||
|
||||
upToDate, err := e.isTaskUpToDate(ctx, t)
|
||||
// Get the fingerprinting method to use
|
||||
method := e.Taskfile.Method
|
||||
if t.Method != "" {
|
||||
method = t.Method
|
||||
}
|
||||
|
||||
upToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -243,6 +272,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
|
||||
}
|
||||
return nil
|
||||
case cmd.Cmd != "":
|
||||
if !shouldRunOnCurrentPlatform(cmd.Platforms) {
|
||||
e.Logger.VerboseOutf(logger.Yellow, `task: [%s] %s not for current platform - ignored`, t.Name(), cmd.Cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
|
||||
e.Logger.Errf(logger.Green, "task: [%s] %s", t.Name(), cmd.Cmd)
|
||||
}
|
||||
@@ -261,20 +295,20 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
|
||||
return fmt.Errorf("task: failed to get variables: %w", err)
|
||||
}
|
||||
stdOut, stdErr, close := outputWrapper.WrapWriter(e.Stdout, e.Stderr, t.Prefix, outputTemplater)
|
||||
defer func() {
|
||||
if err := close(); err != nil {
|
||||
e.Logger.Errf(logger.Red, "task: unable to close writter: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: cmd.Cmd,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
Stdin: e.Stdin,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
Command: cmd.Cmd,
|
||||
Dir: t.Dir,
|
||||
Env: env.Get(t),
|
||||
PosixOpts: slicesext.UniqueJoin(e.Taskfile.Set, t.Set, cmd.Set),
|
||||
BashOpts: slicesext.UniqueJoin(e.Taskfile.Shopt, t.Shopt, cmd.Shopt),
|
||||
Stdin: e.Stdin,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
})
|
||||
if closeErr := close(err); closeErr != nil {
|
||||
e.Logger.Errf(logger.Red, "task: unable to close writer: %v", closeErr)
|
||||
}
|
||||
if execext.IsExitError(err) && cmd.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v", t.Name(), err)
|
||||
return nil
|
||||
@@ -285,29 +319,6 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
|
||||
}
|
||||
}
|
||||
|
||||
func getEnviron(t *taskfile.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
environ := os.Environ()
|
||||
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
str, isString := v.(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
|
||||
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
|
||||
}
|
||||
|
||||
return environ
|
||||
}
|
||||
|
||||
func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute func(ctx context.Context) error) error {
|
||||
h, err := e.GetHash(t)
|
||||
if err != nil {
|
||||
@@ -319,11 +330,15 @@ func (e *Executor) startExecution(ctx context.Context, t *taskfile.Task, execute
|
||||
}
|
||||
|
||||
e.executionHashesMutex.Lock()
|
||||
otherExecutionCtx, ok := e.executionHashes[h]
|
||||
|
||||
if ok {
|
||||
if otherExecutionCtx, ok := e.executionHashes[h]; ok {
|
||||
e.executionHashesMutex.Unlock()
|
||||
e.Logger.VerboseErrf(logger.Magenta, "task: skipping execution of task: %s", h)
|
||||
|
||||
// Release our execution slot to avoid blocking other tasks while we wait
|
||||
reacquire := e.releaseConcurrencyLimit()
|
||||
defer reacquire()
|
||||
|
||||
<-otherExecutionCtx.Done()
|
||||
return nil
|
||||
}
|
||||
@@ -377,26 +392,42 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) {
|
||||
return matchingTask, nil
|
||||
}
|
||||
|
||||
type FilterFunc func(tasks []*taskfile.Task) []*taskfile.Task
|
||||
type FilterFunc func(task *taskfile.Task) bool
|
||||
|
||||
func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task {
|
||||
func (e *Executor) GetTaskList(filters ...FilterFunc) ([]*taskfile.Task, error) {
|
||||
tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
|
||||
|
||||
// Create an error group to wait for each task to be compiled
|
||||
var g errgroup.Group
|
||||
|
||||
// Fetch and compile the list of tasks
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task})
|
||||
if err == nil {
|
||||
task = compiledTask
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
for key := range e.Taskfile.Tasks {
|
||||
task := e.Taskfile.Tasks[key]
|
||||
g.Go(func() error {
|
||||
|
||||
// Check if we should filter the task
|
||||
for _, filter := range filters {
|
||||
if filter(task) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Compile the task
|
||||
compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task})
|
||||
if err == nil {
|
||||
task = compiledTask
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Filter the tasks
|
||||
for _, filter := range filters {
|
||||
tasks = filter(tasks)
|
||||
// Wait for all the go routines to finish
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort the tasks
|
||||
// Sort the tasks.
|
||||
// Tasks that are not namespaced should be listed before tasks that are.
|
||||
// We detect this by searching for a ':' in the task name.
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
@@ -411,38 +442,27 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task {
|
||||
return false
|
||||
})
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// Filter is a generic task filtering function. It will remove each task in the
|
||||
// slice where the result of the given function is true.
|
||||
func Filter(f func(task *taskfile.Task) bool) FilterFunc {
|
||||
return func(tasks []*taskfile.Task) []*taskfile.Task {
|
||||
shift := 0
|
||||
for _, task := range tasks {
|
||||
if !f(task) {
|
||||
tasks[shift] = task
|
||||
shift++
|
||||
}
|
||||
}
|
||||
// This loop stops any memory leaks
|
||||
for j := shift; j < len(tasks); j++ {
|
||||
tasks[j] = nil
|
||||
}
|
||||
return slices.Clip(tasks[:shift])
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// FilterOutNoDesc removes all tasks that do not contain a description.
|
||||
func FilterOutNoDesc() FilterFunc {
|
||||
return Filter(func(task *taskfile.Task) bool {
|
||||
return task.Desc == ""
|
||||
})
|
||||
func FilterOutNoDesc(task *taskfile.Task) bool {
|
||||
return task.Desc == ""
|
||||
}
|
||||
|
||||
// FilterOutInternal removes all tasks that are marked as internal.
|
||||
func FilterOutInternal() FilterFunc {
|
||||
return Filter(func(task *taskfile.Task) bool {
|
||||
return task.Internal
|
||||
})
|
||||
func FilterOutInternal(task *taskfile.Task) bool {
|
||||
return task.Internal
|
||||
}
|
||||
|
||||
func shouldRunOnCurrentPlatform(platforms []*taskfile.Platform) bool {
|
||||
if len(platforms) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, p := range platforms {
|
||||
if (p.OS == "" || p.OS == runtime.GOOS) && (p.Arch == "" || p.Arch == runtime.GOARCH) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
268
task_test.go
268
task_test.go
@@ -10,8 +10,8 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
@@ -191,11 +191,13 @@ func TestSpecialVars(t *testing.T) {
|
||||
assert.Contains(t, output, "root/TASK=print")
|
||||
assert.Contains(t, output, "root/ROOT_DIR="+toAbs("testdata/special_vars"))
|
||||
assert.Contains(t, output, "root/TASKFILE_DIR="+toAbs("testdata/special_vars"))
|
||||
assert.Contains(t, output, "root/TASK_VERSION=unknown")
|
||||
|
||||
// Included Taskfile
|
||||
assert.Contains(t, output, "included/TASK=included:print")
|
||||
assert.Contains(t, output, "included/ROOT_DIR="+toAbs("testdata/special_vars"))
|
||||
assert.Contains(t, output, "included/TASKFILE_DIR="+toAbs("testdata/special_vars/included"))
|
||||
assert.Contains(t, output, "included/TASK_VERSION=unknown")
|
||||
}
|
||||
|
||||
func TestVarsInvalidTmpl(t *testing.T) {
|
||||
@@ -445,36 +447,43 @@ func TestGenerates(t *testing.T) {
|
||||
func TestStatusChecksum(t *testing.T) {
|
||||
const dir = "testdata/checksum"
|
||||
|
||||
files := []string{
|
||||
"generated.txt",
|
||||
".task/checksum/build",
|
||||
tests := []struct {
|
||||
files []string
|
||||
task string
|
||||
}{
|
||||
{[]string{"generated.txt", ".task/checksum/build"}, "build"},
|
||||
{[]string{"generated.txt", ".task/checksum/build-with-status"}, "build-with-status"},
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
_ = os.Remove(filepathext.SmartJoin(dir, f))
|
||||
for _, test := range tests {
|
||||
t.Run(test.task, func(t *testing.T) {
|
||||
for _, f := range test.files {
|
||||
_ = os.Remove(filepathext.SmartJoin(dir, f))
|
||||
|
||||
_, err := os.Stat(filepathext.SmartJoin(dir, f))
|
||||
assert.Error(t, err)
|
||||
_, err := os.Stat(filepathext.SmartJoin(dir, f))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
TempDir: filepathext.SmartJoin(dir, ".task"),
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: test.task}))
|
||||
for _, f := range test.files {
|
||||
_, err := os.Stat(filepathext.SmartJoin(dir, f))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
buff.Reset()
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: test.task}))
|
||||
assert.Equal(t, `task: Task "`+test.task+`" is up to date`+"\n", buff.String())
|
||||
})
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
TempDir: filepathext.SmartJoin(dir, ".task"),
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
|
||||
for _, f := range files {
|
||||
_, err := os.Stat(filepathext.SmartJoin(dir, f))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
buff.Reset()
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
|
||||
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
|
||||
}
|
||||
|
||||
func TestAlias(t *testing.T) {
|
||||
@@ -606,7 +615,9 @@ func TestNoLabelInList(t *testing.T) {
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc())
|
||||
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Contains(t, buff.String(), "foo")
|
||||
}
|
||||
|
||||
@@ -624,7 +635,9 @@ func TestListAllShowsNoDesc(t *testing.T) {
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
var title string
|
||||
e.ListTasks(task.FilterOutInternal())
|
||||
if _, err := e.ListTasks(task.ListOptions{ListAllTasks: true}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for _, title = range []string{
|
||||
"foo",
|
||||
"voo",
|
||||
@@ -646,7 +659,9 @@ func TestListCanListDescOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.NoError(t, e.Setup())
|
||||
e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc())
|
||||
if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var title string
|
||||
assert.Contains(t, buff.String(), "foo")
|
||||
@@ -721,9 +736,9 @@ func TestCyclicDep(t *testing.T) {
|
||||
func TestTaskVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
Dir string
|
||||
Version string
|
||||
Version *semver.Version
|
||||
}{
|
||||
{"testdata/version/v2", "2"},
|
||||
{"testdata/version/v2", semver.MustParse("2")},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -1034,7 +1049,7 @@ func TestIncludesInternal(t *testing.T) {
|
||||
}{
|
||||
{"included internal task via task", "task-1", false, "Hello, World!\n"},
|
||||
{"included internal task via dep", "task-2", false, "Hello, World!\n"},
|
||||
{"included internal direct", "included:task-3", true, ""},
|
||||
{"included internal direct", "included:task-3", true, "task: No tasks with description available. Try --list-all to list all tasks\n"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -1561,6 +1576,36 @@ Bye!
|
||||
t.Log(buff.String())
|
||||
assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
|
||||
}
|
||||
func TestOutputGroupErrorOnlySwallowsOutputOnSuccess(t *testing.T) {
|
||||
const dir = "testdata/output_group_error_only"
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "passing"}))
|
||||
t.Log(buff.String())
|
||||
assert.Empty(t, buff.String())
|
||||
}
|
||||
|
||||
func TestOutputGroupErrorOnlyShowsOutputOnFailure(t *testing.T) {
|
||||
const dir = "testdata/output_group_error_only"
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "failing"}))
|
||||
t.Log(buff.String())
|
||||
assert.Contains(t, "failing-output", strings.TrimSpace(buff.String()))
|
||||
assert.NotContains(t, "passing", strings.TrimSpace(buff.String()))
|
||||
}
|
||||
|
||||
func TestIncludedVars(t *testing.T) {
|
||||
const dir = "testdata/include_with_vars"
|
||||
@@ -1643,67 +1688,6 @@ func TestEvaluateSymlinksInPaths(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFileWatcherInterval(t *testing.T) {
|
||||
const dir = "testdata/watcher_interval"
|
||||
expectedOutput := strings.TrimSpace(`
|
||||
task: Started watching for tasks: default
|
||||
task: [default] echo "Hello, World!"
|
||||
Hello, World!
|
||||
task: [default] echo "Hello, World!"
|
||||
Hello, World!
|
||||
`)
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Watch: true,
|
||||
}
|
||||
|
||||
assert.NoError(t, e.Setup())
|
||||
buff.Reset()
|
||||
|
||||
err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
err := e.Run(ctx, taskfile.Call{Task: "default"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(700 * time.Millisecond)
|
||||
cancel()
|
||||
assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
|
||||
buff.Reset()
|
||||
err = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestTaskfileWalk(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1752,3 +1736,99 @@ func TestUserWorkingDirectory(t *testing.T) {
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
||||
assert.Equal(t, fmt.Sprintf("%s\n", wd), buff.String())
|
||||
}
|
||||
|
||||
func TestPlatforms(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/platforms",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-" + runtime.GOOS}))
|
||||
assert.Equal(t, fmt.Sprintf("task: [build-%s] echo 'Running task on %s'\nRunning task on %s\n", runtime.GOOS, runtime.GOOS, runtime.GOOS), buff.String())
|
||||
}
|
||||
|
||||
func TestPOSIXShellOptsGlobalLevel(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/shopts/global_level",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pipefail\ton\n", buff.String())
|
||||
}
|
||||
|
||||
func TestPOSIXShellOptsTaskLevel(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/shopts/task_level",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pipefail\ton\n", buff.String())
|
||||
}
|
||||
|
||||
func TestPOSIXShellOptsCommandLevel(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/shopts/command_level",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(context.Background(), taskfile.Call{Task: "pipefail"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pipefail\ton\n", buff.String())
|
||||
}
|
||||
|
||||
func TestBashShellOptsGlobalLevel(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/shopts/global_level",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "globstar\ton\n", buff.String())
|
||||
}
|
||||
|
||||
func TestBashShellOptsTaskLevel(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/shopts/task_level",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "globstar\ton\n", buff.String())
|
||||
}
|
||||
|
||||
func TestBashShellOptsCommandLevel(t *testing.T) {
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: "testdata/shopts/command_level",
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
err := e.Run(context.Background(), taskfile.Call{Task: "globstar"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "globstar\ton\n", buff.String())
|
||||
}
|
||||
|
||||
152
taskfile/cmd.go
152
taskfile/cmd.go
@@ -1,13 +1,22 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Cmd is a task command
|
||||
type Cmd struct {
|
||||
Cmd string
|
||||
Silent bool
|
||||
Task string
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
IgnoreError bool
|
||||
Defer bool
|
||||
Platforms []*Platform
|
||||
}
|
||||
|
||||
// Dep is a task dependency
|
||||
@@ -16,68 +25,99 @@ type Dep struct {
|
||||
Vars *Vars
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd string
|
||||
if err := unmarshal(&cmd); err == nil {
|
||||
func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Cmd = cmd
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
|
||||
// A command with additional options
|
||||
var cmdStruct struct {
|
||||
Cmd string
|
||||
Silent bool
|
||||
Set []string
|
||||
Shopt []string
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
Platforms []*Platform
|
||||
}
|
||||
if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
|
||||
c.Cmd = cmdStruct.Cmd
|
||||
c.Silent = cmdStruct.Silent
|
||||
c.Set = cmdStruct.Set
|
||||
c.Shopt = cmdStruct.Shopt
|
||||
c.IgnoreError = cmdStruct.IgnoreError
|
||||
c.Platforms = cmdStruct.Platforms
|
||||
return nil
|
||||
}
|
||||
|
||||
// A deferred command
|
||||
var deferredCmd struct {
|
||||
Defer string
|
||||
}
|
||||
if err := node.Decode(&deferredCmd); err == nil && deferredCmd.Defer != "" {
|
||||
c.Defer = true
|
||||
c.Cmd = deferredCmd.Defer
|
||||
return nil
|
||||
}
|
||||
|
||||
// A deferred task call
|
||||
var deferredCall struct {
|
||||
Defer Call
|
||||
}
|
||||
if err := node.Decode(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
|
||||
c.Defer = true
|
||||
c.Task = deferredCall.Defer.Task
|
||||
c.Vars = deferredCall.Defer.Vars
|
||||
return nil
|
||||
}
|
||||
|
||||
// A task call
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" {
|
||||
c.Task = taskCall.Task
|
||||
c.Vars = taskCall.Vars
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("yaml: line %d: invalid keys in command", node.Line)
|
||||
}
|
||||
var cmdStruct struct {
|
||||
Cmd string
|
||||
Silent bool
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
}
|
||||
if err := unmarshal(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
|
||||
c.Cmd = cmdStruct.Cmd
|
||||
c.Silent = cmdStruct.Silent
|
||||
c.IgnoreError = cmdStruct.IgnoreError
|
||||
return nil
|
||||
}
|
||||
var deferredCmd struct {
|
||||
Defer string
|
||||
}
|
||||
if err := unmarshal(&deferredCmd); err == nil && deferredCmd.Defer != "" {
|
||||
c.Defer = true
|
||||
c.Cmd = deferredCmd.Defer
|
||||
return nil
|
||||
}
|
||||
var deferredCall struct {
|
||||
Defer Call
|
||||
}
|
||||
if err := unmarshal(&deferredCall); err == nil && deferredCall.Defer.Task != "" {
|
||||
c.Defer = true
|
||||
c.Task = deferredCall.Defer.Task
|
||||
c.Vars = deferredCall.Defer.Vars
|
||||
return nil
|
||||
}
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := unmarshal(&taskCall); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Task = taskCall.Task
|
||||
c.Vars = taskCall.Vars
|
||||
return nil
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into command", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var task string
|
||||
if err := unmarshal(&task); err == nil {
|
||||
func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var task string
|
||||
if err := node.Decode(&task); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Task = task
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := node.Decode(&taskCall); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Task = taskCall.Task
|
||||
d.Vars = taskCall.Vars
|
||||
return nil
|
||||
}
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := unmarshal(&taskCall); err != nil {
|
||||
return err
|
||||
}
|
||||
d.Task = taskCall.Task
|
||||
d.Vars = taskCall.Vars
|
||||
return nil
|
||||
|
||||
return fmt.Errorf("cannot unmarshal %s into dependency", node.ShortTag())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
@@ -32,24 +31,26 @@ type IncludedTaskfiles struct {
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (tfs *IncludedTaskfiles) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return errors.New("task: includes is not a map")
|
||||
}
|
||||
switch node.Kind {
|
||||
|
||||
// NOTE(@andreynering): on this style of custom unmarsheling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
case yaml.MappingNode:
|
||||
// NOTE(@andreynering): on this style of custom unmarshalling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
var v IncludedTaskfile
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
var v IncludedTaskfile
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
tfs.Set(keyNode.Value, v)
|
||||
}
|
||||
tfs.Set(keyNode.Value, v)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfiles", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// Len returns the length of the map
|
||||
@@ -60,14 +61,6 @@ func (tfs *IncludedTaskfiles) Len() int {
|
||||
return len(tfs.Keys)
|
||||
}
|
||||
|
||||
// Merge merges the given IncludedTaskfiles into the caller one
|
||||
func (tfs *IncludedTaskfiles) Merge(other *IncludedTaskfiles) {
|
||||
_ = other.Range(func(key string, value IncludedTaskfile) error {
|
||||
tfs.Set(key, value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Set sets a value to a given key
|
||||
func (tfs *IncludedTaskfiles) Set(key string, includedTaskfile IncludedTaskfile) {
|
||||
if tfs.Mapping == nil {
|
||||
@@ -92,33 +85,40 @@ func (tfs *IncludedTaskfiles) Range(yield func(key string, includedTaskfile Incl
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var str string
|
||||
if err := unmarshal(&str); err == nil {
|
||||
func (it *IncludedTaskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var str string
|
||||
if err := node.Decode(&str); err != nil {
|
||||
return err
|
||||
}
|
||||
it.Taskfile = str
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var includedTaskfile struct {
|
||||
Taskfile string
|
||||
Dir string
|
||||
Optional bool
|
||||
Internal bool
|
||||
Aliases []string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := node.Decode(&includedTaskfile); err != nil {
|
||||
return err
|
||||
}
|
||||
it.Taskfile = includedTaskfile.Taskfile
|
||||
it.Dir = includedTaskfile.Dir
|
||||
it.Optional = includedTaskfile.Optional
|
||||
it.Internal = includedTaskfile.Internal
|
||||
it.Aliases = includedTaskfile.Aliases
|
||||
it.AdvancedImport = true
|
||||
it.Vars = includedTaskfile.Vars
|
||||
return nil
|
||||
}
|
||||
|
||||
var includedTaskfile struct {
|
||||
Taskfile string
|
||||
Dir string
|
||||
Optional bool
|
||||
Internal bool
|
||||
Aliases []string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := unmarshal(&includedTaskfile); err != nil {
|
||||
return err
|
||||
}
|
||||
it.Taskfile = includedTaskfile.Taskfile
|
||||
it.Dir = includedTaskfile.Dir
|
||||
it.Optional = includedTaskfile.Optional
|
||||
it.Internal = includedTaskfile.Internal
|
||||
it.Aliases = includedTaskfile.Aliases
|
||||
it.AdvancedImport = true
|
||||
it.Vars = includedTaskfile.Vars
|
||||
return nil
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into included taskfile", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of IncludedTaskfile and copies
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NamespaceSeparator contains the character that separates namescapes
|
||||
// NamespaceSeparator contains the character that separates namespaces
|
||||
const NamespaceSeparator = ":"
|
||||
|
||||
// Merge merges the second Taskfile into the first
|
||||
func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...string) error {
|
||||
if t1.Version != t2.Version {
|
||||
if !t1.Version.Equal(t2.Version) {
|
||||
return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,6 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
|
||||
t1.Output = t2.Output
|
||||
}
|
||||
|
||||
if t1.Includes == nil {
|
||||
t1.Includes = &IncludedTaskfiles{}
|
||||
}
|
||||
t1.Includes.Merge(t2.Includes)
|
||||
|
||||
if t1.Vars == nil {
|
||||
t1.Vars = &Vars{}
|
||||
}
|
||||
@@ -45,7 +40,7 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
|
||||
|
||||
// Set the task to internal if EITHER the included task or the included
|
||||
// taskfile are marked as internal
|
||||
task.Internal = task.Internal || includedTaskfile.Internal
|
||||
task.Internal = task.Internal || (includedTaskfile != nil && includedTaskfile.Internal)
|
||||
|
||||
// Add namespaces to dependencies, commands and aliases
|
||||
for _, dep := range task.Deps {
|
||||
|
||||
@@ -2,6 +2,8 @@ package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Output of the Task output
|
||||
@@ -17,33 +19,41 @@ func (s *Output) IsSet() bool {
|
||||
return s.Name != ""
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler
|
||||
// It accepts a scalar node representing the Output.Name or a mapping node representing the OutputGroup.
|
||||
func (s *Output) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var name string
|
||||
if err := unmarshal(&name); err == nil {
|
||||
func (s *Output) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var name string
|
||||
if err := node.Decode(&name); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Name = name
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var tmp struct {
|
||||
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)
|
||||
}
|
||||
if tmp.Group == nil {
|
||||
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
|
||||
}
|
||||
*s = Output{
|
||||
Name: "group",
|
||||
Group: *tmp.Group,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var tmp struct {
|
||||
Group *OutputGroup
|
||||
}
|
||||
if err := unmarshal(&tmp); err != nil {
|
||||
return fmt.Errorf("task: output style must be a string or mapping with a \"group\" key: %w", err)
|
||||
}
|
||||
if tmp.Group == nil {
|
||||
return fmt.Errorf("task: output style must have the \"group\" key when in mapping form")
|
||||
}
|
||||
*s = Output{
|
||||
Name: "group",
|
||||
Group: *tmp.Group,
|
||||
}
|
||||
return nil
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into output", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// OutputGroup is the style options specific to the Group style.
|
||||
type OutputGroup struct {
|
||||
Begin, End string
|
||||
ErrorOnly bool `yaml:"error_only"`
|
||||
}
|
||||
|
||||
// IsSet returns true if and only if a custom output style is set.
|
||||
|
||||
90
taskfile/platforms.go
Normal file
90
taskfile/platforms.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/go-task/task/v3/internal/goext"
|
||||
)
|
||||
|
||||
// Platform represents GOOS and GOARCH values
|
||||
type Platform struct {
|
||||
OS string
|
||||
Arch string
|
||||
}
|
||||
|
||||
type ErrInvalidPlatform struct {
|
||||
Platform string
|
||||
}
|
||||
|
||||
func (err *ErrInvalidPlatform) Error() string {
|
||||
return fmt.Sprintf(`task: Invalid platform "%s"`, err.Platform)
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (p *Platform) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var platform string
|
||||
if err := node.Decode(&platform); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.parsePlatform(platform); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into platform", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// parsePlatform takes a string representing an OS/Arch combination (or either on their own)
|
||||
// and parses it into the Platform struct. It returns an error if the input string is invalid.
|
||||
// Valid combinations for input: OS, Arch, OS/Arch
|
||||
func (p *Platform) parsePlatform(input string) error {
|
||||
splitValues := strings.Split(input, "/")
|
||||
if len(splitValues) > 2 {
|
||||
return &ErrInvalidPlatform{Platform: input}
|
||||
}
|
||||
if err := p.parseOsOrArch(splitValues[0]); err != nil {
|
||||
return &ErrInvalidPlatform{Platform: input}
|
||||
}
|
||||
if len(splitValues) == 2 {
|
||||
if err := p.parseArch(splitValues[1]); err != nil {
|
||||
return &ErrInvalidPlatform{Platform: input}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseOsOrArch will check if the given input is a valid OS or Arch value.
|
||||
// If so, it will store it. If not, an error is returned
|
||||
func (p *Platform) parseOsOrArch(osOrArch string) error {
|
||||
if osOrArch == "" {
|
||||
return fmt.Errorf("task: Blank OS/Arch value provided")
|
||||
}
|
||||
if goext.IsKnownOS(osOrArch) {
|
||||
p.OS = osOrArch
|
||||
return nil
|
||||
}
|
||||
if goext.IsKnownArch(osOrArch) {
|
||||
p.Arch = osOrArch
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("task: Invalid OS/Arch value provided (%s)", osOrArch)
|
||||
}
|
||||
|
||||
func (p *Platform) parseArch(arch string) error {
|
||||
if arch == "" {
|
||||
return fmt.Errorf("task: Blank Arch value provided")
|
||||
}
|
||||
if p.Arch != "" {
|
||||
return fmt.Errorf("task: Multiple Arch values provided")
|
||||
}
|
||||
if goext.IsKnownArch(arch) {
|
||||
p.Arch = arch
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("task: Invalid Arch value provided (%s)", arch)
|
||||
}
|
||||
49
taskfile/platforms_test.go
Normal file
49
taskfile/platforms_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPlatformParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
Input string
|
||||
ExpectedOS string
|
||||
ExpectedArch string
|
||||
Error string
|
||||
}{
|
||||
{Input: "windows", ExpectedOS: "windows", ExpectedArch: ""},
|
||||
{Input: "linux", ExpectedOS: "linux", ExpectedArch: ""},
|
||||
{Input: "darwin", ExpectedOS: "darwin", ExpectedArch: ""},
|
||||
|
||||
{Input: "386", ExpectedOS: "", ExpectedArch: "386"},
|
||||
{Input: "amd64", ExpectedOS: "", ExpectedArch: "amd64"},
|
||||
{Input: "arm64", ExpectedOS: "", ExpectedArch: "arm64"},
|
||||
|
||||
{Input: "windows/386", ExpectedOS: "windows", ExpectedArch: "386"},
|
||||
{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"`},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Input, func(t *testing.T) {
|
||||
var p Platform
|
||||
err := p.parsePlatform(test.Input)
|
||||
|
||||
if test.Error != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, test.Error, err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.ExpectedOS, p.OS)
|
||||
assert.Equal(t, test.ExpectedArch, p.Arch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package taskfile
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,29 +19,33 @@ type Precondition struct {
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd string
|
||||
func (p *Precondition) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
if err := unmarshal(&cmd); err == nil {
|
||||
case yaml.ScalarNode:
|
||||
var cmd string
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Sh = cmd
|
||||
p.Msg = fmt.Sprintf("`%s` failed", cmd)
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var sh struct {
|
||||
Sh string
|
||||
Msg string
|
||||
}
|
||||
if err := node.Decode(&sh); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Sh = sh.Sh
|
||||
p.Msg = sh.Msg
|
||||
if p.Msg == "" {
|
||||
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var sh struct {
|
||||
Sh string
|
||||
Msg string
|
||||
}
|
||||
|
||||
if err := unmarshal(&sh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Sh = sh.Sh
|
||||
p.Msg = sh.Msg
|
||||
if p.Msg == "" {
|
||||
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
|
||||
}
|
||||
|
||||
return nil
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into precondition", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
@@ -58,11 +58,6 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
v, err := t.ParsedVersion()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Annotate any included Taskfile reference with a base directory for resolving relative paths
|
||||
_ = t.Includes.Range(func(key string, includedFile taskfile.IncludedTaskfile) error {
|
||||
// Set the base directory for resolving relative paths, but only if not already set
|
||||
@@ -74,7 +69,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
})
|
||||
|
||||
err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error {
|
||||
if v >= 3.0 {
|
||||
if t.Version.Compare(taskfile.V3) >= 0 {
|
||||
tr := templater.Templater{Vars: t.Vars, RemoveNoValue: true}
|
||||
includedTask = taskfile.IncludedTaskfile{
|
||||
Taskfile: tr.Replace(includedTask.Taskfile),
|
||||
@@ -123,7 +118,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 {
|
||||
if t.Version.Compare(taskfile.V3) >= 0 && len(includedTaskfile.Dotenv) > 0 {
|
||||
return ErrIncludedTaskfilesCantHaveDotenvs
|
||||
}
|
||||
|
||||
@@ -168,7 +163,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, string, error) {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if v < 3.0 {
|
||||
if t.Version.Compare(taskfile.V3) < 0 {
|
||||
path = filepathext.SmartJoin(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
|
||||
if _, err = os.Stat(path); err == nil {
|
||||
osTaskfile, err := readTaskfile(path)
|
||||
@@ -197,6 +192,8 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var t taskfile.Taskfile
|
||||
if err := yaml.NewDecoder(f).Decode(&t); err != nil {
|
||||
return nil, fmt.Errorf("task: Failed to parse %s:\n%w", filepathext.TryAbsToRel(file), err)
|
||||
|
||||
139
taskfile/task.go
139
taskfile/task.go
@@ -1,5 +1,11 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Tasks represents a group of tasks
|
||||
type Tasks map[string]*Task
|
||||
|
||||
@@ -17,6 +23,8 @@ type Task struct {
|
||||
Status []string
|
||||
Preconditions []*Precondition
|
||||
Dir string
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
@@ -30,6 +38,7 @@ type Task struct {
|
||||
IncludeVars *Vars
|
||||
IncludedTaskfileVars *Vars
|
||||
IncludedTaskfile *IncludedTaskfile
|
||||
Platforms []*Platform
|
||||
}
|
||||
|
||||
func (t *Task) Name() string {
|
||||
@@ -39,67 +48,86 @@ func (t *Task) Name() string {
|
||||
return t.Task
|
||||
}
|
||||
|
||||
func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd Cmd
|
||||
if err := unmarshal(&cmd); err == nil && cmd.Cmd != "" {
|
||||
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
// Shortcut syntax for a task with a single command
|
||||
case yaml.ScalarNode:
|
||||
var cmd Cmd
|
||||
if err := node.Decode(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Cmds = append(t.Cmds, &cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
var cmds []*Cmd
|
||||
if err := unmarshal(&cmds); err == nil && len(cmds) > 0 {
|
||||
// Shortcut syntax for a simple task with a list of commands
|
||||
case yaml.SequenceNode:
|
||||
var cmds []*Cmd
|
||||
if err := node.Decode(&cmds); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Cmds = cmds
|
||||
return nil
|
||||
|
||||
// Full task object
|
||||
case yaml.MappingNode:
|
||||
var task struct {
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Summary string
|
||||
Aliases []string
|
||||
Sources []string
|
||||
Generates []string
|
||||
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 `yaml:"ignore_error"`
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
}
|
||||
if err := node.Decode(&task); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Cmds = task.Cmds
|
||||
t.Deps = task.Deps
|
||||
t.Label = task.Label
|
||||
t.Desc = task.Desc
|
||||
t.Summary = task.Summary
|
||||
t.Aliases = task.Aliases
|
||||
t.Sources = task.Sources
|
||||
t.Generates = task.Generates
|
||||
t.Status = task.Status
|
||||
t.Preconditions = task.Preconditions
|
||||
t.Dir = task.Dir
|
||||
t.Set = task.Set
|
||||
t.Shopt = task.Shopt
|
||||
t.Vars = task.Vars
|
||||
t.Env = task.Env
|
||||
t.Dotenv = task.Dotenv
|
||||
t.Silent = task.Silent
|
||||
t.Interactive = task.Interactive
|
||||
t.Internal = task.Internal
|
||||
t.Method = task.Method
|
||||
t.Prefix = task.Prefix
|
||||
t.IgnoreError = task.IgnoreError
|
||||
t.Run = task.Run
|
||||
t.Platforms = task.Platforms
|
||||
return nil
|
||||
}
|
||||
|
||||
var task struct {
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Summary string
|
||||
Aliases []string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Status []string
|
||||
Preconditions []*Precondition
|
||||
Dir string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Dotenv []string
|
||||
Silent bool
|
||||
Interactive bool
|
||||
Internal bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
Run string
|
||||
}
|
||||
if err := unmarshal(&task); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Cmds = task.Cmds
|
||||
t.Deps = task.Deps
|
||||
t.Label = task.Label
|
||||
t.Desc = task.Desc
|
||||
t.Aliases = task.Aliases
|
||||
t.Summary = task.Summary
|
||||
t.Sources = task.Sources
|
||||
t.Generates = task.Generates
|
||||
t.Status = task.Status
|
||||
t.Preconditions = task.Preconditions
|
||||
t.Dir = task.Dir
|
||||
t.Vars = task.Vars
|
||||
t.Env = task.Env
|
||||
t.Dotenv = task.Dotenv
|
||||
t.Silent = task.Silent
|
||||
t.Interactive = task.Interactive
|
||||
t.Internal = task.Internal
|
||||
t.Method = task.Method
|
||||
t.Prefix = task.Prefix
|
||||
t.IgnoreError = task.IgnoreError
|
||||
t.Run = task.Run
|
||||
return nil
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into task", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of Task and copies
|
||||
@@ -118,6 +146,8 @@ func (t *Task) DeepCopy() *Task {
|
||||
Status: deepCopySlice(t.Status),
|
||||
Preconditions: deepCopySlice(t.Preconditions),
|
||||
Dir: t.Dir,
|
||||
Set: deepCopySlice(t.Set),
|
||||
Shopt: deepCopySlice(t.Shopt),
|
||||
Vars: t.Vars.DeepCopy(),
|
||||
Env: t.Env.DeepCopy(),
|
||||
Dotenv: deepCopySlice(t.Dotenv),
|
||||
@@ -131,6 +161,7 @@ func (t *Task) DeepCopy() *Task {
|
||||
IncludeVars: t.IncludeVars.DeepCopy(),
|
||||
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
|
||||
IncludedTaskfile: t.IncludedTaskfile.DeepCopy(),
|
||||
Platforms: deepCopySlice(t.Platforms),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -2,76 +2,83 @@ package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
V3 = semver.MustParse("3")
|
||||
V2 = semver.MustParse("2")
|
||||
)
|
||||
|
||||
// Taskfile represents a Taskfile.yml
|
||||
type Taskfile struct {
|
||||
Version string
|
||||
Version *semver.Version
|
||||
Expansions int
|
||||
Output Output
|
||||
Method string
|
||||
Includes *IncludedTaskfiles
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval string
|
||||
Interval time.Duration
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var taskfile struct {
|
||||
Version string
|
||||
Expansions int
|
||||
Output Output
|
||||
Method string
|
||||
Includes *IncludedTaskfiles
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval string
|
||||
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.MappingNode:
|
||||
var taskfile struct {
|
||||
Version *semver.Version
|
||||
Expansions int
|
||||
Output Output
|
||||
Method string
|
||||
Includes *IncludedTaskfiles
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
}
|
||||
if err := node.Decode(&taskfile); err != nil {
|
||||
return err
|
||||
}
|
||||
tf.Version = taskfile.Version
|
||||
tf.Expansions = taskfile.Expansions
|
||||
tf.Output = taskfile.Output
|
||||
tf.Method = taskfile.Method
|
||||
tf.Includes = taskfile.Includes
|
||||
tf.Set = taskfile.Set
|
||||
tf.Shopt = taskfile.Shopt
|
||||
tf.Vars = taskfile.Vars
|
||||
tf.Env = taskfile.Env
|
||||
tf.Tasks = taskfile.Tasks
|
||||
tf.Silent = taskfile.Silent
|
||||
tf.Dotenv = taskfile.Dotenv
|
||||
tf.Run = taskfile.Run
|
||||
tf.Interval = taskfile.Interval
|
||||
if tf.Expansions <= 0 {
|
||||
tf.Expansions = 2
|
||||
}
|
||||
if tf.Vars == nil {
|
||||
tf.Vars = &Vars{}
|
||||
}
|
||||
if tf.Env == nil {
|
||||
tf.Env = &Vars{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := unmarshal(&taskfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tf.Version = taskfile.Version
|
||||
tf.Expansions = taskfile.Expansions
|
||||
tf.Output = taskfile.Output
|
||||
tf.Method = taskfile.Method
|
||||
tf.Includes = taskfile.Includes
|
||||
tf.Vars = taskfile.Vars
|
||||
tf.Env = taskfile.Env
|
||||
tf.Tasks = taskfile.Tasks
|
||||
tf.Silent = taskfile.Silent
|
||||
tf.Dotenv = taskfile.Dotenv
|
||||
tf.Run = taskfile.Run
|
||||
tf.Interval = taskfile.Interval
|
||||
|
||||
if tf.Expansions <= 0 {
|
||||
tf.Expansions = 2
|
||||
}
|
||||
if tf.Vars == nil {
|
||||
tf.Vars = &Vars{}
|
||||
}
|
||||
if tf.Env == nil {
|
||||
tf.Env = &Vars{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParsedVersion returns the version as a float64
|
||||
func (tf *Taskfile) ParsedVersion() (float64, error) {
|
||||
v, err := strconv.ParseFloat(tf.Version, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(`task: Could not parse taskfile version "%s": %v`, tf.Version, err)
|
||||
}
|
||||
return v, nil
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -13,26 +13,27 @@ type Vars struct {
|
||||
Mapping map[string]Var
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return errors.New("task: vars is not a map")
|
||||
}
|
||||
switch node.Kind {
|
||||
|
||||
// NOTE(@andreynering): on this style of custom unmarsheling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
case yaml.MappingNode:
|
||||
// NOTE(@andreynering): on this style of custom unmarshalling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
var v Var
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
var v Var
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
vs.Set(keyNode.Value, v)
|
||||
}
|
||||
vs.Set(keyNode.Value, v)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variables", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
// DeepCopy creates a new instance of Vars and copies
|
||||
@@ -116,20 +117,27 @@ type Var struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var str string
|
||||
if err := unmarshal(&str); err == nil {
|
||||
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
|
||||
case yaml.ScalarNode:
|
||||
var str string
|
||||
if err := node.Decode(&str); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Static = str
|
||||
return nil
|
||||
|
||||
case yaml.MappingNode:
|
||||
var sh struct {
|
||||
Sh string
|
||||
}
|
||||
if err := node.Decode(&sh); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Sh = sh.Sh
|
||||
return nil
|
||||
}
|
||||
|
||||
var sh struct {
|
||||
Sh string
|
||||
}
|
||||
if err := unmarshal(&sh); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Sh = sh.Sh
|
||||
return nil
|
||||
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into variable", node.Line, node.ShortTag())
|
||||
}
|
||||
|
||||
8
testdata/checksum/Taskfile.yml
vendored
8
testdata/checksum/Taskfile.yml
vendored
@@ -10,3 +10,11 @@ tasks:
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
|
||||
build-with-status:
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./source.txt
|
||||
status:
|
||||
- test -f ./generated.txt
|
||||
|
||||
17
testdata/output_group_error_only/Taskfile.yml
vendored
Normal file
17
testdata/output_group_error_only/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
output:
|
||||
group:
|
||||
error_only: true
|
||||
|
||||
tasks:
|
||||
passing: echo 'passing-output'
|
||||
|
||||
failing:
|
||||
cmds:
|
||||
- task: passing
|
||||
- echo 'passing-output-2'
|
||||
- echo 'passing-output-3'
|
||||
- echo 'failing-output' && exit 1
|
||||
55
testdata/platforms/Taskfile.yml
vendored
Normal file
55
testdata/platforms/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build-windows:
|
||||
platforms: [windows]
|
||||
cmds:
|
||||
- echo 'Running task on windows'
|
||||
|
||||
build-darwin:
|
||||
platforms: [darwin]
|
||||
cmds:
|
||||
- echo 'Running task on darwin'
|
||||
|
||||
build-linux:
|
||||
platforms: [linux]
|
||||
cmds:
|
||||
- echo 'Running task on linux'
|
||||
|
||||
build-freebsd:
|
||||
platforms: [freebsd]
|
||||
cmds:
|
||||
- echo 'Running task on freebsd'
|
||||
|
||||
build-blank-os:
|
||||
platforms: []
|
||||
cmds:
|
||||
- echo 'Running command'
|
||||
|
||||
build-multiple:
|
||||
platforms: []
|
||||
cmds:
|
||||
- cmd: echo 'Running command'
|
||||
- cmd: echo 'Running on Windows'
|
||||
platforms: [windows]
|
||||
- cmd: echo 'Running on Darwin'
|
||||
platforms: [darwin]
|
||||
|
||||
build-amd64:
|
||||
platforms: [amd64]
|
||||
cmds:
|
||||
- echo "Running command on amd64"
|
||||
|
||||
build-arm64:
|
||||
platforms: [arm64]
|
||||
cmds:
|
||||
- echo "Running command on arm64"
|
||||
|
||||
build-mixed:
|
||||
cmds:
|
||||
- cmd: echo 'building on windows/arm64'
|
||||
platforms: [windows/arm64]
|
||||
- cmd: echo 'building on linux/amd64'
|
||||
platforms: [linux/amd64]
|
||||
- cmd: echo 'building on darwin'
|
||||
platforms: [darwin]
|
||||
14
testdata/shopts/command_level/Taskfile.yml
vendored
Normal file
14
testdata/shopts/command_level/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
tasks:
|
||||
pipefail:
|
||||
cmds:
|
||||
- cmd: set -o | grep pipefail
|
||||
set: [pipefail]
|
||||
|
||||
globstar:
|
||||
cmds:
|
||||
- cmd: shopt | grep globstar
|
||||
shopt: [globstar]
|
||||
14
testdata/shopts/global_level/Taskfile.yml
vendored
Normal file
14
testdata/shopts/global_level/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
set: [pipefail]
|
||||
shopt: [globstar]
|
||||
|
||||
tasks:
|
||||
pipefail:
|
||||
cmds:
|
||||
- set -o | grep pipefail
|
||||
|
||||
globstar:
|
||||
cmds:
|
||||
- shopt | grep globstar
|
||||
14
testdata/shopts/task_level/Taskfile.yml
vendored
Normal file
14
testdata/shopts/task_level/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
tasks:
|
||||
pipefail:
|
||||
set: [pipefail]
|
||||
cmds:
|
||||
- set -o | grep pipefail
|
||||
|
||||
globstar:
|
||||
shopt: [globstar]
|
||||
cmds:
|
||||
- shopt | grep globstar
|
||||
1
testdata/special_vars/Taskfile.yml
vendored
1
testdata/special_vars/Taskfile.yml
vendored
@@ -16,3 +16,4 @@ tasks:
|
||||
- echo root/TASK={{.TASK}}
|
||||
- echo root/ROOT_DIR={{.ROOT_DIR}}
|
||||
- echo root/TASKFILE_DIR={{.TASKFILE_DIR}}
|
||||
- echo root/TASK_VERSION={{.TASK_VERSION}}
|
||||
|
||||
1
testdata/special_vars/included/Taskfile.yml
vendored
1
testdata/special_vars/included/Taskfile.yml
vendored
@@ -6,3 +6,4 @@ tasks:
|
||||
- echo included/TASK={{.TASK}}
|
||||
- echo included/ROOT_DIR={{.ROOT_DIR}}
|
||||
- echo included/TASKFILE_DIR={{.TASKFILE_DIR}}
|
||||
- echo included/TASK_VERSION={{.TASK_VERSION}}
|
||||
|
||||
26
variables.go
26
variables.go
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/status"
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
@@ -40,12 +40,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := templater.Templater{Vars: vars, RemoveNoValue: v >= 3.0}
|
||||
r := templater.Templater{Vars: vars, RemoveNoValue: e.Taskfile.Version.Compare(taskfile.V3) >= 0}
|
||||
|
||||
new := taskfile.Task{
|
||||
Task: origTask.Task,
|
||||
@@ -56,6 +51,8 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
||||
Sources: r.ReplaceSlice(origTask.Sources),
|
||||
Generates: r.ReplaceSlice(origTask.Generates),
|
||||
Dir: r.Replace(origTask.Dir),
|
||||
Set: origTask.Set,
|
||||
Shopt: origTask.Shopt,
|
||||
Vars: nil,
|
||||
Env: nil,
|
||||
Dotenv: r.ReplaceSlice(origTask.Dotenv),
|
||||
@@ -68,6 +65,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
||||
Run: r.Replace(origTask.Run),
|
||||
IncludeVars: origTask.IncludeVars,
|
||||
IncludedTaskfileVars: origTask.IncludedTaskfileVars,
|
||||
Platforms: origTask.Platforms,
|
||||
}
|
||||
new.Dir, err = execext.Expand(new.Dir)
|
||||
if err != nil {
|
||||
@@ -124,12 +122,15 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
||||
continue
|
||||
}
|
||||
new.Cmds = append(new.Cmds, &taskfile.Cmd{
|
||||
Task: r.Replace(cmd.Task),
|
||||
Silent: cmd.Silent,
|
||||
Cmd: r.Replace(cmd.Cmd),
|
||||
Silent: cmd.Silent,
|
||||
Task: r.Replace(cmd.Task),
|
||||
Set: cmd.Set,
|
||||
Shopt: cmd.Shopt,
|
||||
Vars: r.ReplaceVars(cmd.Vars),
|
||||
IgnoreError: cmd.IgnoreError,
|
||||
Defer: cmd.Defer,
|
||||
Platforms: cmd.Platforms,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -160,8 +161,11 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
||||
}
|
||||
|
||||
if len(origTask.Status) > 0 {
|
||||
for _, checker := range []status.Checker{e.timestampChecker(&new), e.checksumChecker(&new)} {
|
||||
value, err := checker.Value()
|
||||
timestampChecker := fingerprint.NewTimestampChecker(e.TempDir, e.Dry)
|
||||
checksumChecker := fingerprint.NewChecksumChecker(e.TempDir, e.Dry)
|
||||
|
||||
for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
|
||||
value, err := checker.Value(&new)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
42
watch.go
42
watch.go
@@ -10,10 +10,11 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/status"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/radovskyb/watcher"
|
||||
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
const defaultWatchInterval = 5 * time.Second
|
||||
@@ -37,23 +38,14 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error {
|
||||
}()
|
||||
}
|
||||
|
||||
var watchIntervalString string
|
||||
|
||||
if e.Interval != "" {
|
||||
watchIntervalString = e.Interval
|
||||
} else if e.Taskfile.Interval != "" {
|
||||
watchIntervalString = e.Taskfile.Interval
|
||||
}
|
||||
|
||||
watchInterval := defaultWatchInterval
|
||||
|
||||
if watchIntervalString != "" {
|
||||
var err error
|
||||
watchInterval, err = parseWatchInterval(watchIntervalString)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
var watchInterval time.Duration
|
||||
switch {
|
||||
case e.Interval != 0:
|
||||
watchInterval = e.Interval
|
||||
case e.Taskfile.Interval != 0:
|
||||
watchInterval = e.Taskfile.Interval
|
||||
default:
|
||||
watchInterval = defaultWatchInterval
|
||||
}
|
||||
|
||||
e.Logger.VerboseOutf(logger.Green, "task: Watching for changes every %v", watchInterval)
|
||||
@@ -150,7 +142,7 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca
|
||||
}
|
||||
|
||||
for _, s := range task.Sources {
|
||||
files, err := status.Glob(task.Dir, s)
|
||||
files, err := fingerprint.Glob(task.Dir, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("task: %s: %w", s, err)
|
||||
}
|
||||
@@ -185,11 +177,3 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Ca
|
||||
func shouldIgnoreFile(path string) bool {
|
||||
return strings.Contains(path, "/.git") || strings.Contains(path, "/.task") || strings.Contains(path, "/node_modules")
|
||||
}
|
||||
|
||||
func parseWatchInterval(watchInterval string) (time.Duration, error) {
|
||||
v, err := time.ParseDuration(watchInterval)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(`task: Could not parse watch interval "%s": %v`, watchInterval, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
80
watch_test.go
Normal file
80
watch_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
//go:build watch
|
||||
// +build watch
|
||||
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
func TestFileWatcherInterval(t *testing.T) {
|
||||
const dir = "testdata/watcher_interval"
|
||||
expectedOutput := strings.TrimSpace(`
|
||||
task: Started watching for tasks: default
|
||||
task: [default] echo "Hello, World!"
|
||||
Hello, World!
|
||||
task: [default] echo "Hello, World!"
|
||||
Hello, World!
|
||||
`)
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Watch: true,
|
||||
}
|
||||
|
||||
assert.NoError(t, e.Setup())
|
||||
buff.Reset()
|
||||
|
||||
err := os.MkdirAll(filepathext.SmartJoin(dir, "src"), 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
err := e.Run(ctx, taskfile.Call{Task: "default"})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
err = os.WriteFile(filepathext.SmartJoin(dir, "src/a"), []byte("test updated"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(700 * time.Millisecond)
|
||||
cancel()
|
||||
assert.Equal(t, expectedOutput, strings.TrimSpace(buff.String()))
|
||||
buff.Reset()
|
||||
err = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(filepathext.SmartJoin(dir, "src"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user