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:
commit
1a33f9168b
@ -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
3
go.sum
@ -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=
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
task.go
2
task.go
@ -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
|
||||||
|
23
task_test.go
23
task_test.go
@ -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) {
|
||||||
|
14
testdata/checksum/Taskfile.yml
vendored
14
testdata/checksum/Taskfile.yml
vendored
@ -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}}'
|
||||||
|
22
variables.go
22
variables.go
@ -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()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user