Compare commits

..

49 Commits

Author SHA1 Message Date
Andrey Nering
3eb4c9eae8 v3.22.0 2023-03-10 15:35:56 -03:00
Pete Davison
0838d48ee3 refactor: decouple fingerprinting from executor (#1039) 2023-03-10 15:27:30 -03:00
Pete Davison
c64f8818be Merge pull request #1043 from go-task/update-install-from-source-docs
chore: remove installation docs for Go 1.15
2023-03-09 19:16:57 +00:00
Pete Davison
97ffd84d0e chore: remove installation docs for Go 1.15 2023-03-09 19:07:05 +00:00
Andrey Nering
f2114f09f7 Fix capitalization of flags descriptions on task -h
Also, adds missing periods.
2023-03-08 23:24:39 -03:00
Andrey Nering
9c844850e4 Add --global (-g) flag (#1029)
This will run a Taskfile from the home directory, i.e., `$HOME/Taskfile.yml`.
2023-03-08 23:21:23 -03:00
Andrey Nering
68aef2ef0d Add CHANGELOG entry for #1022 2023-03-08 22:37:04 -03:00
Dennis Jekubczyk
88d644a7e9 Add ability to set error_only: true on the group output mode 2023-03-08 22:34:52 -03:00
dependabot[bot]
4b97d4f7f5 build(deps): bump dns-packet from 5.3.1 to 5.4.0 in /docs (#1034)
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v5.3.1...5.4.0)

---
updated-dependencies:
- dependency-name: dns-packet
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-08 22:23:31 -03:00
Andrey Nering
bc14c633ae Taskfile: Remove task commited by mistake 2023-03-08 22:21:27 -03:00
Pete Davison
a29e5d39ca Merge pull request #1037 from go-task/fix-status-and-sources
fix: status and sources
2023-03-07 00:35:45 +00:00
Pete Davison
f1506ee500 fix: status and sources 2023-03-07 00:30:24 +00:00
Andrey Nering
6e346de9fb CHANGELOG: Add entry for #1035 2023-03-06 09:47:33 -03:00
Harel Wahnich
99ab2a4d62 for task up to date check both status and sources (#1035)
* remove redundant if statement

* add subtests to TestStatusChecksum
2023-03-05 22:16:41 -08:00
Pete Davison
d4ed7c3cfc Merge pull request #1004 from go-task/semver
feat: use semver package for taskfile schema version
2023-03-02 19:07:52 +00:00
pzloty
bc0554575a Fix output "prefixed" option in schema.json (#1031)
Fix the output option to match implementation and documentation.
2023-03-02 10:42:11 -03:00
Andrey Nering
1f4906244b Add CHANGELOG for #1025 2023-03-01 22:06:16 -03:00
Bevan Arps
52756ab83e Fix deadlock issue with run: once (#1025) 2023-03-01 21:53:38 -03:00
Pete Davison
97dcbe6932 Merge pull request #1026 from go-task/fix-schema-for-group
fix: schema for output group
2023-02-28 13:49:03 +00:00
Pete Davison
e35bf22dd3 fix: schema for output group (#1005) 2023-02-28 11:50:26 +01:00
Andrey Nering
a36b1b9cec Website: Remove Carbon 2023-02-23 19:30:10 -03:00
Andrey Nering
1920ee38c3 v3.21.0 2023-02-22 22:12:48 -03:00
João Pedro
ec2110e58f Add new TASK_VERSION special variable
Closes #1014
Closes #990
2023-02-22 22:08:38 -03:00
Andrey Nering
12a1cd6f62 Docs: Update API page 2023-02-16 21:16:43 -03:00
Aleksandr Komlev
9af056e746 Add FORCE_COLOR env support (#1003) 2023-02-16 21:12:44 -03:00
Pete Davison
c8fe450623 Merge pull request #1010 from go-task/go-1-20
chore: update to go 1.20
2023-02-13 13:33:51 +00:00
Pete Davison
ab1fe742f3 chore: update to go 1.20 2023-02-13 13:28:49 +00:00
Pete Davison
8b72c86ba5 feat: use semver package for taskfile schema version 2023-02-10 18:14:38 +00:00
Pete Davison
28c5f4a635 Merge pull request #1007 from go-task/fix-tasks-being-incorrectly-marked-as-internal
fix: tasks being incorrectly marked as internal
2023-02-10 18:14:01 +00:00
Pete Davison
74f69a21cd fix: tasks being incorrectly marked as internal 2023-02-10 17:02:11 +00:00
dependabot[bot]
e23dacd6d4 build(deps): bump github.com/joho/godotenv from 1.4.0 to 1.5.1 (#1002)
Bumps [github.com/joho/godotenv](https://github.com/joho/godotenv) from 1.4.0 to 1.5.1.
- [Release notes](https://github.com/joho/godotenv/releases)
- [Commits](https://github.com/joho/godotenv/compare/v1.4.0...v1.5.1)

---
updated-dependencies:
- dependency-name: github.com/joho/godotenv
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-06 09:41:37 -03:00
Andrey Nering
58d582941b Documentation: Remove mentions of Minify in favor of esbuild 2023-02-04 20:29:10 -03:00
dependabot[bot]
3bbc51949c build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 in /docs (#1000)
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-04 18:51:05 -03:00
Andrey Nering
69e0254a99 Website: Update 2023-01-29 15:42:59 -03:00
Andrey Nering
1091a914bd Documentation: version: '2' -> version: '3'
https://github.com/go-task/task/pull/929#discussion_r1082370557
2023-01-29 15:39:45 -03:00
dependabot[bot]
ecc65a218e build(deps): bump github.com/fatih/color from 1.13.0 to 1.14.1 (#995)
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.13.0 to 1.14.1.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.13.0...v1.14.1)

---
updated-dependencies:
- dependency-name: github.com/fatih/color
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-28 08:07:12 -03:00
dependabot[bot]
426ed7eff6 build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /docs (#991)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-27 09:55:18 -03:00
Andrey Nering
73aba36309 v3.20.0 2023-01-14 17:34:15 -03:00
Andrey Nering
cb393ccd3a Add CHANGELOG entry + small adjustments to #977 2023-01-14 17:18:26 -03:00
Amin Yahyaabadi
347fcf9f67 fix: avoid reruns when the timestamp method is used (#977) 2023-01-14 17:17:36 -03:00
Andrey Nering
fce7575b03 Add README entry for #982 2023-01-14 16:48:04 -03:00
Pete Davison
2da7ddc399 chore: optimize task filtering (#982) 2023-01-14 16:45:52 -03:00
Pete Davison
1c1be683ab feat: set and shopt directives (#929)
Co-authored-by: Andrey Nering <andrey@nering.com.br>
2023-01-14 16:41:56 -03:00
Andrey Nering
4be1050234 Optimize the Taskfile a bit
`go list ./...` takes quite a few seconds to run. Let's restrict it to the
tasks that actually use it.
2023-01-06 21:41:18 -03:00
Andrey Nering
2efb3533ec Add CHANGELOG + improvements to #980
Closes #978
2023-01-06 21:39:57 -03:00
Lea Anthony
aa6c7e4b94 Add support for 'platforms' in both task and command (#980) 2023-01-06 21:38:35 -03:00
Andrey Nering
63c50d13ee Website/README: Add link to the Mastodon account 2023-01-01 21:24:16 -03:00
Andrey Nering
c1e127e42f Website: Update outdated URL 2022-12-31 14:28:37 -03:00
dependabot[bot]
9e38e8a4db build(deps): bump json5 from 2.2.1 to 2.2.2 in /docs (#972)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-31 14:15:20 -03:00
79 changed files with 2219 additions and 822 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,47 @@
# 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

View File

@@ -10,7 +10,7 @@
</p>
<p>
<a href="https://taskfile.dev/installation/">Installation</a> | <a href="https://taskfile.dev/usage/">Documentation</a> | <a href="https://twitter.com/taskfiledev">Twitter</a> | <a href="https://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>

View File

@@ -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
@@ -73,12 +88,18 @@ tasks:
deps: [install]
cmds:
- go test {{catLines .GO_PACKAGES}}
vars:
GO_PACKAGES:
sh: go list ./...
test:all:
desc: Runs test suite with signals and watch tests included
deps: [install, sleepit:build]
cmds:
- go test {{catLines .GO_PACKAGES}} -tags 'signals watch'
vars:
GO_PACKAGES:
sh: go list ./...
test-release:
desc: Tests release process without publishing
@@ -106,4 +127,7 @@ tasks:
packages:
cmds:
- echo '{{.GO_PACKAGES}}'
vars:
GO_PACKAGES:
sh: go list ./...
silent: true

View File

@@ -6,7 +6,6 @@ import (
"log"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"
@@ -16,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
@@ -76,35 +72,38 @@ func main() {
output taskfile.Output
color bool
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.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(&parallel, "parallel", "p", false, "executes tasks provided on command line in parallel")
pflag.BoolVarP(&dry, "dry", "n", false, "compiles and prints tasks in the order that they would be run, without executing them")
pflag.BoolVar(&summary, "summary", false, "show summary about a task")
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.DurationVarP(&interval, "interval", "I", 0, "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(&parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
pflag.BoolVarP(&dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
pflag.BoolVar(&summary, "summary", false, "Show summary about a task.")
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
}
@@ -124,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
@@ -142,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{
@@ -178,11 +194,6 @@ func main() {
if err := e.Setup(); err != nil {
log.Fatal(err)
}
v, err := e.Taskfile.ParsedVersion()
if err != nil {
log.Fatal(err)
return
}
if listOptions.ShouldListTasks() {
if foundTasks, err := e.ListTasks(listOptions); !foundTasks || err != nil {
@@ -201,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...)
@@ -255,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
}

View File

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

View File

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

View File

@@ -5,6 +5,48 @@ 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

View File

@@ -11,7 +11,7 @@ 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 $100/month will be featured as a "Gold Sponsor"
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.

View File

@@ -170,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
@@ -184,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)

View File

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

View File

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

View File

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

View File

@@ -13,10 +13,6 @@ const sidebars = {
type: 'link',
label: 'Chinese | 中国人',
href: CHINESE_URL
},
{
type: 'html',
value: '<div id="sidebar-ads"></div>'
}
]
};

View File

@@ -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%);
}

View File

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

View File

@@ -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);
})();

View File

@@ -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.",

View File

@@ -4473,9 +4473,9 @@ dns-equal@^1.0.0:
integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==
dns-packet@^5.2.2:
version "5.3.1"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.3.1.tgz#eb94413789daec0f0ebe2fcc230bdc9d7c91b43d"
integrity sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==
version "5.4.0"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
@@ -5414,9 +5414,9 @@ htmlparser2@^8.0.1:
entities "^4.3.0"
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
http-deceiver@^1.2.7:
version "1.2.7"
@@ -5877,9 +5877,9 @@ json-schema-traverse@^1.0.0:
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json5@^2.1.2, json5@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
version "2.2.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.2.tgz#64471c5bdcc564c18f7c1d4df2e2297f2457c5ab"
integrity sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==
jsonfile@^6.0.1:
version "6.1.0"
@@ -8212,9 +8212,9 @@ typedarray-to-buffer@^3.1.5:
is-typedarray "^1.0.0"
ua-parser-js@^0.7.30:
version "0.7.31"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
unherit@^1.0.4:
version "1.1.3"

16
go.mod
View File

@@ -1,16 +1,20 @@
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/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
@@ -18,12 +22,10 @@ require (
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.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
go 1.18

53
go.sum
View File

@@ -1,22 +1,26 @@
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/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/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.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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=
@@ -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=
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/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-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-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=

22
help.go
View File

@@ -12,6 +12,7 @@ import (
"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"
)
@@ -51,10 +52,10 @@ func (o ListOptions) Validate() error {
// 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()}
filters := []FilterFunc{FilterOutInternal}
if o.ListOnlyTasksWithDescriptions {
filters = append(filters, FilterOutNoDesc())
filters = append(filters, FilterOutNoDesc)
}
return filters
@@ -65,7 +66,10 @@ func (o ListOptions) Filters() []FilterFunc {
// 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 := e.GetTaskList(o.Filters()...)
tasks, err := e.GetTaskList(o.Filters()...)
if err != nil {
return false, err
}
if o.FormatTaskListAsJSON {
output, err := e.ToEditorOutput(tasks)
if err != nil {
@@ -145,7 +149,17 @@ func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Output, erro
Tasks: make([]editors.Task, len(tasks)),
}
for i, t := range tasks {
upToDate, err := e.isTaskUpToDate(context.Background(), t)
// 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
}

View File

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

31
internal/env/env.go vendored Normal file
View 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
}

View File

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

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

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

View File

@@ -1,4 +1,4 @@
package status
package fingerprint
import (
"os"

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

View 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, "-")
}

View File

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

View 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"
}

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

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

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

View 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
View 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": {},
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package output_test
import (
"bytes"
"errors"
"fmt"
"io"
"testing"
@@ -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())
})
}

View File

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

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

View File

@@ -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, "-")
}

View File

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

View File

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

View File

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

View 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
View File

@@ -1,12 +1,12 @@
{
"name": "@go-task/cli",
"version": "3.19.1",
"version": "3.22.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@go-task/cli",
"version": "3.19.1",
"version": "3.22.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@go-task/cli",
"version": "3.19.1",
"version": "3.22.0",
"description": "A task runner / simpler Make alternative written in Go",
"scripts": {
"postinstall": "go-npm install",

View File

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

View File

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

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

163
task.go
View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"runtime"
"sort"
"strings"
"sync"
@@ -12,9 +13,12 @@ import (
"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"
@@ -135,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
@@ -150,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
}
@@ -252,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)
}
@@ -270,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
@@ -294,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 {
@@ -328,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
}
@@ -386,23 +392,39 @@ 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.
@@ -420,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
}

View File

@@ -11,6 +11,7 @@ import (
"strings"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
"github.com/go-task/task/v3"
@@ -190,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) {
@@ -444,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) {
@@ -726,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 {
@@ -1566,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"
@@ -1696,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())
}

View File

@@ -11,9 +11,12 @@ 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
@@ -39,12 +42,18 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
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
}

View File

@@ -61,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 {

View File

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

View File

@@ -53,6 +53,7 @@ func (s *Output) UnmarshalYAML(node *yaml.Node) error {
// 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
View 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)
}

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

View File

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

View File

@@ -23,6 +23,8 @@ type Task struct {
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
@@ -36,6 +38,7 @@ type Task struct {
IncludeVars *Vars
IncludedTaskfileVars *Vars
IncludedTaskfile *IncludedTaskfile
Platforms []*Platform
}
func (t *Task) Name() string {
@@ -80,6 +83,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Status []string
Preconditions []*Precondition
Dir string
Set []string
Shopt []string
Vars *Vars
Env *Vars
Dotenv []string
@@ -90,6 +95,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Run string
Platforms []*Platform
}
if err := node.Decode(&task); err != nil {
return err
@@ -105,6 +111,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
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
@@ -115,6 +123,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
t.Prefix = task.Prefix
t.IgnoreError = task.IgnoreError
t.Run = task.Run
t.Platforms = task.Platforms
return nil
}
@@ -137,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),
@@ -150,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
}

View File

@@ -2,19 +2,26 @@ 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
@@ -29,11 +36,13 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
case yaml.MappingNode:
var 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
@@ -50,6 +59,8 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
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
@@ -71,12 +82,3 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
return fmt.Errorf("yaml: line %d: cannot unmarshal %s into taskfile", node.Line, node.ShortTag())
}
// 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
}

View File

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

View 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
View 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]

View 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]

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

View File

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

View File

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

View File

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

View File

@@ -12,8 +12,8 @@ import (
"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/internal/status"
"github.com/go-task/task/v3/taskfile"
)
@@ -142,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)
}