Compare commits

..

43 Commits

Author SHA1 Message Date
Andrey Nering
2eb52da0db v2.6.0 2019-07-21 11:11:30 -03:00
Andrey Nering
d8bfb3ab13 Add CHANGELOG and documentation for Linux support on Homebrew
Ref: https://github.com/go-task/homebrew-tap/pull/1

Thanks @dawidd6
2019-07-21 11:03:22 -03:00
Andrey Nering
d970e93507 Add --taskfile flag (alias -t) to allow running another Taskfile
Closes #221
2019-07-21 10:57:04 -03:00
Andrey Nering
762714de68 Merge pull request #213 from marco-m/document-dir-creation
Document dir: creation (see PR #211)
2019-06-16 11:03:08 -03:00
Marco Molteni
82a3651a18 Document dir: creation (see PR #211) 2019-06-16 11:33:00 +02:00
Andrey Nering
abe0352de9 Fixed some bugs regarding minor version checks on version:
1. I have forgot to update it on recent releases. Seems that most people just
   use round versions since nobody complained.
2. It's too hard to understand how the github.com/Masterminds/semver package
   works, so I just got rid of it and we're now using plain float checks.
2019-06-15 22:39:35 -03:00
Andrey Nering
4cee4aa5a8 Fix typo 2019-06-15 21:58:37 -03:00
Andrey Nering
9c68c7c50b Add changelog for #205 2019-06-15 21:56:34 -03:00
Andrey Nering
0608782cfa Merge pull request #205 from CypherpunkArmory/add-precondition-to-task
Add Preconditions to Tasks
2019-06-15 21:55:20 -03:00
Andrey Nering
edeaf3794a Merge pull request #212 from ezhukov/patch-1
Add missing "-" in usage.md
2019-06-15 21:21:41 -03:00
Andrey Nering
fe2b8c8afa Post-fixes to #211 2019-06-15 21:12:54 -03:00
Andrey Nering
b66bf58064 Merge branch 'marco-m-209-create-dir' 2019-06-15 20:53:34 -03:00
Andrey Nering
957dfa9cdf Merge branch '209-create-dir' of https://github.com/marco-m/task into marco-m-209-create-dir 2019-06-15 20:52:08 -03:00
Stephen Prater
cc9264854e Change error output 2019-06-11 12:20:56 -07:00
Stephen Prater
d1463b3e24 Fix typos per review 2019-06-11 11:46:22 -07:00
Eugene Zhukov
f1082520e1 Add missing "-" in usage.md 2019-06-11 11:35:10 +03:00
Marco Molteni
733c563194 Protect creation of "dir:" with a mutex 2019-06-10 17:40:20 +02:00
Andrey Nering
0200d043c3 Add funding button via OpenCollective 2019-06-09 21:53:55 -03:00
Marco Molteni
9c475c36e7 Handle the common case when the task directory is not specified
Closes #209
2019-06-06 20:40:31 +02:00
Marco Molteni
c663c5c507 When "dir:" attribute points to a non-existing dir, create it
Closes #209
2019-06-04 18:58:22 +02:00
Marco Molteni
1e93c38307 Task directory: test when "dir:" attribute points to an existing dir 2019-06-04 18:36:35 +02:00
Marco Molteni
81baf808c9 Task directory: test default case (no "dir:" attribute) 2019-06-04 18:24:01 +02:00
Marco Molteni
74537689dc Fix spelling 2019-06-04 08:08:25 +02:00
Stephen Prater
12ab01d5e6 Clarify difference between status and precondition in docs 2019-05-28 13:18:06 -07:00
Stephen Prater
044d3a0ff9 Remove ignore_errors 2019-05-28 13:02:59 -07:00
Stephen Prater
659cae6a4c Apply suggestions from code review
Co-Authored-By: Andrey Nering <andrey.nering@gmail.com>
2019-05-28 12:28:29 -07:00
Stephen Prater
bd5882f0f0 Add Preconditions to Tasks 2019-05-17 13:51:15 -07:00
Andrey Nering
6ff9ba9df9 v2.5.2 2019-05-11 11:28:21 -03:00
Andrey Nering
b2df398a12 go mod vendor 2019-05-11 11:22:47 -03:00
Andrey Nering
83d618e1eb Revert "Upgrade to yaml/go-yaml v3"
This reverts commit 8001fb3915.
2019-05-11 11:22:13 -03:00
Andrey Nering
f0768b3af1 Allow setting global variables through the CLI
Closes #192
2019-05-11 11:06:47 -03:00
Andrey Nering
0233ce52ed v2.5.1 2019-04-27 17:56:30 -03:00
Andrey Nering
6e6f337509 Updated change log 2019-04-27 17:28:58 -03:00
Andrey Nering
1546415b8f Update CHANGELOG.md 2019-04-21 17:16:35 -03:00
Andrey Nering
20725c69bf Merge pull request #200 from go-task/fix-output-issues
Fixes some bugs relatated to commands output handling
2019-04-21 17:05:21 -03:00
Andrey Nering
90613220c6 Fixes some bugs relatated to commands output handling
This seems to fix some of the bugs reported by issues like #114 and #190.

Seems that the standard library's os/exec package has some black magic to
detect if a writer is an actual *os.File, and some stuff are handled
differently, then.

Fixes #114
Fixes #190
2019-04-21 16:55:47 -03:00
Andrey Nering
659fd2ae93 Update Go version on CI 2019-04-13 17:44:55 -03:00
Andrey Nering
29d899f7da Merge pull request #198 from go-task/yaml-v3
Upgrade to go-yaml/yaml v3
2019-04-13 17:29:36 -03:00
Andrey Nering
902a0a01a9 go vendor mod 2019-04-13 17:26:27 -03:00
Andrey Nering
8001fb3915 Upgrade to yaml/go-yaml v3 2019-04-13 17:25:28 -03:00
Andrey Nering
e81e2802f0 Small fix to redirector 2019-03-23 17:48:18 -03:00
Andrey Nering
1ee066ec42 Merge pull request #188 from sosiska/patch-1
Rewrite if-else chain to switch statement
2019-03-23 17:04:31 -03:00
Kirill Motkov
53d54d1c4a Rewrite if-else chain to switch statement 2019-03-19 14:19:21 +03:00
33 changed files with 602 additions and 176 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
open_collective: task

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ dist/
# intellij idea/goland
.idea/
# exuberant ctags
tags

View File

@@ -1,8 +1,8 @@
language: go
go:
- 1.10.x
- 1.11.x
- 1.12.x
addons:
apt:

View File

@@ -1,6 +1,33 @@
# Changelog
## v2.5.0 = 2019-03-16
## Unreleased
- Fixed some bugs regarding minor version checks on `version:`.
- Add `preconditions:` to task
([#205](https://github.com/go-task/task/pull/205)).
- Create directory informed on `dir:` if it doesn't exist
([#209](https://github.com/go-task/task/issues/209), [#211](https://github.com/go-task/task/pull/211)).
- We now have a `--taskfile` flag (alias `-t`), which can be used to run
another Taskfile (other than the default `Taskfile.yml`)
([#221](https://github.com/go-task/task/pull/221)).
- It's now possible to install Task using Homebrew on Linux
([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)).
## v2.5.2 - 2019-05-11
- Reverted YAML upgrade due issues with CRLF on Windows
([#201](https://github.com/go-task/task/issues/201), [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)).
- Allow setting global variables through the CLI
([#192](https://github.com/go-task/task/issues/192)).
## 2.5.1 - 2019-04-27
- Fixed some issues with interactive command line tools, where sometimes
the output were not being shown, and similar issues
([#114](https://github.com/go-task/task/issues/114), [#190](https://github.com/go-task/task/issues/190), [#200](https://github.com/go-task/task/pull/200)).
- Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3.
## v2.5.0 - 2019-03-16
- We moved from the taskfile.org domain to the new fancy taskfile.dev domain.
While stuff is being redirected, we strongly recommend to everyone that use

View File

@@ -1,5 +1,3 @@
//+build ignore
// This small web app is used to redirect from the old taskfile.org domain
// to the new taskfile.dev without breaking CIs that uses cURL to download
// "/install.sh" without the -L flag (which follow redirects).
@@ -19,9 +17,9 @@ func main() {
return
}
println("Redirecting to taskfile.dev")
println("Redirecting to https://taskfile.dev" + r.URL.Path)
w.Header().Set("Location", "https://taskfile.dev")
w.Header().Set("Location", "https://taskfile.dev"+r.URL.Path)
w.WriteHeader(301)
})

View File

@@ -5,6 +5,7 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/go-task/task/v2"
@@ -17,7 +18,7 @@ var (
version = "master"
)
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--dry] [--summary] [task...]
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
was specified, or lists all tasks if an unknown task name was specified.
@@ -58,6 +59,7 @@ func main() {
dry bool
summary bool
dir string
entrypoint string
output string
)
@@ -72,6 +74,7 @@ func main() {
pflag.BoolVar(&dry, "dry", 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.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, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
pflag.Parse()
@@ -91,14 +94,26 @@ func main() {
return
}
if dir != "" && entrypoint != "" {
log.Fatal("task: You can't set both --dir and --taskfile")
return
}
if entrypoint != "" {
dir = filepath.Dir(entrypoint)
entrypoint = filepath.Base(entrypoint)
} else {
entrypoint = "Taskfile.yml"
}
e := task.Executor{
Force: force,
Watch: watch,
Verbose: verbose,
Silent: silent,
Dir: dir,
Dry: dry,
Summary: summary,
Force: force,
Watch: watch,
Verbose: verbose,
Silent: silent,
Dir: dir,
Dry: dry,
Entrypoint: entrypoint,
Summary: summary,
Stdin: os.Stdin,
Stdout: os.Stdout,
@@ -121,9 +136,9 @@ func main() {
arguments = []string{"default"}
}
calls, err := args.Parse(arguments...)
if err != nil {
log.Fatal(err)
calls, globals := args.Parse(arguments...)
for name, value := range globals {
e.Taskfile.Vars[name] = value
}
ctx := context.Background()
@@ -132,7 +147,7 @@ func main() {
}
if status {
if err = e.Status(ctx, calls...); err != nil {
if err := e.Status(ctx, calls...); err != nil {
log.Fatal(err)
}
return

View File

@@ -8,13 +8,15 @@ The `task_checksums.txt` file contains the sha256 checksum for each file.
## Homebrew
If you're on macOS and have [Homebrew][homebrew] installed, getting Task is
as simple as running:
If you're on macOS or Linux and have [Homebrew][homebrew] installed, getting
Task is as simple as running:
```bash
brew install go-task/tap/go-task
```
> This installation method is only currently supported on amd64 architectures.
## Snap
Task is available for [Snapcraft][snapcraft], but keep in mind that your

View File

@@ -141,6 +141,21 @@ includes:
docker: ./DockerTasks.yml
```
## Version 2.6
Version 2.6 comes with `preconditions` stanza in tasks.
```yaml
version: '2'
tasks:
upload_environment:
preconditions:
- test -f .env
cmds:
- aws s3 cp .env s3://myenvironment
```
Please check the [documentation][includes]
[output]: usage.md#output-syntax

View File

@@ -148,6 +148,8 @@ tasks:
- caddy
```
If the directory doesn't exist, `task` creates it.
## Task dependencies
You may have tasks that depend on others. Just pointing them on `deps` will
@@ -344,6 +346,55 @@ up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
the tasks are not up-to-date.
If you need a certain set of conditions to be _true_ you can use the
`preconditions` stanza. `preconditions` are very similar to `status`
lines except they support `sh` expansion and they SHOULD all return 0.
```yaml
version: '2'
tasks:
generate-files:
cmds:
- mkdir directory
- touch directory/file1.txt
- touch directory/file2.txt
# test existence of files
preconditions:
- test -f .env
- sh: "[ 1 = 0 ]"
msg: "One doesn't equal Zero, Halting"
```
Preconditions can set specific failure messages that can tell
a user what steps to take using the `msg` field.
If a task has a dependency on a sub-task with a precondition, and that
precondition is not met - the calling task will fail. Note that a task
executed with a failing precondition will not run unless `--force` is
given.
Unlike `status` which will skip a task if it is up to date, and continue
executing tasks that depend on it, a `precondition` will fail a task, along
with any other tasks that depend on it.
```yaml
version: '2'
tasks:
task_will_fail:
preconditions:
- sh: "exit 1"
task_will_also_fail:
deps:
- task_will_fail
task_will_still_fail:
cmds:
- task: task_will_fail
- echo "I will not run"
```
## Variables
When doing interpolation of variables, Task will look for the below.
@@ -371,6 +422,12 @@ right before.
$ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!"
```
If you want to set global variables using this syntax, give it before any task:
```bash
$ task OUTPUT=file.txt generate-file
```
Example of locally declared vars:
```yaml
@@ -379,7 +436,7 @@ version: '2'
tasks:
print-var:
cmds:
echo "{{.VAR}}"
- echo "{{.VAR}}"
vars:
VAR: Hello!
```
@@ -582,7 +639,7 @@ dependencies:
commands:
- your-release-tool
```
If a summary is missing, the description will be printed.
If a summary is missing, the description will be printed.
If the task does not have a summary or a description, a warning is printed.
Please note: *showing the summary will not execute the command*.

2
go.mod
View File

@@ -1,7 +1,7 @@
module github.com/go-task/task/v2
require (
github.com/Masterminds/semver v1.4.2
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.16.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

View File

@@ -1,36 +1,43 @@
package args
import (
"errors"
"strings"
"github.com/go-task/task/v2/internal/taskfile"
)
var (
// ErrVariableWithoutTask is returned when variables are given before any task
ErrVariableWithoutTask = errors.New("task: variable given before any task")
)
// Parse parses command line argument: tasks and vars of each task
func Parse(args ...string) ([]taskfile.Call, error) {
func Parse(args ...string) ([]taskfile.Call, taskfile.Vars) {
var calls []taskfile.Call
var globals taskfile.Vars
for _, arg := range args {
if !strings.Contains(arg, "=") {
calls = append(calls, taskfile.Call{Task: arg})
continue
}
if len(calls) < 1 {
return nil, ErrVariableWithoutTask
}
if globals == nil {
globals = taskfile.Vars{}
}
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = make(taskfile.Vars)
}
name, value := splitVar(arg)
globals[name] = taskfile.Var{Static: value}
} else {
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = make(taskfile.Vars)
}
pair := strings.SplitN(arg, "=", 2)
calls[len(calls)-1].Vars[pair[0]] = taskfile.Var{Static: pair[1]}
name, value := splitVar((arg))
calls[len(calls)-1].Vars[name] = taskfile.Var{Static: value}
}
}
return calls, nil
return calls, globals
}
func splitVar(s string) (string, string) {
pair := strings.SplitN(s, "=", 2)
return pair[0], pair[1]
}

View File

@@ -12,13 +12,13 @@ import (
func TestArgs(t *testing.T) {
tests := []struct {
Args []string
Expected []taskfile.Call
Err error
Args []string
ExpectedCalls []taskfile.Call
ExpectedGlobals taskfile.Vars
}{
{
Args: []string{"task-a", "task-b", "task-c"},
Expected: []taskfile.Call{
ExpectedCalls: []taskfile.Call{
{Task: "task-a"},
{Task: "task-b"},
{Task: "task-c"},
@@ -26,7 +26,7 @@ func TestArgs(t *testing.T) {
},
{
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
Expected: []taskfile.Call{
ExpectedCalls: []taskfile.Call{
{
Task: "task-a",
Vars: taskfile.Vars{
@@ -45,7 +45,7 @@ func TestArgs(t *testing.T) {
},
{
Args: []string{"task-a", "CONTENT=with some spaces"},
Expected: []taskfile.Call{
ExpectedCalls: []taskfile.Call{
{
Task: "task-a",
Vars: taskfile.Vars{
@@ -55,16 +55,22 @@ func TestArgs(t *testing.T) {
},
},
{
Args: []string{"FOO=bar", "task-a"},
Err: args.ErrVariableWithoutTask,
Args: []string{"FOO=bar", "task-a", "task-b"},
ExpectedCalls: []taskfile.Call{
{Task: "task-a"},
{Task: "task-b"},
},
ExpectedGlobals: taskfile.Vars{
"FOO": {Static: "bar"},
},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
calls, err := args.Parse(test.Args...)
assert.Equal(t, test.Err, err)
assert.Equal(t, test.Expected, calls)
calls, globals := args.Parse(test.Args...)
assert.Equal(t, test.ExpectedCalls, calls)
assert.Equal(t, test.ExpectedGlobals, globals)
})
}
}

View File

@@ -7,7 +7,7 @@ import (
type Group struct{}
func (Group) WrapWriter(w io.Writer, _ string) io.WriteCloser {
func (Group) WrapWriter(w io.Writer, _ string) io.Writer {
return &groupWriter{writer: w}
}

View File

@@ -6,18 +6,6 @@ import (
type Interleaved struct{}
func (Interleaved) WrapWriter(w io.Writer, _ string) io.WriteCloser {
return nopWriterCloser{w: w}
}
type nopWriterCloser struct {
w io.Writer
}
func (wc nopWriterCloser) Write(p []byte) (int, error) {
return wc.w.Write(p)
}
func (wc nopWriterCloser) Close() error {
return nil
func (Interleaved) WrapWriter(w io.Writer, _ string) io.Writer {
return w
}

View File

@@ -5,5 +5,5 @@ import (
)
type Output interface {
WrapWriter(w io.Writer, prefix string) io.WriteCloser
WrapWriter(w io.Writer, prefix string) io.Writer
}

View File

@@ -3,6 +3,7 @@ package output_test
import (
"bytes"
"fmt"
"io"
"testing"
"github.com/go-task/task/v2/internal/output"
@@ -24,7 +25,7 @@ func TestInterleaved(t *testing.T) {
func TestGroup(t *testing.T) {
var b bytes.Buffer
var o output.Output = output.Group{}
var w = o.WrapWriter(&b, "")
var w = o.WrapWriter(&b, "").(io.WriteCloser)
fmt.Fprintln(w, "foo\nbar")
assert.Equal(t, "", b.String())
@@ -37,7 +38,7 @@ func TestGroup(t *testing.T) {
func TestPrefixed(t *testing.T) {
var b bytes.Buffer
var o output.Output = output.Prefixed{}
var w = o.WrapWriter(&b, "prefix")
var w = o.WrapWriter(&b, "prefix").(io.WriteCloser)
t.Run("simple use cases", func(t *testing.T) {
b.Reset()

View File

@@ -9,7 +9,7 @@ import (
type Prefixed struct{}
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.WriteCloser {
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.Writer {
return &prefixWriter{writer: w, prefix: prefix}
}
@@ -34,12 +34,12 @@ func (pw *prefixWriter) Close() error {
func (pw *prefixWriter) writeOutputLines(force bool) error {
for {
line, err := pw.buff.ReadString('\n')
if err == nil {
switch line, err := pw.buff.ReadString('\n'); err {
case nil:
if err = pw.writeLine(line); err != nil {
return err
}
} else if err == io.EOF {
case io.EOF:
// if this line was not a complete line, re-add to the buffer
if !force && !strings.HasSuffix(line, "\n") {
_, err = pw.buff.WriteString(line)
@@ -47,7 +47,7 @@ func (pw *prefixWriter) writeOutputLines(force bool) error {
}
return pw.writeLine(line)
} else {
default:
return err
}
}

View File

@@ -0,0 +1,45 @@
package taskfile
import (
"errors"
"fmt"
)
var (
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value")
)
// Precondition represents a precondition necessary for a task to run
type Precondition struct {
Sh string
Msg string
}
// UnmarshalYAML implements yaml.Unmarshaler interface.
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
var cmd string
if err := unmarshal(&cmd); err == nil {
p.Sh = cmd
p.Msg = fmt.Sprintf("`%s` failed", cmd)
return nil
}
var sh struct {
Sh string
Msg string
}
if err := unmarshal(&sh); err != nil {
return err
}
p.Sh = sh.Sh
p.Msg = sh.Msg
if p.Msg == "" {
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
}
return nil
}

View File

@@ -0,0 +1,48 @@
package taskfile_test
import (
"testing"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestPreconditionParse(t *testing.T) {
tests := []struct {
content string
v interface{}
expected interface{}
}{
{
"test -f foo.txt",
&taskfile.Precondition{},
&taskfile.Precondition{Sh: `test -f foo.txt`, Msg: "`test -f foo.txt` failed"},
},
{
"sh: '[ 1 = 0 ]'",
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 0 ]", Msg: "[ 1 = 0 ] failed"},
},
{`
sh: "[ 1 = 2 ]"
msg: "1 is not 2"
`,
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"},
},
{`
sh: "[ 1 = 2 ]"
msg: "1 is not 2"
`,
&taskfile.Precondition{},
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"},
},
}
for _, test := range tests {
err := yaml.Unmarshal([]byte(test.content), test.v)
assert.NoError(t, err)
assert.Equal(t, test.expected, test.v)
}
}

View File

@@ -15,16 +15,13 @@ import (
var (
// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes
ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile")
// ErrNoTaskfileFound is returned when Taskfile.yml is not found
ErrNoTaskfileFound = errors.New(`task: No Taskfile.yml found. Use "task --init" to create a new one`)
)
// Taskfile reads a Taskfile for a given directory
func Taskfile(dir string) (*taskfile.Taskfile, error) {
path := filepath.Join(dir, "Taskfile.yml")
func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
path := filepath.Join(dir, entrypoint)
if _, err := os.Stat(path); err != nil {
return nil, ErrNoTaskfileFound
return nil, fmt.Errorf(`task: No Taskfile found on "%s". Use "task --init" to create a new one`, path)
}
t, err := readTaskfile(path)
if err != nil {

View File

@@ -5,19 +5,20 @@ type Tasks map[string]*Task
// Task represents a task
type Task struct {
Task string
Cmds []*Cmd
Deps []*Dep
Desc string
Summary string
Sources []string
Generates []string
Status []string
Dir string
Vars Vars
Env Vars
Silent bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
Task string
Cmds []*Cmd
Deps []*Dep
Desc string
Summary string
Sources []string
Generates []string
Status []string
Preconditions []*Precondition
Dir string
Vars Vars
Env Vars
Silent bool
Method string
Prefix string
IgnoreError bool `yaml:"ignore_error"`
}

View File

@@ -1,46 +0,0 @@
package version
import (
"github.com/Masterminds/semver"
)
var (
v1 = mustVersion("1")
v2 = mustVersion("2")
v21 = mustVersion("2.1")
v22 = mustVersion("2.2")
v23 = mustVersion("2.3")
)
// IsV1 returns if is a given Taskfile version is version 1
func IsV1(v *semver.Constraints) bool {
return v.Check(v1)
}
// IsV2 returns if is a given Taskfile version is at least version 2
func IsV2(v *semver.Constraints) bool {
return v.Check(v2)
}
// IsV21 returns if is a given Taskfile version is at least version 2.1
func IsV21(v *semver.Constraints) bool {
return v.Check(v21)
}
// IsV22 returns if is a given Taskfile version is at least version 2.2
func IsV22(v *semver.Constraints) bool {
return v.Check(v22)
}
// IsV23 returns if is a given Taskfile version is at least version 2.3
func IsV23(v *semver.Constraints) bool {
return v.Check(v23)
}
func mustVersion(s string) *semver.Version {
v, err := semver.NewVersion(s)
if err != nil {
panic(err)
}
return v
}

31
precondition.go Normal file
View File

@@ -0,0 +1,31 @@
package task
import (
"context"
"errors"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/taskfile"
)
var (
// ErrPreconditionFailed is returned when a precondition fails
ErrPreconditionFailed = errors.New("task: precondition not met")
)
func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task) (bool, error) {
for _, p := range t.Preconditions {
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: p.Sh,
Dir: t.Dir,
Env: getEnviron(t),
})
if err != nil {
e.Logger.Errf("task: %s", p.Msg)
return false, ErrPreconditionFailed
}
}
return true, nil
}

View File

@@ -78,8 +78,10 @@ func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (
Env: getEnviron(t),
})
if err != nil {
e.Logger.VerboseOutf("task: status command %s exited non-zero: %s", s, err)
return false, nil
}
e.Logger.VerboseOutf("task: status command %s exited zero", s)
}
return true, nil
}

131
task.go
View File

@@ -2,9 +2,12 @@ package task
import (
"context"
"errors"
"fmt"
"io"
"os"
"strconv"
"sync"
"sync/atomic"
"github.com/go-task/task/v2/internal/compiler"
@@ -16,9 +19,7 @@ import (
"github.com/go-task/task/v2/internal/summary"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/go-task/task/v2/internal/taskfile/read"
"github.com/go-task/task/v2/internal/taskfile/version"
"github.com/Masterminds/semver"
"golang.org/x/sync/errgroup"
)
@@ -31,13 +32,15 @@ const (
// Executor executes a Taskfile
type Executor struct {
Taskfile *taskfile.Taskfile
Dir string
Force bool
Watch bool
Verbose bool
Silent bool
Dry bool
Summary bool
Dir string
Entrypoint string
Force bool
Watch bool
Verbose bool
Silent bool
Dry bool
Summary bool
Stdin io.Reader
Stdout io.Writer
@@ -51,6 +54,7 @@ type Executor struct {
taskvars taskfile.Vars
taskCallCount map[string]*int32
mkdirMutexMap map[string]*sync.Mutex
}
// Run runs Task
@@ -83,8 +87,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
// Setup setups Executor's internal state
func (e *Executor) Setup() error {
if e.Entrypoint == "" {
e.Entrypoint = "Taskfile.yml"
}
var err error
e.Taskfile, err = read.Taskfile(e.Dir)
e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
if err != nil {
return err
}
@@ -93,11 +101,6 @@ func (e *Executor) Setup() error {
return err
}
v, err := semver.NewConstraint(e.Taskfile.Version)
if err != nil {
return fmt.Errorf(`task: could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
}
if e.Stdin == nil {
e.Stdin = os.Stdin
}
@@ -112,14 +115,30 @@ func (e *Executor) Setup() error {
Stderr: e.Stderr,
Verbose: e.Verbose,
}
switch {
case version.IsV1(v):
v, err := strconv.ParseFloat(e.Taskfile.Version, 64)
if err != nil {
return fmt.Errorf(`task: Could not parse taskfile version "%s": %v`, e.Taskfile.Version, err)
}
// consider as equal to the greater version if round
if v == 2.0 {
v = 2.6
}
if v < 1 {
return fmt.Errorf(`task: Taskfile version should be greater or equal to v1`)
}
if v > 2.6 {
return fmt.Errorf(`task: Taskfile versions greater than v2.6 not implemented in the version of Task`)
}
if v < 2 {
e.Compiler = &compilerv1.CompilerV1{
Dir: e.Dir,
Vars: e.taskvars,
Logger: e.Logger,
}
case version.IsV2(v), version.IsV21(v), version.IsV22(v):
} else { // v >= 2
e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir,
Taskvars: e.taskvars,
@@ -127,16 +146,15 @@ func (e *Executor) Setup() error {
Expansions: e.Taskfile.Expansions,
Logger: e.Logger,
}
case version.IsV23(v):
return fmt.Errorf(`task: Taskfile versions greater than v2.3 not implemented in the version of Task`)
}
if !version.IsV21(v) && e.Taskfile.Output != "" {
if v < 2.1 && e.Taskfile.Output != "" {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
}
if !version.IsV22(v) && len(e.Taskfile.Includes) > 0 {
if v < 2.2 && len(e.Taskfile.Includes) > 0 {
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
}
if e.OutputStyle != "" {
e.Taskfile.Output = e.OutputStyle
}
@@ -151,8 +169,8 @@ func (e *Executor) Setup() error {
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
}
if !version.IsV21(v) {
err := fmt.Errorf(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
if v <= 2.1 {
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
for _, task := range e.Taskfile.Tasks {
if task.IgnoreError {
@@ -166,9 +184,19 @@ func (e *Executor) Setup() error {
}
}
if v < 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`)
}
}
}
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
for k := range e.Taskfile.Tasks {
e.taskCallCount[k] = new(int32)
e.mkdirMutexMap[k] = &sync.Mutex{}
}
return nil
}
@@ -188,11 +216,17 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
}
if !e.Force {
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
if err != nil {
return err
}
upToDate, err := e.isTaskUpToDate(ctx, t)
if err != nil {
return err
}
if upToDate {
if upToDate && preCondMet {
if !e.Silent {
e.Logger.Errf(`task: Task "%s" is up to date`, t.Task)
}
@@ -200,6 +234,10 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
}
}
if err := e.mkdir(t); err != nil {
e.Logger.Errf("task: cannot make directory %q: %v", t.Dir, err)
}
for i := range t.Cmds {
if err := e.runCommand(ctx, t, call, i); err != nil {
if err2 := e.statusOnError(t); err2 != nil {
@@ -217,6 +255,23 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
return nil
}
func (e *Executor) mkdir(t *taskfile.Task) error {
if t.Dir == "" {
return nil
}
mutex := e.mkdirMutexMap[t.Task]
mutex.Lock()
defer mutex.Unlock()
if _, err := os.Stat(t.Dir); os.IsNotExist(err) {
if err := os.MkdirAll(t.Dir, 0755); err != nil {
return err
}
}
return nil
}
func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
g, ctx := errgroup.WithContext(ctx)
@@ -224,7 +279,11 @@ func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
d := d
g.Go(func() error {
return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
err := e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
if err != nil {
return err
}
return nil
})
}
@@ -236,7 +295,11 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
switch {
case cmd.Task != "":
return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
if err != nil {
return err
}
return nil
case cmd.Cmd != "":
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
e.Logger.Errf(cmd.Cmd)
@@ -248,8 +311,18 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
defer stdOut.Close()
defer stdErr.Close()
defer func() {
if _, ok := stdOut.(*os.File); !ok {
if closer, ok := stdOut.(io.Closer); ok {
closer.Close()
}
}
if _, ok := stdErr.(*os.File); !ok {
if closer, ok := stdErr.(io.Closer); ok {
closer.Close()
}
}
}()
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
Command: cmd.Cmd,

View File

@@ -236,7 +236,7 @@ func TestDeps(t *testing.T) {
for _, f := range files {
f = filepath.Join(dir, f)
if _, err := os.Stat(f); err != nil {
t.Errorf("File %s should exists", f)
t.Errorf("File %s should exist", f)
}
}
}
@@ -248,7 +248,7 @@ func TestStatus(t *testing.T) {
_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("File should not exists: %v", err)
t.Errorf("File should not exist: %v", err)
}
var buff bytes.Buffer
@@ -262,7 +262,7 @@ func TestStatus(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"}))
if _, err := os.Stat(file); err != nil {
t.Errorf("File should exists: %v", err)
t.Errorf("File should exist: %v", err)
}
e.Silent = false
@@ -273,6 +273,47 @@ func TestStatus(t *testing.T) {
}
}
func TestPrecondition(t *testing.T) {
const dir = "testdata/precondition"
var buff bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
}
// A precondition that has been met
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
if buff.String() != "" {
t.Errorf("Got Output when none was expected: %s", buff.String())
}
// A precondition that was not met
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "impossible"}))
if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
// Calling a task with a precondition in a dependency fails the task
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "depends_on_impossible"}))
if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
// Calling a task with a precondition in a cmd fails the task
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "executes_failing_task_as_cmd"}))
if buff.String() != "task: 1 != 0 obviously!\n" {
t.Errorf("Wrong output message: %s", buff.String())
}
buff.Reset()
}
func TestGenerates(t *testing.T) {
const (
srcTask = "sub/src.txt"
@@ -290,7 +331,7 @@ func TestGenerates(t *testing.T) {
path := filepath.Join(dir, task)
_ = os.Remove(path)
if _, err := os.Stat(path); err == nil {
t.Errorf("File should not exists: %v", err)
t.Errorf("File should not exist: %v", err)
}
}
@@ -311,10 +352,10 @@ func TestGenerates(t *testing.T) {
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask}))
if _, err := os.Stat(srcFile); err != nil {
t.Errorf("File should exists: %v", err)
t.Errorf("File should exist: %v", err)
}
if _, err := os.Stat(destFile); err != nil {
t.Errorf("File should exists: %v", err)
t.Errorf("File should exist: %v", err)
}
// Ensure task was not incorrectly found to be up-to-date on first run.
if buff.String() == upToDate {
@@ -371,7 +412,7 @@ func TestInit(t *testing.T) {
_ = os.Remove(file)
if _, err := os.Stat(file); err == nil {
t.Errorf("Taskfile.yml should not exists")
t.Errorf("Taskfile.yml should not exist")
}
if err := task.InitTaskfile(ioutil.Discard, dir); err != nil {
@@ -379,7 +420,7 @@ func TestInit(t *testing.T) {
}
if _, err := os.Stat(file); err != nil {
t.Errorf("Taskfile.yml should exists")
t.Errorf("Taskfile.yml should exist")
}
}
@@ -575,3 +616,65 @@ func readTestFixture(t *testing.T, dir string, file string) string {
assert.NoError(t, err, "error reading text fixture")
return string(b)
}
func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
const expected = "dir"
const dir = "testdata/" + expected
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "whereami"}))
// got should be the "dir" part of "testdata/dir"
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
}
func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
const expected = "exists"
const dir = "testdata/dir/explicit_exists"
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "whereami"}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
}
func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
const expected = "createme"
const dir = "testdata/dir/explicit_doesnt_exist/"
const toBeCreated = dir + expected
const target = "whereami"
var out bytes.Buffer
e := &task.Executor{
Dir: dir,
Stdout: &out,
Stderr: &out,
}
// Ensure that the directory to be created doesn't actually exist.
_ = os.Remove(toBeCreated)
if _, err := os.Stat(toBeCreated); err == nil {
t.Errorf("Directory should not exist: %v", err)
}
assert.NoError(t, e.Setup())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: target}))
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
assert.Equal(t, expected, got, "Mismatch in the working directory")
// Clean-up after ourselves only if no error.
_ = os.Remove(toBeCreated)
}

7
testdata/dir/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: '2'
tasks:
whereami:
cmds:
- pwd
silent: true

View File

@@ -0,0 +1,8 @@
version: '2'
tasks:
whereami:
dir: createme
cmds:
- pwd
silent: true

View File

@@ -0,0 +1,8 @@
version: '2'
tasks:
whereami:
dir: exists
cmds:
- pwd
silent: true

View File

19
testdata/precondition/Taskfile.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
version: '2'
tasks:
foo:
preconditions:
- test -f foo.txt
impossible:
preconditions:
- sh: "[ 1 = 0 ]"
msg: "1 != 0 obviously!"
depends_on_impossible:
deps:
- impossible
executes_failing_task_as_cmd:
cmds:
- task: impossible

0
testdata/precondition/foo.txt vendored Normal file
View File

View File

@@ -84,5 +84,15 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
}
}
if len(origTask.Preconditions) > 0 {
new.Preconditions = make([]*taskfile.Precondition, len(origTask.Preconditions))
for i, precond := range origTask.Preconditions {
new.Preconditions[i] = &taskfile.Precondition{
Sh: r.Replace(precond.Sh),
Msg: r.Replace(precond.Msg),
}
}
}
return &new, r.Err()
}