diff --git a/docs/usage.md b/docs/usage.md index d8ffbd90..4e657149 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -340,6 +340,16 @@ tasks: - test -f directory/file2.txt ``` +Normally, you would use either `status` or `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. + You can use `--force` or `-f` if you want to force a task to run even when up-to-date. diff --git a/internal/status/checksum.go b/internal/status/checksum.go index 44331047..f33c1d22 100644 --- a/internal/status/checksum.go +++ b/internal/status/checksum.go @@ -46,6 +46,10 @@ func (c *Checksum) IsUpToDate() (bool, error) { return oldMd5 == newMd5, nil } +func (t *Checksum) Kind() string { + return "checksum" +} + func (c *Checksum) checksum(files ...string) (string, error) { h := md5.New() @@ -73,6 +77,11 @@ func (c *Checksum) checksum(files ...string) (string, error) { return fmt.Sprintf("%x", h.Sum(nil)), nil } +// Value implements the Chcker Interface +func (c *Checksum) Value() (string, error) { + return c.checksum() +} + // OnError implements the Checker interface func (c *Checksum) OnError() error { return os.Remove(c.checksumFilePath()) diff --git a/internal/status/none.go b/internal/status/none.go index 01e35060..c67dc437 100644 --- a/internal/status/none.go +++ b/internal/status/none.go @@ -8,6 +8,15 @@ func (None) IsUpToDate() (bool, error) { return false, nil } +// Value implements the Checker interface +func (None) Value() (string, error) { + return "", nil +} + +func (None) Kind() string { + return "none" +} + // OnError implements the Checker interface func (None) OnError() error { return nil diff --git a/internal/status/status.go b/internal/status/status.go index 320ca8a6..45388b49 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -9,5 +9,7 @@ var ( // Checker is an interface that checks if the status is up-to-date type Checker interface { IsUpToDate() (bool, error) + Value() (string, error) OnError() error + Kind() string } diff --git a/internal/status/timestamp.go b/internal/status/timestamp.go index 62b9aafb..a97549fc 100644 --- a/internal/status/timestamp.go +++ b/internal/status/timestamp.go @@ -1,6 +1,7 @@ package status import ( + "fmt" "os" "time" ) @@ -41,6 +42,29 @@ func (t *Timestamp) IsUpToDate() (bool, error) { return !generatesMinTime.Before(sourcesMaxTime), nil } +func (t *Timestamp) Kind() string { + return "timestamp" +} + +// Value implements the Checker Interface +func (t *Timestamp) Value() (string, error) { + sources, err := glob(t.Dir, t.Sources) + if err != nil { + return "", err + } + + sourcesMaxTime, err := getMaxTime(sources...) + if err != nil { + return "", err + } + + if sourcesMaxTime.IsZero() { + return "0", nil + } + + return fmt.Sprintf("%d", sourcesMaxTime.Unix()), nil +} + func getMinTime(files ...string) (time.Time, error) { var t time.Time for _, f := range files { diff --git a/status.go b/status.go index 7ebb5f84..74f25489 100644 --- a/status.go +++ b/status.go @@ -32,7 +32,7 @@ func (e *Executor) isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, return e.isTaskUpToDateStatus(ctx, t) } - checker, err := e.getStatusChecker(t) + checker, err := e.GetStatusChecker(t) if err != nil { return false, err } @@ -41,14 +41,14 @@ func (e *Executor) isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, } func (e *Executor) statusOnError(t *taskfile.Task) error { - checker, err := e.getStatusChecker(t) + checker, err := e.GetStatusChecker(t) if err != nil { return err } return checker.OnError() } -func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) { +func (e *Executor) GetStatusChecker(t *taskfile.Task) (status.Checker, error) { switch t.Method { case "", "timestamp": return &status.Timestamp{ diff --git a/task_test.go b/task_test.go index 1b97fbb6..b732bfe5 100644 --- a/task_test.go +++ b/task_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/go-task/task/v2" + "github.com/go-task/task/v2/internal/logger" "github.com/go-task/task/v2/internal/taskfile" "github.com/mitchellh/go-homedir" @@ -388,12 +389,20 @@ func TestStatusChecksum(t *testing.T) { } var buff bytes.Buffer + + logCapturer := logger.Logger{ + Stdout: &buff, + Stderr: &buff, + Verbose: true, + } + e := task.Executor{ Dir: dir, Stdout: &buff, Stderr: &buff, } assert.NoError(t, e.Setup()) + e.Logger = &logCapturer assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"})) for _, f := range files { @@ -404,6 +413,18 @@ func TestStatusChecksum(t *testing.T) { 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()) + + 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()) + assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build-with-timestamp"})) + assert.Contains(t, buff.String(), ts) } func TestInit(t *testing.T) { diff --git a/testdata/checksum/Taskfile.yml b/testdata/checksum/Taskfile.yml index 4f4a2362..b9d5f6e9 100644 --- a/testdata/checksum/Taskfile.yml +++ b/testdata/checksum/Taskfile.yml @@ -7,3 +7,16 @@ build: generates: - ./generated.txt method: checksum + +build-with-checksum: + sources: + - ./source.txt + method: checksum + status: + - echo "{{.CHECKSUM}}" + +build-with-timestamp: + sources: + - ./source.txt + status: + - echo "{{.TIMESTAMP}}" diff --git a/variables.go b/variables.go index 9c227372..59b1ac81 100644 --- a/variables.go +++ b/variables.go @@ -1,7 +1,9 @@ package task import ( + "io/ioutil" "path/filepath" + "strings" "github.com/go-task/task/v2/internal/execext" "github.com/go-task/task/v2/internal/taskfile" @@ -20,6 +22,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { if err != nil { return nil, err } + r := templater.Templater{Vars: vars} new := taskfile.Task{ @@ -27,7 +30,6 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { Desc: r.Replace(origTask.Desc), Sources: r.ReplaceSlice(origTask.Sources), Generates: r.ReplaceSlice(origTask.Generates), - Status: r.ReplaceSlice(origTask.Status), Dir: r.Replace(origTask.Dir), Vars: nil, Env: nil, @@ -62,6 +64,31 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { new.Env[k] = taskfile.Var{Static: static} } + if len(origTask.Status) > 0 { + + e := &Executor{ + Dir: new.Dir, + Stdout: ioutil.Discard, + Stderr: ioutil.Discard, + Dry: true, + } + + 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{Static: value} + + statusTemplater := templater.Templater{Vars: vars} + new.Status = statusTemplater.ReplaceSlice(origTask.Status) + } + if len(origTask.Cmds) > 0 { new.Cmds = make([]*taskfile.Cmd, len(origTask.Cmds)) for i, cmd := range origTask.Cmds {