1
0
mirror of https://github.com/go-task/task.git synced 2025-04-23 12:18:57 +02:00

Merge branch 'report-timestamp-to-status' of https://github.com/CypherpunkArmory/task into CypherpunkArmory-report-timestamp-to-status

This commit is contained in:
Andrey Nering 2019-09-01 21:44:23 -03:00
commit 1a33f9168b
12 changed files with 154 additions and 14 deletions

View File

@ -266,6 +266,8 @@ The above syntax is also supported in `deps`.
## Prevent unnecessary work ## Prevent unnecessary work
### By fingerprinting locally generated files and their sources
If a task generates something, you can inform Task the source and generated If a task generates something, you can inform Task the source and generated
files, so Task will prevent to run them if not necessary. files, so Task will prevent to run them if not necessary.
@ -321,6 +323,9 @@ tasks:
> TIP: method `none` skips any validation and always run the task. > TIP: method `none` skips any validation and always run the task.
### Using programmatic checks to indicate a task is up to date.
Alternatively, you can inform a sequence of tests as `status`. If no error Alternatively, you can inform a sequence of tests as `status`. If no error
is returned (exit status 0), the task is considered up-to-date: is returned (exit status 0), the task is considered up-to-date:
@ -340,15 +345,35 @@ tasks:
- test -f directory/file2.txt - test -f directory/file2.txt
``` ```
Normally, you would use `sources` in combination with
`generates` - but for tasks that generate remote artifacts (Docker images,
deploys, CD releases) the checksum source and timestamps require either
access to the artifact or for an out-of-band refresh of the `.checksum`
fingerprint file.
Two special variables `{{.CHECKSUM}}` and `{{.TIMESTAMP}}` are available
for interpolation within `status` commands, depending on the method assigned
to fingerprint the sources. Only `source` globs are fingerprinted.
Note that the `{{.TIMESTAMP}}` variable is a "live" Go time struct, and can be
formatted using any of the methods that `Time` responds to.
See [the Go Time documentation](https://golang.org/pkg/time/) for more information.
You can use `--force` or `-f` if you want to force a task to run even when You can use `--force` or `-f` if you want to force a task to run even when
up-to-date. up-to-date.
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
the tasks are not up-to-date. the tasks are not up-to-date.
If you need a certain set of conditions to be _true_ you can use the ### Using programmatic checks to cancel execution of an task and it's dependencies
`preconditions` stanza. `preconditions` are very similar to `status`
lines except they support `sh` expansion and they SHOULD all return 0. In addition to `status` checks, there are also `preconditions` checks, which are
the logical inverse of `status` checks. That is, if you need a certain set of
conditions to be _true_ you can use the `preconditions` stanza.
`preconditions` are similar to `status` lines except they support `sh`
expansion and they SHOULD all return 0.
```yaml ```yaml
version: '2' version: '2'

3
go.sum
View File

@ -30,8 +30,11 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 h1:rVTAlhYa4+lCfNxmAIEOGQRoD23UqP72M3+rSWVGDTg= golang.org/x/crypto v0.0.0-20180830192347-182538f80094 h1:rVTAlhYa4+lCfNxmAIEOGQRoD23UqP72M3+rSWVGDTg=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=

View File

@ -84,11 +84,21 @@ func (c *Checksum) checksum(files ...string) (string, error) {
return fmt.Sprintf("%x", h.Sum(nil)), nil 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 // OnError implements the Checker interface
func (c *Checksum) OnError() error { func (c *Checksum) OnError() error {
return os.Remove(c.checksumFilePath()) return os.Remove(c.checksumFilePath())
} }
// Kind implements the Checker Interface
func (t *Checksum) Kind() string {
return "checksum"
}
func (c *Checksum) checksumFilePath() string { func (c *Checksum) checksumFilePath() string {
return filepath.Join(c.Dir, ".task", "checksum", c.normalizeFilename(c.Task)) return filepath.Join(c.Dir, ".task", "checksum", c.normalizeFilename(c.Task))
} }

View File

@ -8,6 +8,15 @@ func (None) IsUpToDate() (bool, error) {
return false, nil 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 // OnError implements the Checker interface
func (None) OnError() error { func (None) OnError() error {
return nil return nil

View File

@ -9,5 +9,7 @@ var (
// Checker is an interface that checks if the status is up-to-date // Checker is an interface that checks if the status is up-to-date
type Checker interface { type Checker interface {
IsUpToDate() (bool, error) IsUpToDate() (bool, error)
Value() (interface{}, error)
OnError() error OnError() error
Kind() string
} }

View File

@ -41,6 +41,29 @@ func (t *Timestamp) IsUpToDate() (bool, error) {
return !generatesMinTime.Before(sourcesMaxTime), nil return !generatesMinTime.Before(sourcesMaxTime), nil
} }
func (t *Timestamp) Kind() string {
return "timestamp"
}
// Value implements the Checker Interface
func (t *Timestamp) Value() (interface{}, error) {
sources, err := globs(t.Dir, t.Sources)
if err != nil {
return time.Now(), err
}
sourcesMaxTime, err := getMaxTime(sources...)
if err != nil {
return time.Now(), err
}
if sourcesMaxTime.IsZero() {
return time.Unix(0, 0), nil
}
return sourcesMaxTime, nil
}
func getMinTime(files ...string) (time.Time, error) { func getMinTime(files ...string) (time.Time, error) {
var t time.Time var t time.Time
for _, f := range files { for _, f := range files {

View File

@ -13,24 +13,30 @@ var (
// Vars is a string[string] variables map. // Vars is a string[string] variables map.
type Vars map[string]Var type Vars map[string]Var
// ToStringMap converts Vars to a string map containing only the static // ToCacheMap converts Vars to a map containing only the static
// variables // variables
func (vs Vars) ToStringMap() (m map[string]string) { func (vs Vars) ToCacheMap() (m map[string](interface{})) {
m = make(map[string]string, len(vs)) m = make(map[string](interface{}), len(vs))
for k, v := range vs { for k, v := range vs {
if v.Sh != "" { if v.Sh != "" {
// Dynamic variable is not yet resolved; trigger // Dynamic variable is not yet resolved; trigger
// <no value> to be used in templates. // <no value> to be used in templates.
continue continue
} }
if v.Live != nil {
m[k] = v.Live
} else {
m[k] = v.Static m[k] = v.Static
} }
}
return return
} }
// Var represents either a static or dynamic variable. // Var represents either a static or dynamic variable.
type Var struct { type Var struct {
Static string Static string
Live interface{}
Sh string Sh string
} }

View File

@ -14,10 +14,14 @@ import (
type Templater struct { type Templater struct {
Vars taskfile.Vars Vars taskfile.Vars
strMap map[string]string cacheMap map[string](interface{})
err error err error
} }
func (r *Templater) RefreshCacheMap() {
r.cacheMap = r.Vars.ToCacheMap()
}
func (r *Templater) Replace(str string) string { func (r *Templater) Replace(str string) string {
if r.err != nil || str == "" { if r.err != nil || str == "" {
return "" return ""
@ -29,12 +33,12 @@ func (r *Templater) Replace(str string) string {
return "" return ""
} }
if r.strMap == nil { if r.cacheMap == nil {
r.strMap = r.Vars.ToStringMap() r.cacheMap = r.Vars.ToCacheMap()
} }
var b bytes.Buffer var b bytes.Buffer
if err = templ.Execute(&b, r.strMap); err != nil { if err = templ.Execute(&b, r.cacheMap); err != nil {
r.err = err r.err = err
return "" return ""
} }
@ -62,6 +66,7 @@ func (r *Templater) ReplaceVars(vars taskfile.Vars) taskfile.Vars {
for k, v := range vars { for k, v := range vars {
new[k] = taskfile.Var{ new[k] = taskfile.Var{
Static: r.Replace(v.Static), Static: r.Replace(v.Static),
Live: v.Live,
Sh: r.Replace(v.Sh), Sh: r.Replace(v.Sh),
} }
} }

View File

@ -348,7 +348,7 @@ func getEnviron(t *taskfile.Task) []string {
} }
environ := os.Environ() environ := os.Environ()
for k, v := range t.Env.ToStringMap() { for k, v := range t.Env.ToCacheMap() {
environ = append(environ, fmt.Sprintf("%s=%s", k, v)) environ = append(environ, fmt.Sprintf("%s=%s", k, v))
} }
return environ return environ

View File

@ -11,6 +11,7 @@ import (
"testing" "testing"
"github.com/go-task/task/v2" "github.com/go-task/task/v2"
"github.com/go-task/task/v2/internal/logger"
"github.com/go-task/task/v2/internal/taskfile" "github.com/go-task/task/v2/internal/taskfile"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
@ -351,12 +352,20 @@ func TestStatusChecksum(t *testing.T) {
} }
var buff bytes.Buffer var buff bytes.Buffer
logCapturer := logger.Logger{
Stdout: &buff,
Stderr: &buff,
Verbose: true,
}
e := task.Executor{ e := task.Executor{
Dir: dir, Dir: dir,
Stdout: &buff, Stdout: &buff,
Stderr: &buff, Stderr: &buff,
} }
assert.NoError(t, e.Setup()) assert.NoError(t, e.Setup())
e.Logger = &logCapturer
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
for _, f := range files { for _, f := range files {
@ -367,6 +376,20 @@ func TestStatusChecksum(t *testing.T) {
buff.Reset() buff.Reset()
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"})) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String()) assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
buff.Reset()
e.Silent = false
e.Verbose = true
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-with-checksum"}))
assert.Contains(t, buff.String(), "d41d8cd98f00b204e9800998ecf8427e")
buff.Reset()
inf, _ := os.Stat(filepath.Join(dir, "source.txt"))
ts := fmt.Sprintf("%d", inf.ModTime().Unix())
tf := fmt.Sprintf("%s", inf.ModTime())
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-with-timestamp"}))
assert.Contains(t, buff.String(), ts)
assert.Contains(t, buff.String(), tf)
} }
func TestInit(t *testing.T) { func TestInit(t *testing.T) {

View File

@ -10,3 +10,17 @@ tasks:
generates: generates:
- ./generated.txt - ./generated.txt
method: checksum method: checksum
build-with-checksum:
sources:
- ./source.txt
method: checksum
status:
- echo "{{.CHECKSUM}}"
build-with-timestamp:
sources:
- ./source.txt
status:
- echo '{{.TIMESTAMP.Unix }}'
- echo '{{.TIMESTAMP}}'

View File

@ -2,6 +2,7 @@ package task
import ( import (
"path/filepath" "path/filepath"
"strings"
"github.com/go-task/task/v2/internal/execext" "github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/taskfile" "github.com/go-task/task/v2/internal/taskfile"
@ -20,6 +21,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := templater.Templater{Vars: vars} r := templater.Templater{Vars: vars}
new := taskfile.Task{ new := taskfile.Task{
@ -27,7 +29,6 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
Desc: r.Replace(origTask.Desc), Desc: r.Replace(origTask.Desc),
Sources: r.ReplaceSlice(origTask.Sources), Sources: r.ReplaceSlice(origTask.Sources),
Generates: r.ReplaceSlice(origTask.Generates), Generates: r.ReplaceSlice(origTask.Generates),
Status: r.ReplaceSlice(origTask.Status),
Dir: r.Replace(origTask.Dir), Dir: r.Replace(origTask.Dir),
Vars: nil, Vars: nil,
Env: nil, Env: nil,
@ -94,5 +95,24 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
} }
} }
if len(origTask.Status) > 0 {
checker, err := e.getStatusChecker(&new)
if err != nil {
return nil, err
}
value, err := checker.Value()
if err != nil {
return nil, err
}
vars[strings.ToUpper(checker.Kind())] = taskfile.Var{Live: value}
// Adding new variables, requires us to refresh the templaters
// cache of the the values manually
r.RefreshCacheMap()
new.Status = r.ReplaceSlice(origTask.Status)
}
return &new, r.Err() return &new, r.Err()
} }