Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,93 @@ func TestStatus(t *testing.T) {
)
}

func TestInheritOutdated(t *testing.T) {
t.Parallel()

const dir = "testdata/inherit_outdated"

var buf bytes.Buffer

// Test 1: Task without inherit_outdated should pass even if dep is outdated
e1 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e1.Setup())
ctx := t.Context()
err := e1.Status(ctx, &task.Call{Task: "task-without-inherit-outdated"})
require.NoError(t, err, "task-without-inherit-outdated itself is up-to-date, so without inherit_outdated it should pass")

// Test 2: Task with inherit_outdated on bad dep should fail
buf.Reset()
e2 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e2.Setup())
err = e2.Status(ctx, &task.Call{Task: "task-with-inherit-outdated-bad"})
require.Error(t, err, "task-with-inherit-outdated-bad has a dependency with inherit_outdated that is not up-to-date")
require.Contains(t, err.Error(), "not-up-to-date-leaf", "error should mention the dependency that failed")

// Test 3: Task with inherit_outdated on good deps should pass
buf.Reset()
e3 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e3.Setup())
err = e3.Status(ctx, &task.Call{Task: "task-with-inherit-outdated-good"})
require.NoError(t, err, "task-with-inherit-outdated-good and all its inherit_outdated dependencies are up-to-date")

// Test 4: Nested inherit_outdated should propagate
buf.Reset()
e4 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e4.Setup())
err = e4.Status(ctx, &task.Call{Task: "nested-inherit-outdated"})
require.Error(t, err, "nested-inherit-outdated has a transitive dependency with inherit_outdated that is not up-to-date")
require.Contains(t, err.Error(), "not-up-to-date", "error should mention something is not up-to-date")

// Test 5: Mixed deps (some with inherit_outdated, some without) - only checks those with inherit_outdated
buf.Reset()
e5 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e5.Setup())
err = e5.Status(ctx, &task.Call{Task: "mixed-inherit-outdated"})
require.NoError(t, err, "mixed-inherit-outdated should pass because only up-to-date-leaf has inherit_outdated")

// Test 6: Leaf task that is up-to-date should pass
buf.Reset()
e6 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e6.Setup())
err = e6.Status(ctx, &task.Call{Task: "up-to-date-leaf"})
require.NoError(t, err, "up-to-date-leaf is up-to-date")

// Test 7: Leaf task that is not up-to-date should fail
buf.Reset()
e7 := task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buf),
task.WithStderr(&buf),
)
require.NoError(t, e7.Setup())
err = e7.Status(ctx, &task.Call{Task: "not-up-to-date-leaf"})
require.Error(t, err, "not-up-to-date-leaf is not up-to-date")
}

func TestPrecondition(t *testing.T) {
t.Parallel()
const dir = "testdata/precondition"
Expand Down
100 changes: 84 additions & 16 deletions status.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,103 @@ import (
"github.com/go-task/task/v3/taskfile/ast"
)

// Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(ctx context.Context, calls ...*Call) error {
for _, call := range calls {
// checkTaskStatus checks if a single task is up-to-date
func (e *Executor) checkTaskStatus(ctx context.Context, call *Call) (bool, *ast.Task, error) {
// Compile the task
t, err := e.CompiledTask(call)
if err != nil {
return false, nil, err
}

// 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),
fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger),
)
if err != nil {
return false, t, err
}

return isUpToDate, t, nil
}

