Repository: go-task/task (2026-03-05T01:56:55Z)

Issue:
method: timestamp does not run when generates: path is missing
```  
my-task:
    desc: does useful things
    cmds:
      - echo "I ran yo"
    sources:
      - [insert some path to source files]
    generates:
      - [insert some path to generated file]
    method: timestamp  
```

- Task version: Task version: v3.26.0 (h1:NfYktTh/XXJUFtUFdle+BZFtvGlmhk4Q7YfoeOBLPlE=)
- Operating system: Darwin
- Experiments enabled: no


If i add method timestamp then the task is not run when the output is not there... (like after a clean operation)


Discussion:
I also experienced this issue using the latest version. Every condition is the same, except that I'm running this on Linux (Manjaro).
+1 to this.

@andreynering — could we consider adding a check to the [`IsUpToDate`](https://github.com/go-task/task/blob/a459eeaabbb15989cae2ca4ad9dd63c471003a46/internal/fingerprint/sources_timestamp.go#L26) function to mark a task as outdated if **any of the `generates` files are missing**?
> could we consider adding a check to the `IsUpToDate`

Any ideas how this would work with glob patterns (generate doesn't just have to be a list of explicit files).

Or, perhaps the file existence check can _only_ work if you're using explicit list (non-glob) file specifications.

All of these have surprising (in a bad way) behavior, though the current behavior for the normal case is very broken.


Have fix for this just waiting for a review https://github.com/go-task/task/pull/2716



Patch:
diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go
index 16a98c1640..f1108e111c 100644
--- a/internal/fingerprint/sources_checksum.go
+++ b/internal/fingerprint/sources_checksum.go
@@ -53,6 +53,7 @@ func (checker *ChecksumChecker) IsUpToDate(t *ast.Task) (bool, error) {
 	if len(t.Generates) > 0 {
 		// For each specified 'generates' field, check whether the files actually exist
 		for _, g := range t.Generates {
+			// Exclusion patterns don't represent output files; skip them.
 			if g.Negate {
 				continue
 			}
diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go
index b1a6f299d5..258d9386d9 100644
--- a/internal/fingerprint/sources_timestamp.go
+++ b/internal/fingerprint/sources_timestamp.go
@@ -32,6 +32,28 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
 	if err != nil {
 		return false, nil
 	}
+
+	// If generates are declared, ensure they all exist. A missing generated
+	// file means the task must run regardless of timestamps.
+	if len(t.Generates) > 0 {
+		for _, g := range t.Generates {
+			// Exclusion patterns don't represent output files; skip them.
+			if g.Negate {
+				continue
+			}
+			files, err := glob(t.Dir, g.Glob)
+			if os.IsNotExist(err) {
+				return false, nil
+			}
+			if err != nil {
+				return false, err
+			}
+			if len(files) == 0 {
+				return false, nil
+			}
+		}
+	}
+
 	generates, err := Globs(t.Dir, t.Generates)
 	if err != nil {
 		return false, nil


Test patch:
diff --git a/task_test.go b/task_test.go
index a09e496f8b..cc1a7538d1 100644
--- a/task_test.go
+++ b/task_test.go
@@ -555,6 +555,104 @@ func TestStatusChecksum(t *testing.T) { // nolint:paralleltest // cannot run in
 	}
 }
 
+// TestStatusTimestamp is a regression test for https://github.com/go-task/task/issues/1230.
+// When using method: timestamp, deleting a generated file should cause the task to re-run,
+// not be skipped because the timestamp file is still present.
+func TestStatusTimestamp(t *testing.T) { // nolint:paralleltest // cannot run in parallel
+	const dir = "testdata/timestamp"
+
+	generatedFile := filepathext.SmartJoin(dir, "generated.txt")
+	tempDir := task.TempDir{
+		Remote:      filepathext.SmartJoin(dir, ".task"),
+		Fingerprint: filepathext.SmartJoin(dir, ".task"),
+	}
+
+	// Clean up any state from previous runs.
+	_ = os.Remove(generatedFile)
+	_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
+
+	var buff bytes.Buffer
+	e := task.NewExecutor(
+		task.WithDir(dir),
+		task.WithStdout(&buff),
+		task.WithStderr(&buff),
+		task.WithTempDir(tempDir),
+	)
+	require.NoError(t, e.Setup())
+
+	// First run: task should execute and create generated.txt.
+	require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
+	_, err := os.Stat(generatedFile)
+	require.NoError(t, err, "generated.txt should exist after first run")
+	buff.Reset()
+
+	// Second run: task should be up to date.
+	require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
+	assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
+	buff.Reset()
+
+	// Delete the generated file (simulate a clean), but leave the timestamp file.
+	require.NoError(t, os.Remove(generatedFile))
+	_, err = os.Stat(generatedFile)
+	require.Error(t, err, "generated.txt should be gone")
+
+	// Third run: task MUST re-run because generated.txt is missing.
+	// This is the regression: previously the task was incorrectly skipped.
+	require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
+	assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing")
+	_, err = os.Stat(generatedFile)
+	require.NoError(t, err, "generated.txt should be recreated after third run")
+}
+
+// TestStatusChecksumMissingGenerated is a regression test for https://github.com/go-task/task/issues/1230.
+// When using method: checksum, deleting a generated file should cause the task to re-run,
+// not be skipped because the checksum file still matches.
+func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest // cannot run in parallel
+	const dir = "testdata/checksum"
+
+	generatedFile := filepathext.SmartJoin(dir, "generated.txt")
+	tempDir := task.TempDir{
+		Remote:      filepathext.SmartJoin(dir, ".task"),
+		Fingerprint: filepathext.SmartJoin(dir, ".task"),
+	}
+
+	// Clean up any state from previous runs.
+	_ = os.Remove(generatedFile)
+	_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
+
+	var buff bytes.Buffer
+	e := task.NewExecutor(
+		task.WithDir(dir),
+		task.WithStdout(&buff),
+		task.WithStderr(&buff),
+		task.WithTempDir(tempDir),
+	)
+	require.NoError(t, e.Setup())
+
+	// First run: task should execute and create generated.txt.
+	require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
+	_, err := os.Stat(generatedFile)
+	require.NoError(t, err, "generated.txt should exist after first run")
+	buff.Reset()
+
+	// Second run: task should be up to date.
+	require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
+	assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
+	buff.Reset()
+
+	// Delete the generated file (simulate a clean), but leave the checksum file.
+	require.NoError(t, os.Remove(generatedFile))
+	_, err = os.Stat(generatedFile)
+	require.Error(t, err, "generated.txt should be gone")
+
+	// Third run: task MUST re-run because generated.txt is missing.
+	// This is the regression: previously the task was incorrectly skipped.
+	require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
+	assert.NotContains(t, buff.String(), "is up to date", "task should re-run when generated file is missing")
+	_, err = os.Stat(generatedFile)
+	require.NoError(t, err, "generated.txt should be recreated after third run")
+}
+
 func TestStatusVariables(t *testing.T) {
 	t.Parallel()
 
diff --git a/testdata/timestamp/.gitignore b/testdata/timestamp/.gitignore
new file mode 100644
index 0000000000..8443a36988
--- /dev/null
+++ b/testdata/timestamp/.gitignore
@@ -0,0 +1,2 @@
+.task
+generated.txt
diff --git a/testdata/timestamp/Taskfile.yml b/testdata/timestamp/Taskfile.yml
new file mode 100644
index 0000000000..450ed56021
--- /dev/null
+++ b/testdata/timestamp/Taskfile.yml
@@ -0,0 +1,11 @@
+version: '3'
+
+tasks:
+  build:
+    cmds:
+      - cp ./source.txt ./generated.txt
+    sources:
+      - ./source.txt
+    generates:
+      - ./generated.txt
+    method: timestamp
diff --git a/testdata/timestamp/source.txt b/testdata/timestamp/source.txt
new file mode 100644
index 0000000000..3a4e1f3cac
--- /dev/null
+++ b/testdata/timestamp/source.txt
@@ -0,0 +1,1 @@
+hello from source