Skip to content

Commit 3b669e3

Browse files
authored
fix: use frontmatter hash instead of mtime to determine compiled status in gh aw list (#25364)
1 parent 9fb1155 commit 3b669e3

3 files changed

Lines changed: 99 additions & 9 deletions

File tree

pkg/cli/list_workflows_command.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool,
133133
// Build workflow list
134134
var workflows []WorkflowListItem
135135

136+
// Shared import cache across all iterations to avoid re-creating it for every workflow
137+
importCache := parser.NewImportCache("")
138+
136139
for _, file := range mdFiles {
137140
name := extractWorkflowNameFromPath(file)
138141

@@ -161,14 +164,7 @@ func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool,
161164
compiled := "N/A"
162165

163166
if _, err := os.Stat(lockFile); err == nil {
164-
// Check if up to date
165-
mdStat, _ := os.Stat(file)
166-
lockStat, _ := os.Stat(lockFile)
167-
if mdStat.ModTime().After(lockStat.ModTime()) {
168-
compiled = "No"
169-
} else {
170-
compiled = "Yes"
171-
}
167+
compiled = isCompiledUpToDateWithCache(file, lockFile, importCache)
172168
}
173169

174170
// Extract "on" field and labels from frontmatter

pkg/cli/list_workflows_command_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
package cli
44

55
import (
6+
"bytes"
67
"encoding/json"
8+
"io"
79
"os"
810
"path/filepath"
911
"testing"
1012

13+
"github.com/github/gh-aw/pkg/parser"
1114
"github.com/stretchr/testify/assert"
1215
"github.com/stretchr/testify/require"
1316
)
@@ -120,3 +123,88 @@ func TestNewListCommand(t *testing.T) {
120123
labelFlag := cmd.Flags().Lookup("label")
121124
assert.NotNil(t, labelFlag, "Command should have --label flag")
122125
}
126+
127+
// captureListOutput calls RunListWorkflows and returns the captured stdout as a string.
128+
func captureListOutput(t *testing.T, dir, pattern string) string {
129+
t.Helper()
130+
oldStdout := os.Stdout
131+
r, w, err := os.Pipe()
132+
require.NoError(t, err, "Should create pipe")
133+
os.Stdout = w
134+
135+
runErr := RunListWorkflows("", dir, pattern, false, true, "")
136+
137+
w.Close()
138+
os.Stdout = oldStdout
139+
140+
var buf bytes.Buffer
141+
_, _ = io.Copy(&buf, r)
142+
require.NoError(t, runErr, "RunListWorkflows should not error")
143+
return buf.String()
144+
}
145+
146+
func TestRunListWorkflows_CompiledField(t *testing.T) {
147+
tmpDir := t.TempDir()
148+
149+
// Minimal valid workflow markdown
150+
mdContent := `---
151+
name: test-workflow
152+
on:
153+
workflow_dispatch:
154+
engine: copilot
155+
---
156+
157+
# Test Workflow
158+
159+
A simple test workflow.
160+
`
161+
mdPath := filepath.Join(tmpDir, "test-workflow.md")
162+
require.NoError(t, os.WriteFile(mdPath, []byte(mdContent), 0644), "Should write test workflow")
163+
164+
// Compute the real frontmatter hash
165+
cache := parser.NewImportCache("")
166+
realHash, err := parser.ComputeFrontmatterHashFromFile(mdPath, cache)
167+
require.NoError(t, err, "Should compute frontmatter hash")
168+
169+
t.Run("compiled Yes when hash matches", func(t *testing.T) {
170+
lockPath := filepath.Join(tmpDir, "test-workflow.lock.yml")
171+
lockContent := `# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"` + realHash + `"}
172+
name: test-workflow
173+
`
174+
require.NoError(t, os.WriteFile(lockPath, []byte(lockContent), 0644), "Should write lock file")
175+
176+
output := captureListOutput(t, tmpDir, "")
177+
178+
var items []WorkflowListItem
179+
require.NoError(t, json.Unmarshal([]byte(output), &items), "Should unmarshal JSON output")
180+
require.Len(t, items, 1, "Should have one workflow")
181+
assert.Equal(t, "Yes", items[0].Compiled, "Compiled should be Yes when hash matches")
182+
})
183+
184+
t.Run("compiled No when hash mismatches", func(t *testing.T) {
185+
lockPath := filepath.Join(tmpDir, "test-workflow.lock.yml")
186+
lockContent := `# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"0000000000000000000000000000000000000000000000000000000000000000"}
187+
name: test-workflow
188+
`
189+
require.NoError(t, os.WriteFile(lockPath, []byte(lockContent), 0644), "Should write lock file with wrong hash")
190+
191+
output := captureListOutput(t, tmpDir, "")
192+
193+
var items []WorkflowListItem
194+
require.NoError(t, json.Unmarshal([]byte(output), &items), "Should unmarshal JSON output")
195+
require.Len(t, items, 1, "Should have one workflow")
196+
assert.Equal(t, "No", items[0].Compiled, "Compiled should be No when hash mismatches")
197+
})
198+
199+
t.Run("compiled NA when no lock file", func(t *testing.T) {
200+
// Remove lock file
201+
_ = os.Remove(filepath.Join(tmpDir, "test-workflow.lock.yml"))
202+
203+
output := captureListOutput(t, tmpDir, "")
204+
205+
var items []WorkflowListItem
206+
require.NoError(t, json.Unmarshal([]byte(output), &items), "Should unmarshal JSON output")
207+
require.Len(t, items, 1, "Should have one workflow")
208+
assert.Equal(t, "N/A", items[0].Compiled, "Compiled should be N/A when no lock file exists")
209+
})
210+
}

pkg/cli/status_command.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,13 @@ func calculateTimeRemaining(stopTimeStr string) string {
266266
// isCompiledUpToDate checks if a workflow's lock file is up to date with the current source
267267
// using hash-based comparison. Falls back to "Yes" when no hash is available (legacy lock files).
268268
func isCompiledUpToDate(workflowPath, lockFilePath string) string {
269+
return isCompiledUpToDateWithCache(workflowPath, lockFilePath, parser.NewImportCache(""))
270+
}
271+
272+
// isCompiledUpToDateWithCache is the same as isCompiledUpToDate but accepts a shared
273+
// ImportCache so callers that check multiple workflows can avoid creating a new cache
274+
// on every call.
275+
func isCompiledUpToDateWithCache(workflowPath, lockFilePath string, cache *parser.ImportCache) string {
269276
lockContent, err := os.ReadFile(lockFilePath)
270277
if err != nil {
271278
return "No"
@@ -277,7 +284,6 @@ func isCompiledUpToDate(workflowPath, lockFilePath string) string {
277284
return "Yes"
278285
}
279286

280-
cache := parser.NewImportCache("")
281287
currentHash, err := parser.ComputeFrontmatterHashFromFile(workflowPath, cache)
282288
if err != nil {
283289
statusLog.Printf("Failed to compute frontmatter hash for %s: %v", workflowPath, err)

0 commit comments

Comments
 (0)