mirror of
https://github.com/go-task/task.git
synced 2026-05-18 21:26:37 +02:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3eb4c9eae8 | ||
|
|
0838d48ee3 | ||
|
|
c64f8818be | ||
|
|
97ffd84d0e | ||
|
|
f2114f09f7 | ||
|
|
9c844850e4 | ||
|
|
68aef2ef0d | ||
|
|
88d644a7e9 | ||
|
|
4b97d4f7f5 | ||
|
|
bc14c633ae | ||
|
|
a29e5d39ca | ||
|
|
f1506ee500 | ||
|
|
6e346de9fb | ||
|
|
99ab2a4d62 | ||
|
|
d4ed7c3cfc | ||
|
|
bc0554575a | ||
|
|
1f4906244b | ||
|
|
52756ab83e | ||
|
|
97dcbe6932 | ||
|
|
e35bf22dd3 | ||
|
|
a36b1b9cec | ||
|
|
8b72c86ba5 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
||||
# 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
|
||||
|
||||
22
Taskfile.yml
22
Taskfile.yml
@@ -27,6 +27,24 @@ tasks:
|
||||
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
|
||||
cmds:
|
||||
@@ -113,7 +131,3 @@ tasks:
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
silent: true
|
||||
|
||||
foo:
|
||||
cmds:
|
||||
- echo "{{.TASK_VERSION}}"
|
||||
|
||||
@@ -72,31 +72,34 @@ 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(¶llel, "parallel", "p", false, "executes tasks provided on command line in parallel")
|
||||
pflag.BoolVarP(&dry, "dry", "n", false, "compiles and prints tasks in the order that they would be run, without executing them")
|
||||
pflag.BoolVar(&summary, "summary", false, "show summary about a task")
|
||||
pflag.BoolVarP(&exitCode, "exit-code", "x", false, "pass-through the exit code of the task command")
|
||||
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
|
||||
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
|
||||
pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
|
||||
pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "message template to print before a task's grouped output")
|
||||
pflag.StringVar(&output.Group.End, "output-group-end", "", "message template to print after a task's grouped output")
|
||||
pflag.BoolVarP(&color, "color", "c", true, "colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable")
|
||||
pflag.IntVarP(&concurrency, "concurrency", "C", 0, "limit number tasks to run concurrently")
|
||||
pflag.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(¶llel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
|
||||
pflag.BoolVarP(&dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
|
||||
pflag.BoolVar(&summary, "summary", false, "Show summary about a task.")
|
||||
pflag.BoolVarP(&exitCode, "exit-code", "x", false, "Pass-through the exit code of the task command.")
|
||||
pflag.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
|
||||
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
||||
pflag.StringVarP(&output.Name, "output", "o", "", "Sets output style: [interleaved|group|prefixed].")
|
||||
pflag.StringVar(&output.Group.Begin, "output-group-begin", "", "Message template to print before a task's grouped output.")
|
||||
pflag.StringVar(&output.Group.End, "output-group-end", "", "Message template to print after a task's grouped output.")
|
||||
pflag.BoolVar(&output.Group.ErrorOnly, "output-group-error-only", false, "Swallow output from successful tasks.")
|
||||
pflag.BoolVarP(&color, "color", "c", true, "Colored output. Enabled by default. Set flag to false or use NO_COLOR=1 to disable.")
|
||||
pflag.IntVarP(&concurrency, "concurrency", "C", 0, "Limit number tasks to run concurrently.")
|
||||
pflag.DurationVarP(&interval, "interval", "I", 0, "Interval to watch for changes.")
|
||||
pflag.BoolVarP(&global, "global", "g", false, "Runs global Taskfile, from $HOME/Taskfile.{yml,yaml}.")
|
||||
pflag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
@@ -120,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
|
||||
@@ -138,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{
|
||||
@@ -174,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 {
|
||||
@@ -197,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...)
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -5,6 +5,23 @@ 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -1342,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:
|
||||
|
||||
@@ -41,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',
|
||||
@@ -187,14 +184,7 @@ const config = {
|
||||
apiKey: '34b64ae4fc8d9da43d9a13d9710aaddc',
|
||||
indexName: 'taskfile'
|
||||
}
|
||||
}),
|
||||
|
||||
scripts: [
|
||||
{
|
||||
src: '/js/carbon.js',
|
||||
async: true
|
||||
}
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -13,10 +13,6 @@ const sidebars = {
|
||||
type: 'link',
|
||||
label: 'Chinese | 中国人',
|
||||
href: CHINESE_URL
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
value: '<div id="sidebar-ads"></div>'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
#carbonads * {
|
||||
margin: initial;
|
||||
padding: initial;
|
||||
}
|
||||
#carbonads {
|
||||
display: flex;
|
||||
max-width: 330px;
|
||||
background-color: hsl(0, 0%, 98%);
|
||||
box-shadow: 0 1px 4px 1px hsla(0, 0%, 0%, 0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
#carbonads a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
#carbonads a:hover {
|
||||
color: inherit;
|
||||
}
|
||||
#carbonads span {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
#carbonads .carbon-wrap {
|
||||
display: flex;
|
||||
}
|
||||
#carbonads .carbon-img {
|
||||
display: block;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
#carbonads .carbon-img img {
|
||||
display: block;
|
||||
}
|
||||
#carbonads .carbon-text {
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
}
|
||||
#carbonads .carbon-poweredby {
|
||||
display: block;
|
||||
padding: 6px 8px;
|
||||
background: #f1f1f2;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
border-top-left-radius: 3px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[data-theme='dark'] #carbonads {
|
||||
background-color: hsl(0, 0%, 35%);
|
||||
box-shadow: 0 1px 4px 1px hsl(0, 0%, 55%);
|
||||
}
|
||||
|
||||
[data-theme='dark'] #carbonads .carbon-poweredby {
|
||||
background-color: hsl(0, 0%, 55%);
|
||||
}
|
||||
@@ -23,11 +23,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#carbonads {
|
||||
margin-top: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.gold-sponsors {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
0
docs/static/js/.keep
vendored
Normal file
0
docs/static/js/.keep
vendored
Normal file
29
docs/static/js/carbon.js
vendored
29
docs/static/js/carbon.js
vendored
@@ -1,29 +0,0 @@
|
||||
(function () {
|
||||
function attachAd() {
|
||||
var el = document.createElement('script');
|
||||
el.setAttribute('type', 'text/javascript');
|
||||
el.setAttribute('id', '_carbonads_js');
|
||||
el.setAttribute(
|
||||
'src',
|
||||
'//cdn.carbonads.com/carbon.js?serve=CESI65QJ&placement=taskfiledev'
|
||||
);
|
||||
el.setAttribute('async', 'async');
|
||||
|
||||
var wrapper = document.getElementById('sidebar-ads');
|
||||
wrapper.innerHTML = '';
|
||||
wrapper.appendChild(el);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
attachAd();
|
||||
|
||||
var currentPath = window.location.pathname;
|
||||
|
||||
setInterval(function () {
|
||||
if (currentPath !== window.location.pathname) {
|
||||
currentPath = window.location.pathname;
|
||||
attachAd();
|
||||
}
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
})();
|
||||
33
docs/static/schema.json
vendored
33
docs/static/schema.json
vendored
@@ -314,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -336,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)",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
6
go.mod
6
go.mod
@@ -1,8 +1,12 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
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/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
|
||||
@@ -25,5 +29,3 @@ require (
|
||||
golang.org/x/term v0.3.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
||||
go 1.19
|
||||
|
||||
29
go.sum
29
go.sum
@@ -1,3 +1,5 @@
|
||||
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=
|
||||
@@ -7,6 +9,8 @@ github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8Wlg
|
||||
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.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
@@ -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-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=
|
||||
|
||||
13
help.go
13
help.go
@@ -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"
|
||||
)
|
||||
@@ -148,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
|
||||
}
|
||||
|
||||
31
internal/env/env.go
vendored
Normal file
31
internal/env/env.go
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
func Get(t *taskfile.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
environ := os.Environ()
|
||||
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
str, isString := v.(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, alreadySet := os.LookupEnv(k); alreadySet {
|
||||
continue
|
||||
}
|
||||
|
||||
environ = append(environ, fmt.Sprintf("%s=%s", k, str))
|
||||
}
|
||||
|
||||
return environ
|
||||
}
|
||||
20
internal/fingerprint/checker.go
Normal file
20
internal/fingerprint/checker.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// StatusCheckable defines any type that can check if the status of a task is up-to-date.
|
||||
type StatusCheckable interface {
|
||||
IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error)
|
||||
}
|
||||
|
||||
// SourcesCheckable defines any type that can check if the sources of a task are up-to-date.
|
||||
type SourcesCheckable interface {
|
||||
IsUpToDate(t *taskfile.Task) (bool, error)
|
||||
Value(t *taskfile.Task) (interface{}, error)
|
||||
OnError(t *taskfile.Task) error
|
||||
Kind() string
|
||||
}
|
||||
132
internal/fingerprint/checker_mock.go
Normal file
132
internal/fingerprint/checker_mock.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: checker.go
|
||||
|
||||
// Package fingerprint is a generated GoMock package.
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
taskfile "github.com/go-task/task/v3/taskfile"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockStatusCheckable is a mock of StatusCheckable interface.
|
||||
type MockStatusCheckable struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockStatusCheckableMockRecorder
|
||||
}
|
||||
|
||||
// MockStatusCheckableMockRecorder is the mock recorder for MockStatusCheckable.
|
||||
type MockStatusCheckableMockRecorder struct {
|
||||
mock *MockStatusCheckable
|
||||
}
|
||||
|
||||
// NewMockStatusCheckable creates a new mock instance.
|
||||
func NewMockStatusCheckable(ctrl *gomock.Controller) *MockStatusCheckable {
|
||||
mock := &MockStatusCheckable{ctrl: ctrl}
|
||||
mock.recorder = &MockStatusCheckableMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockStatusCheckable) EXPECT() *MockStatusCheckableMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// IsUpToDate mocks base method.
|
||||
func (m *MockStatusCheckable) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsUpToDate", ctx, t)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsUpToDate indicates an expected call of IsUpToDate.
|
||||
func (mr *MockStatusCheckableMockRecorder) IsUpToDate(ctx, t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockStatusCheckable)(nil).IsUpToDate), ctx, t)
|
||||
}
|
||||
|
||||
// MockSourcesCheckable is a mock of SourcesCheckable interface.
|
||||
type MockSourcesCheckable struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSourcesCheckableMockRecorder
|
||||
}
|
||||
|
||||
// MockSourcesCheckableMockRecorder is the mock recorder for MockSourcesCheckable.
|
||||
type MockSourcesCheckableMockRecorder struct {
|
||||
mock *MockSourcesCheckable
|
||||
}
|
||||
|
||||
// NewMockSourcesCheckable creates a new mock instance.
|
||||
func NewMockSourcesCheckable(ctrl *gomock.Controller) *MockSourcesCheckable {
|
||||
mock := &MockSourcesCheckable{ctrl: ctrl}
|
||||
mock.recorder = &MockSourcesCheckableMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSourcesCheckable) EXPECT() *MockSourcesCheckableMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// IsUpToDate mocks base method.
|
||||
func (m *MockSourcesCheckable) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsUpToDate", t)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsUpToDate indicates an expected call of IsUpToDate.
|
||||
func (mr *MockSourcesCheckableMockRecorder) IsUpToDate(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUpToDate", reflect.TypeOf((*MockSourcesCheckable)(nil).IsUpToDate), t)
|
||||
}
|
||||
|
||||
// Kind mocks base method.
|
||||
func (m *MockSourcesCheckable) Kind() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Kind")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Kind indicates an expected call of Kind.
|
||||
func (mr *MockSourcesCheckableMockRecorder) Kind() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kind", reflect.TypeOf((*MockSourcesCheckable)(nil).Kind))
|
||||
}
|
||||
|
||||
// OnError mocks base method.
|
||||
func (m *MockSourcesCheckable) OnError(t *taskfile.Task) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OnError", t)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// OnError indicates an expected call of OnError.
|
||||
func (mr *MockSourcesCheckableMockRecorder) OnError(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnError", reflect.TypeOf((*MockSourcesCheckable)(nil).OnError), t)
|
||||
}
|
||||
|
||||
// Value mocks base method.
|
||||
func (m *MockSourcesCheckable) Value(t *taskfile.Task) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Value", t)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Value indicates an expected call of Value.
|
||||
func (mr *MockSourcesCheckableMockRecorder) Value(t interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*MockSourcesCheckable)(nil).Value), t)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
16
internal/fingerprint/sources.go
Normal file
16
internal/fingerprint/sources.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package fingerprint
|
||||
|
||||
import "fmt"
|
||||
|
||||
func NewSourcesChecker(method, tempDir string, dry bool) (SourcesCheckable, error) {
|
||||
switch method {
|
||||
case "timestamp":
|
||||
return NewTimestampChecker(tempDir, dry), nil
|
||||
case "checksum":
|
||||
return NewChecksumChecker(tempDir, dry), nil
|
||||
case "none":
|
||||
return NoneChecker{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: invalid method "%s"`, method)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
@@ -10,51 +10,54 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// Checksum validades if a task is up to date by calculating its source
|
||||
// ChecksumChecker validates 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
|
||||
type ChecksumChecker struct {
|
||||
tempDir string
|
||||
dry bool
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (c *Checksum) IsUpToDate() (bool, error) {
|
||||
if len(c.Sources) == 0 {
|
||||
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 := c.checksumFilePath()
|
||||
checksumFile := checker.checksumFilePath(t)
|
||||
|
||||
data, _ := os.ReadFile(checksumFile)
|
||||
oldMd5 := strings.TrimSpace(string(data))
|
||||
|
||||
sources, err := globs(c.TaskDir, c.Sources)
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newMd5, err := c.checksum(sources...)
|
||||
newMd5, err := checker.checksum(sources...)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !c.Dry {
|
||||
_ = os.MkdirAll(filepathext.SmartJoin(c.TempDir, "checksum"), 0o755)
|
||||
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(c.Generates) > 0 {
|
||||
if len(t.Generates) > 0 {
|
||||
// For each specified 'generates' field, check whether the files actually exist
|
||||
for _, g := range c.Generates {
|
||||
generates, err := Glob(c.TaskDir, g)
|
||||
for _, g := range t.Generates {
|
||||
generates, err := Glob(t.Dir, g)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -70,7 +73,22 @@ func (c *Checksum) IsUpToDate() (bool, error) {
|
||||
return oldMd5 == newMd5, nil
|
||||
}
|
||||
|
||||
func (c *Checksum) checksum(files ...string) (string, error) {
|
||||
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 {
|
||||
@@ -91,31 +109,13 @@ func (c *Checksum) checksum(files ...string) (string, error) {
|
||||
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", normalizeFilename(c.Task))
|
||||
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 caracters on filenames with "-"
|
||||
// replaces invalid characters on filenames with "-"
|
||||
func normalizeFilename(f string) string {
|
||||
return checksumFilenameRegexp.ReplaceAllString(f, "-")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
23
internal/fingerprint/sources_none.go
Normal file
23
internal/fingerprint/sources_none.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package fingerprint
|
||||
|
||||
import "github.com/go-task/task/v3/taskfile"
|
||||
|
||||
// NoneChecker is a no-op Checker.
|
||||
// It will always report that the task is not up-to-date.
|
||||
type NoneChecker struct{}
|
||||
|
||||
func (NoneChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (NoneChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (NoneChecker) OnError(t *taskfile.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NoneChecker) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
package status
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// Timestamp checks if any source change compared with the generated files,
|
||||
// TimestampChecker checks if any source change compared with the generated files,
|
||||
// using file modifications timestamps.
|
||||
type Timestamp struct {
|
||||
TempDir string
|
||||
Task string
|
||||
Dir string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Dry bool
|
||||
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 (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
func (checker *TimestampChecker) IsUpToDate(t *taskfile.Task) (bool, error) {
|
||||
if len(t.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
@@ -32,7 +37,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
timestampFile := t.timestampFilePath()
|
||||
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.
|
||||
@@ -41,7 +46,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
generates = append(generates, timestampFile)
|
||||
} else {
|
||||
// Create the timestamp file for the next execution when the file does not exist.
|
||||
if !t.Dry {
|
||||
if !checker.dry {
|
||||
if err := os.MkdirAll(filepath.Dir(timestampFile), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -70,7 +75,7 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
}
|
||||
|
||||
// Modify the metadata of the file to the the current time.
|
||||
if !t.Dry {
|
||||
if !checker.dry {
|
||||
if err := os.Chtimes(timestampFile, taskTime, taskTime); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -79,12 +84,12 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
return !shouldUpdate, nil
|
||||
}
|
||||
|
||||
func (t *Timestamp) Kind() string {
|
||||
func (checker *TimestampChecker) Kind() string {
|
||||
return "timestamp"
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (t *Timestamp) Value() (interface{}, error) {
|
||||
func (checker *TimestampChecker) Value(t *taskfile.Task) (interface{}, error) {
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
@@ -137,10 +142,10 @@ func anyFileNewerThan(files []string, givenTime time.Time) (bool, error) {
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (*Timestamp) OnError() error {
|
||||
func (*TimestampChecker) OnError(t *taskfile.Task) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Timestamp) timestampFilePath() string {
|
||||
return filepath.Join(t.TempDir, "timestamp", normalizeFilename(t.Task))
|
||||
func (checker *TimestampChecker) timestampFilePath(t *taskfile.Task) string {
|
||||
return filepath.Join(checker.tempDir, "timestamp", normalizeFilename(t.Task))
|
||||
}
|
||||
36
internal/fingerprint/status.go
Normal file
36
internal/fingerprint/status.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
type StatusChecker struct {
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func NewStatusChecker(logger *logger.Logger) StatusCheckable {
|
||||
return &StatusChecker{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (checker *StatusChecker) IsUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
for _, s := range t.Status {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: s,
|
||||
Dir: t.Dir,
|
||||
Env: env.Get(t),
|
||||
})
|
||||
if err != nil {
|
||||
checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err)
|
||||
return false, nil
|
||||
}
|
||||
checker.logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
132
internal/fingerprint/task.go
Normal file
132
internal/fingerprint/task.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
type (
|
||||
CheckerOption func(*CheckerConfig)
|
||||
CheckerConfig struct {
|
||||
method string
|
||||
dry bool
|
||||
tempDir string
|
||||
logger *logger.Logger
|
||||
statusChecker StatusCheckable
|
||||
sourcesChecker SourcesCheckable
|
||||
}
|
||||
)
|
||||
|
||||
func WithMethod(method string) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.method = method
|
||||
}
|
||||
}
|
||||
|
||||
func WithDry(dry bool) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.dry = dry
|
||||
}
|
||||
}
|
||||
|
||||
func WithTempDir(tempDir string) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.tempDir = tempDir
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(logger *logger.Logger) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func WithStatusChecker(checker StatusCheckable) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.statusChecker = checker
|
||||
}
|
||||
}
|
||||
|
||||
func WithSourcesChecker(checker SourcesCheckable) CheckerOption {
|
||||
return func(config *CheckerConfig) {
|
||||
config.sourcesChecker = checker
|
||||
}
|
||||
}
|
||||
|
||||
func IsTaskUpToDate(
|
||||
ctx context.Context,
|
||||
t *taskfile.Task,
|
||||
opts ...CheckerOption,
|
||||
) (bool, error) {
|
||||
var statusUpToDate bool
|
||||
var sourcesUpToDate bool
|
||||
var err error
|
||||
|
||||
// Default config
|
||||
config := &CheckerConfig{
|
||||
method: "none",
|
||||
tempDir: "",
|
||||
dry: false,
|
||||
logger: nil,
|
||||
statusChecker: nil,
|
||||
sourcesChecker: nil,
|
||||
}
|
||||
|
||||
// Apply functional options
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
// If no status checker was given, set up the default one
|
||||
if config.statusChecker == nil {
|
||||
config.statusChecker = NewStatusChecker(config.logger)
|
||||
}
|
||||
|
||||
// If no sources checker was given, set up the default one
|
||||
if config.sourcesChecker == nil {
|
||||
config.sourcesChecker, err = NewSourcesChecker(config.method, config.tempDir, config.dry)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
statusIsSet := len(t.Status) != 0
|
||||
sourcesIsSet := len(t.Sources) != 0
|
||||
|
||||
// If status is set, check if it is up-to-date
|
||||
if statusIsSet {
|
||||
statusUpToDate, err = config.statusChecker.IsUpToDate(ctx, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// If sources is set, check if they are up-to-date
|
||||
if sourcesIsSet {
|
||||
sourcesUpToDate, err = config.sourcesChecker.IsUpToDate(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// If both status and sources are set, the task is up-to-date if both are up-to-date
|
||||
if statusIsSet && sourcesIsSet {
|
||||
return statusUpToDate && sourcesUpToDate, nil
|
||||
}
|
||||
|
||||
// If only status is set, the task is up-to-date if the status is up-to-date
|
||||
if statusIsSet {
|
||||
return statusUpToDate, nil
|
||||
}
|
||||
|
||||
// If only sources is set, the task is up-to-date if the sources are up-to-date
|
||||
if sourcesIsSet {
|
||||
return sourcesUpToDate, nil
|
||||
}
|
||||
|
||||
// If no status or sources are set, the task should always run
|
||||
// i.e. it is never considered "up-to-date"
|
||||
return false, nil
|
||||
}
|
||||
174
internal/fingerprint/task_test.go
Normal file
174
internal/fingerprint/task_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// TruthTable
|
||||
//
|
||||
// | Status up-to-date | Sources up-to-date | Task is up-to-date |
|
||||
// | ----------------- | ------------------ | ------------------ |
|
||||
// | not set | not set | false |
|
||||
// | not set | true | true |
|
||||
// | not set | false | false |
|
||||
// | true | not set | true |
|
||||
// | true | true | true |
|
||||
// | true | false | false |
|
||||
// | false | not set | false |
|
||||
// | false | true | false |
|
||||
// | false | false | false |
|
||||
func TestIsTaskUpToDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
task *taskfile.Task
|
||||
setupMockStatusChecker func(m *MockStatusCheckable)
|
||||
setupMockSourcesChecker func(m *MockSourcesCheckable)
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "expect FALSE when no status or sources are defined",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when no status is defined and sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when no status is defined and sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: nil,
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: nil,
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when status is up-to-date and sources are not defined",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect TRUE when status and sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is up-to-date, but sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(true, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is NOT up-to-date and sources are not defined",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: nil,
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status is NOT up-to-date, but sources are up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(true, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expect FALSE when status and sources are NOT up-to-date",
|
||||
task: &taskfile.Task{
|
||||
Status: []string{"status"},
|
||||
Sources: []string{"sources"},
|
||||
},
|
||||
setupMockStatusChecker: func(m *MockStatusCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any(), gomock.Any()).Return(false, nil)
|
||||
},
|
||||
setupMockSourcesChecker: func(m *MockSourcesCheckable) {
|
||||
m.EXPECT().IsUpToDate(gomock.Any()).Return(false, nil)
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockStatusChecker := NewMockStatusCheckable(ctrl)
|
||||
if tt.setupMockStatusChecker != nil {
|
||||
tt.setupMockStatusChecker(mockStatusChecker)
|
||||
}
|
||||
|
||||
mockSourcesChecker := NewMockSourcesCheckable(ctrl)
|
||||
if tt.setupMockSourcesChecker != nil {
|
||||
tt.setupMockSourcesChecker(mockSourcesChecker)
|
||||
}
|
||||
|
||||
result, err := IsTaskUpToDate(
|
||||
context.Background(),
|
||||
tt.task,
|
||||
WithStatusChecker(mockStatusChecker),
|
||||
WithSourcesChecker(mockSourcesChecker),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type Group struct {
|
||||
Begin, End string
|
||||
ErrorOnly bool
|
||||
}
|
||||
|
||||
func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Writer, io.Writer, CloseFunc) {
|
||||
@@ -17,7 +18,13 @@ func (g Group) WrapWriter(stdOut, _ io.Writer, _ string, tmpl Templater) (io.Wri
|
||||
if g.End != "" {
|
||||
gw.end = tmpl.Replace(g.End) + "\n"
|
||||
}
|
||||
return gw, gw, func() error { return gw.close() }
|
||||
return gw, gw, func(err error) error {
|
||||
if g.ErrorOnly && err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gw.close()
|
||||
}
|
||||
}
|
||||
|
||||
type groupWriter struct {
|
||||
|
||||
@@ -7,5 +7,5 @@ import (
|
||||
type Interleaved struct{}
|
||||
|
||||
func (Interleaved) WrapWriter(stdOut, stdErr io.Writer, _ string, _ Templater) (io.Writer, io.Writer, CloseFunc) {
|
||||
return stdOut, stdErr, func() error { return nil }
|
||||
return stdOut, stdErr, func(error) error { return nil }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type Output interface {
|
||||
WrapWriter(stdOut, stdErr io.Writer, prefix string, tmpl Templater) (io.Writer, io.Writer, CloseFunc)
|
||||
}
|
||||
|
||||
type CloseFunc func() error
|
||||
type CloseFunc func(err error) error
|
||||
|
||||
// Build the Output for the requested taskfile.Output.
|
||||
func BuildFor(o *taskfile.Output) (Output, error) {
|
||||
@@ -30,8 +30,9 @@ func BuildFor(o *taskfile.Output) (Output, error) {
|
||||
return Interleaved{}, nil
|
||||
case "group":
|
||||
return Group{
|
||||
Begin: o.Group.Begin,
|
||||
End: o.Group.End,
|
||||
Begin: o.Group.Begin,
|
||||
End: o.Group.End,
|
||||
ErrorOnly: o.Group.ErrorOnly,
|
||||
}, nil
|
||||
case "prefixed":
|
||||
if err := checkOutputGroupUnset(o); err != nil {
|
||||
|
||||
@@ -2,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())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type Prefixed struct{}
|
||||
|
||||
func (Prefixed) WrapWriter(stdOut, _ io.Writer, prefix string, _ Templater) (io.Writer, io.Writer, CloseFunc) {
|
||||
pw := &prefixWriter{writer: stdOut, prefix: prefix}
|
||||
return pw, pw, func() error { return pw.close() }
|
||||
return pw, pw, func(error) error { return pw.close() }
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package status
|
||||
|
||||
// None is a no-op Checker
|
||||
type None struct{}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (None) IsUpToDate() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Value implements the Checker interface
|
||||
func (None) Value() (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (None) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (None) OnError() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package status
|
||||
|
||||
var (
|
||||
_ Checker = &Timestamp{}
|
||||
_ Checker = &Checksum{}
|
||||
_ Checker = None{}
|
||||
)
|
||||
|
||||
// Checker is an interface that checks if the status is up-to-date
|
||||
type Checker interface {
|
||||
IsUpToDate() (bool, error)
|
||||
Value() (interface{}, error)
|
||||
OnError() error
|
||||
Kind() string
|
||||
}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.21.0",
|
||||
"version": "3.22.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.21.0",
|
||||
"version": "3.22.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@go-task/cli",
|
||||
"version": "3.21.0",
|
||||
"version": "3.22.0",
|
||||
"description": "A task runner / simpler Make alternative written in Go",
|
||||
"scripts": {
|
||||
"postinstall": "go-npm install",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
@@ -19,7 +20,7 @@ func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: p.Sh,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
Env: env.Get(t),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
66
setup.go
66
setup.go
@@ -9,6 +9,9 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/sajari/fuzzy"
|
||||
|
||||
compilerv2 "github.com/go-task/task/v3/internal/compiler/v2"
|
||||
compilerv3 "github.com/go-task/task/v3/internal/compiler/v3"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
@@ -17,8 +20,6 @@ import (
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
"github.com/go-task/task/v3/taskfile/read"
|
||||
|
||||
"github.com/sajari/fuzzy"
|
||||
)
|
||||
|
||||
func (e *Executor) Setup() error {
|
||||
@@ -32,11 +33,6 @@ func (e *Executor) Setup() error {
|
||||
|
||||
e.setupFuzzyModel()
|
||||
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.setupTempDir(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,17 +41,17 @@ func (e *Executor) Setup() error {
|
||||
if err := e.setupOutput(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.setupCompiler(v); err != nil {
|
||||
if err := e.setupCompiler(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.readDotEnvFiles(v); err != nil {
|
||||
if err := e.readDotEnvFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.doVersionChecks(v); err != nil {
|
||||
if err := e.doVersionChecks(); err != nil {
|
||||
return err
|
||||
}
|
||||
e.setupDefaults(v)
|
||||
e.setupDefaults()
|
||||
e.setupConcurrencyState()
|
||||
|
||||
return nil
|
||||
@@ -163,8 +159,8 @@ func (e *Executor) setupOutput() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Executor) setupCompiler(v float64) error {
|
||||
if v < 3 {
|
||||
func (e *Executor) setupCompiler() error {
|
||||
if e.Taskfile.Version.LessThan(taskfile.V3) {
|
||||
var err error
|
||||
e.taskvars, err = read.Taskvars(e.Dir)
|
||||
if err != nil {
|
||||
@@ -195,8 +191,8 @@ func (e *Executor) setupCompiler(v float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) readDotEnvFiles(v float64) error {
|
||||
if v < 3.0 {
|
||||
func (e *Executor) readDotEnvFiles() error {
|
||||
if e.Taskfile.Version.LessThan(taskfile.V3) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -214,14 +210,14 @@ func (e *Executor) readDotEnvFiles(v float64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Executor) setupDefaults(v float64) {
|
||||
func (e *Executor) setupDefaults() {
|
||||
// Color available only on v3
|
||||
if v < 3 {
|
||||
if e.Taskfile.Version.LessThan(taskfile.V3) {
|
||||
e.Logger.Color = false
|
||||
}
|
||||
|
||||
if e.Taskfile.Method == "" {
|
||||
if v >= 3 {
|
||||
if e.Taskfile.Version.Compare(taskfile.V3) >= 0 {
|
||||
e.Taskfile.Method = "checksum"
|
||||
} else {
|
||||
e.Taskfile.Method = "timestamp"
|
||||
@@ -248,37 +244,41 @@ func (e *Executor) setupConcurrencyState() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) doVersionChecks(v float64) error {
|
||||
if v < 2 {
|
||||
func (e *Executor) doVersionChecks() error {
|
||||
// Copy the version to avoid modifying the original
|
||||
v := &semver.Version{}
|
||||
*v = *e.Taskfile.Version
|
||||
|
||||
if v.LessThan(taskfile.V2) {
|
||||
return fmt.Errorf(`task: Taskfile versions prior to v2 are not supported anymore`)
|
||||
}
|
||||
|
||||
// consider as equal to the greater version if round
|
||||
if v == 2.0 {
|
||||
v = 2.6
|
||||
if v.Equal(taskfile.V2) {
|
||||
v = semver.MustParse("2.6")
|
||||
}
|
||||
if v == 3.0 {
|
||||
v = 3.8
|
||||
if v.Equal(taskfile.V3) {
|
||||
v = semver.MustParse("3.8")
|
||||
}
|
||||
|
||||
if v > 3.8 {
|
||||
if v.GreaterThan(semver.MustParse("3.8")) {
|
||||
return fmt.Errorf(`task: Taskfile versions greater than v3.8 not implemented in the version of Task`)
|
||||
}
|
||||
|
||||
if v < 2.1 && !e.Taskfile.Output.IsSet() {
|
||||
if v.LessThan(semver.MustParse("2.1")) && !e.Taskfile.Output.IsSet() {
|
||||
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
|
||||
}
|
||||
if v < 2.2 && e.Taskfile.Includes.Len() > 0 {
|
||||
if v.LessThan(semver.MustParse("2.2")) && e.Taskfile.Includes.Len() > 0 {
|
||||
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
|
||||
}
|
||||
if v >= 3.0 && e.Taskfile.Expansions > 2 {
|
||||
if v.Compare(taskfile.V3) >= 0 && e.Taskfile.Expansions > 2 {
|
||||
return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`)
|
||||
}
|
||||
if v < 3.8 && e.Taskfile.Output.Group.IsSet() {
|
||||
if v.LessThan(semver.MustParse("3.8")) && e.Taskfile.Output.Group.IsSet() {
|
||||
return fmt.Errorf(`task: Taskfile option "output.group" is only available starting on Taskfile version v3.8`)
|
||||
}
|
||||
|
||||
if v <= 2.1 {
|
||||
if v.Compare(semver.MustParse("2.1")) <= 0 {
|
||||
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
|
||||
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
@@ -293,7 +293,7 @@ func (e *Executor) doVersionChecks(v float64) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v < 2.6 {
|
||||
if v.LessThan(semver.MustParse("2.6")) {
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
if len(task.Preconditions) > 0 {
|
||||
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
|
||||
@@ -301,7 +301,7 @@ func (e *Executor) doVersionChecks(v float64) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v < 3 {
|
||||
if v.LessThan(taskfile.V3) {
|
||||
err := e.Taskfile.Includes.Range(func(_ string, taskfile taskfile.IncludedTaskfile) error {
|
||||
if taskfile.AdvancedImport {
|
||||
return errors.New(`task: Import with additional parameters is only available starting on Taskfile version v3`)
|
||||
@@ -313,7 +313,7 @@ func (e *Executor) doVersionChecks(v float64) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v < 3.7 {
|
||||
if v.LessThan(semver.MustParse("3.7")) {
|
||||
if e.Taskfile.Run != "" {
|
||||
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)
|
||||
}
|
||||
|
||||
112
status.go
112
status.go
@@ -4,20 +4,33 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/status"
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
|
||||
// Status returns an error if any the of given tasks is not up-to-date
|
||||
func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error {
|
||||
for _, call := range calls {
|
||||
|
||||
// Compile the task
|
||||
t, err := e.CompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isUpToDate, err := e.isTaskUpToDate(ctx, t)
|
||||
|
||||
// Get the fingerprinting method to use
|
||||
method := e.Taskfile.Method
|
||||
if t.Method != "" {
|
||||
method = t.Method
|
||||
}
|
||||
|
||||
// Check if the task is up-to-date
|
||||
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
|
||||
fingerprint.WithMethod(method),
|
||||
fingerprint.WithTempDir(e.TempDir),
|
||||
fingerprint.WithDry(e.Dry),
|
||||
fingerprint.WithLogger(e.Logger),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -28,97 +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{
|
||||
TempDir: e.TempDir,
|
||||
Task: t.Name(),
|
||||
Dir: t.Dir,
|
||||
Sources: t.Sources,
|
||||
Generates: t.Generates,
|
||||
Dry: e.Dry,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
56
task.go
56
task.go
@@ -13,7 +13,9 @@ 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"
|
||||
@@ -157,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
|
||||
}
|
||||
@@ -282,22 +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 writer: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: cmd.Cmd,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
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
|
||||
@@ -308,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 {
|
||||
@@ -342,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
|
||||
}
|
||||
|
||||
94
task_test.go
94
task_test.go
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
@@ -446,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) {
|
||||
@@ -728,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 {
|
||||
@@ -1568,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"
|
||||
|
||||
@@ -10,7 +10,7 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,15 +2,20 @@ 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
|
||||
@@ -31,7 +36,7 @@ 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
|
||||
@@ -77,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
|
||||
}
|
||||
|
||||
8
testdata/checksum/Taskfile.yml
vendored
8
testdata/checksum/Taskfile.yml
vendored
@@ -10,3 +10,11 @@ tasks:
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
|
||||
build-with-status:
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./source.txt
|
||||
status:
|
||||
- test -f ./generated.txt
|
||||
|
||||
17
testdata/output_group_error_only/Taskfile.yml
vendored
Normal file
17
testdata/output_group_error_only/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
output:
|
||||
group:
|
||||
error_only: true
|
||||
|
||||
tasks:
|
||||
passing: echo 'passing-output'
|
||||
|
||||
failing:
|
||||
cmds:
|
||||
- task: passing
|
||||
- echo 'passing-output-2'
|
||||
- echo 'passing-output-3'
|
||||
- echo 'failing-output' && exit 1
|
||||
16
variables.go
16
variables.go
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/filepathext"
|
||||
"github.com/go-task/task/v3/internal/status"
|
||||
"github.com/go-task/task/v3/internal/fingerprint"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile"
|
||||
)
|
||||
@@ -40,12 +40,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := templater.Templater{Vars: vars, RemoveNoValue: v >= 3.0}
|
||||
r := templater.Templater{Vars: vars, RemoveNoValue: e.Taskfile.Version.Compare(taskfile.V3) >= 0}
|
||||
|
||||
new := taskfile.Task{
|
||||
Task: origTask.Task,
|
||||
@@ -166,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
|
||||
}
|
||||
|
||||
4
watch.go
4
watch.go
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user