// traverseInheritOutdated traverses dependencies marked with inherit_outdated
// and calls yield for each one. Similar to traverse() but only follows inherit_outdated deps.
func (e *Executor) traverseInheritOutdated(ctx context.Context, task *ast.Task, visited map[string]bool, yield func(*ast.Task) error) error {
if visited == nil {
visited = make(map[string]bool)
}

// Avoid infinite loops
if visited[task.Task] {
return nil
}
visited[task.Task] = true

// Only traverse dependencies marked with inherit_outdated
for _, dep := range task.Deps {
if !dep.InheritOutdated || dep.Task == "" {
continue
}

// Compile the task
t, err := e.CompiledTask(call)
// Compile the dependency task
depTask, err := e.CompiledTask(&Call{Task: dep.Task, Vars: dep.Vars})
if err != nil {
return err
}

// Get the fingerprinting method to use
method := e.Taskfile.Method
if t.Method != "" {
method = t.Method
// Recursively traverse the dependency's inherit_outdated deps
if err := e.traverseInheritOutdated(ctx, depTask, visited, yield); err != nil {
return err
}

// Yield this dependency
if err := yield(depTask); err != nil {
return err
}
}

return nil
}

// Check if the task is up-to-date
isUpToDate, err := fingerprint.IsTaskUpToDate(ctx, t,
fingerprint.WithMethod(method),
fingerprint.WithTempDir(e.TempDir.Fingerprint),
fingerprint.WithDry(e.Dry),
fingerprint.WithLogger(e.Logger),
)
// Status returns an error if any the of given tasks is not up-to-date
func (e *Executor) Status(ctx context.Context, calls ...*Call) error {
for _, call := range calls {
// Check if the task itself is up-to-date
isUpToDate, t, err := e.checkTaskStatus(ctx, call)
if err != nil {
return err
}
if !isUpToDate {
return fmt.Errorf(`task: Task "%s" is not up-to-date`, t.Name())
}

// Check dependencies with inherit_outdated attribute
var outdatedDep string
err = e.traverseInheritOutdated(ctx, t, nil, func(depTask *ast.Task) error {
isUpToDate, _, err := e.checkTaskStatus(ctx, &Call{Task: depTask.Task})
if err != nil {
return err
}
if !isUpToDate && outdatedDep == "" {
outdatedDep = depTask.Task
}
return nil
})
if err != nil {
return err
}
if outdatedDep != "" {
return fmt.Errorf(`task: Task "%s" is not up-to-date because dependency "%s" is not up-to-date`, t.Name(), outdatedDep)
}
}
return nil
}
Expand Down
28 changes: 16 additions & 12 deletions taskfile/ast/dep.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ import (

// Dep is a task dependency
type Dep struct {
Task string
For *For
Vars *Vars
Silent bool
Task string
For *For
Vars *Vars
Silent bool
InheritOutdated bool
}

func (d *Dep) DeepCopy() *Dep {
if d == nil {
return nil
}
return &Dep{
Task: d.Task,
For: d.For.DeepCopy(),
Vars: d.Vars.DeepCopy(),
Silent: d.Silent,
Task: d.Task,
For: d.For.DeepCopy(),
Vars: d.Vars.DeepCopy(),
Silent: d.Silent,
InheritOutdated: d.InheritOutdated,
}
}

Expand All @@ -39,10 +41,11 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {

case yaml.MappingNode:
var taskCall struct {
Task string
For *For
Vars *Vars
Silent bool
Task string `yaml:"task"`
For *For
Vars *Vars
Silent bool
InheritOutdated bool `yaml:"inherit_outdated"`
}
if err := node.Decode(&taskCall); err != nil {
return errors.NewTaskfileDecodeError(err, node)
Expand All @@ -51,6 +54,7 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
d.For = taskCall.For
d.Vars = taskCall.Vars
d.Silent = taskCall.Silent
d.InheritOutdated = taskCall.InheritOutdated
return nil
}

Expand Down
2 changes: 2 additions & 0 deletions testdata/inherit_outdated/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.txt
.task/
69 changes: 69 additions & 0 deletions testdata/inherit_outdated/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
version: '3'

tasks:
# Leaf task that is always up-to-date
up-to-date-leaf:
cmds:
- echo "up-to-date-leaf"
status:
- test 1 = 1 # Always passes

# Leaf task that is never up-to-date
not-up-to-date-leaf:
cmds:
- echo "not-up-to-date-leaf"
status:
- test 1 = 0 # Always fails

# Parent task with inherit_outdated on a bad dependency
# The task itself is up-to-date, but should be marked outdated due to dep
task-with-inherit-outdated-bad:
deps:
- task: up-to-date-leaf
- task: not-up-to-date-leaf
inherit_outdated: true
cmds:
- echo "task-with-inherit-outdated-bad"
status:
- test 1 = 1 # Itself is up-to-date

# Parent task with inherit_outdated on good dependencies
task-with-inherit-outdated-good:
deps:
- task: up-to-date-leaf
inherit_outdated: true
cmds:
- echo "task-with-inherit-outdated-good"
status:
- test 1 = 1 # Itself and all deps are up-to-date

# Parent task WITHOUT inherit_outdated, even though dep is bad
# Should pass status check (only checks itself)
task-without-inherit-outdated:
deps:
- task: not-up-to-date-leaf
cmds:
- echo "task-without-inherit-outdated"
status:
- test 1 = 1 # Itself is up-to-date

# Nested: parent depends on a task that has inherit_outdated on a bad dep
nested-inherit-outdated:
deps:
- task: task-with-inherit-outdated-bad
inherit_outdated: true
cmds:
- echo "nested-inherit-outdated"
status:
- test 1 = 1 # Itself is up-to-date

# Multiple deps with mixed inherit_outdated
mixed-inherit-outdated:
deps:
- task: up-to-date-leaf
inherit_outdated: true
- task: not-up-to-date-leaf # No inherit_outdated, so ignored
cmds:
- echo "mixed-inherit-outdated"
status:
- test 1 = 1