Skip to content
Closed
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
22 changes: 11 additions & 11 deletions .github/workflows/copilot-token-optimizer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 20 additions & 12 deletions pkg/workflow/compiler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ type Compiler struct {
inlinePrompt bool // If true, inline markdown content in YAML instead of using runtime-import macros (for Wasm builds)
priorManifests map[string]*GHAWManifest // Pre-cached manifests keyed by lock file path; takes precedence over git HEAD / filesystem reads
requireDocker bool // If true, fail validation when Docker is not available instead of silently skipping
frontmatterHashCache map[string]frontmatterHashCacheEntry
}

type frontmatterHashCacheEntry struct {
fileSize int64
fileModTimeNS int64
frontmatterHash string
}

// NewCompiler creates a new workflow compiler with functional options.
Expand All @@ -114,18 +121,19 @@ func NewCompiler(opts ...CompilerOption) *Compiler {

// Create compiler with defaults
c := &Compiler{
verbose: false,
engineOverride: "",
version: version,
skipValidation: true, // Skip validation by default for now since existing workflows don't fully comply
actionMode: DetectActionMode(version), // Auto-detect action mode based on version
jobManager: NewJobManager(),
engineRegistry: GetGlobalEngineRegistry(),
engineCatalog: NewEngineCatalog(GetGlobalEngineRegistry()),
stepOrderTracker: NewStepOrderTracker(),
artifactManager: NewArtifactManager(),
actionPinWarnings: make(map[string]bool), // Initialize warning cache
gitRoot: gitRoot, // Auto-detected git root
verbose: false,
engineOverride: "",
version: version,
skipValidation: true, // Skip validation by default for now since existing workflows don't fully comply
actionMode: DetectActionMode(version), // Auto-detect action mode based on version
jobManager: NewJobManager(),
engineRegistry: GetGlobalEngineRegistry(),
engineCatalog: NewEngineCatalog(GetGlobalEngineRegistry()),
stepOrderTracker: NewStepOrderTracker(),
artifactManager: NewArtifactManager(),
actionPinWarnings: make(map[string]bool), // Initialize warning cache
gitRoot: gitRoot, // Auto-detected git root
frontmatterHashCache: make(map[string]frontmatterHashCacheEntry),
}

// Apply functional options
Expand Down
75 changes: 72 additions & 3 deletions pkg/workflow/compiler_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,77 @@ func (c *Compiler) generateWorkflowBody(yaml *strings.Builder, data *WorkflowDat
yaml.WriteString(c.jobManager.RenderToYAML())
}

func canUseFrontmatterHashCache(rawFrontmatter map[string]any) bool {
importsValue, hasImports := rawFrontmatter["imports"]
if !hasImports {
return true
}

switch imports := importsValue.(type) {
case []any:
return len(imports) == 0
case []string:
return len(imports) == 0
case string:
return strings.TrimSpace(imports) == ""
default:
return false
}
}

func (c *Compiler) getCachedFrontmatterHash(markdownPath string) (string, bool) {
entry, ok := c.frontmatterHashCache[markdownPath]
if !ok {
return "", false
}

info, err := os.Stat(markdownPath)
if err != nil {
delete(c.frontmatterHashCache, markdownPath)
return "", false
}
if info.Size() != entry.fileSize || info.ModTime().UnixNano() != entry.fileModTimeNS {
delete(c.frontmatterHashCache, markdownPath)
return "", false
}

return entry.frontmatterHash, true
}

func (c *Compiler) cacheFrontmatterHash(markdownPath string, hash string) {
info, err := os.Stat(markdownPath)
if err != nil {
return
}

c.frontmatterHashCache[markdownPath] = frontmatterHashCacheEntry{
fileSize: info.Size(),
fileModTimeNS: info.ModTime().UnixNano(),
frontmatterHash: hash,
}
}

func (c *Compiler) computeFrontmatterHash(markdownPath string, rawFrontmatter map[string]any) (string, error) {
useCache := canUseFrontmatterHashCache(rawFrontmatter)
if useCache {
if cachedHash, ok := c.getCachedFrontmatterHash(markdownPath); ok {
return cachedHash, nil
}
}

cache := c.getSharedImportCache()
hash, err := parser.ComputeFrontmatterHashFromFileWithParsedFrontmatter(markdownPath, rawFrontmatter, cache, parser.DefaultFileReader)
if err != nil {
return "", err
}

if useCache {
c.cacheFrontmatterHash(markdownPath, hash)
}

return hash, nil
}

func (c *Compiler) generateYAML(data *WorkflowData, markdownPath string) (string, []string, []string, error) {
compilerYamlLog.Printf("Generating YAML for workflow: %s", data.Name)

Expand All @@ -303,9 +374,7 @@ func (c *Compiler) generateYAML(data *WorkflowData, markdownPath string) (string
// the compiled lock file identical across repeated compilations of the same workflow.
var frontmatterHash string
if markdownPath != "" {
baseDir := filepath.Dir(markdownPath)
cache := parser.NewImportCache(baseDir)
hash, err := parser.ComputeFrontmatterHashFromFileWithParsedFrontmatter(markdownPath, data.RawFrontmatter, cache, parser.DefaultFileReader)
hash, err := c.computeFrontmatterHash(markdownPath, data.RawFrontmatter)
if err != nil {
compilerYamlLog.Printf("Warning: failed to compute frontmatter hash: %v", err)
// Continue without hash - non-fatal error
Expand Down
56 changes: 56 additions & 0 deletions pkg/workflow/compiler_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1438,3 +1438,59 @@ Test prompt.
})
}
}

func TestFrontmatterHashCacheInvalidatesWhenWorkflowChanges(t *testing.T) {
tmpDir := testutil.TempDir(t, "frontmatter-hash-cache")
workflowPath := filepath.Join(tmpDir, "cache-test.md")
compiler := NewCompiler()

writeWorkflow := func(t *testing.T, name string) {
t.Helper()
content := fmt.Sprintf(`---
name: %s
on: push
engine: copilot
---

# Cache Test
`, name)
if err := os.WriteFile(workflowPath, []byte(content), 0o644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}
}

readFrontmatterHash := func(t *testing.T) string {
t.Helper()
lockPath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml"
content, err := os.ReadFile(lockPath)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

metadata, _, err := ExtractMetadataFromLockFile(string(content))
if err != nil {
t.Fatalf("Failed to extract lock metadata: %v", err)
}
if metadata == nil || metadata.FrontmatterHash == "" {
t.Fatal("Expected lock metadata to include a frontmatter hash")
}

return metadata.FrontmatterHash
}

writeWorkflow(t, "first")
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("First compile failed: %v", err)
}
firstHash := readFrontmatterHash(t)

writeWorkflow(t, "second")
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Second compile failed: %v", err)
}
secondHash := readFrontmatterHash(t)

if firstHash == secondHash {
t.Fatalf("Expected frontmatter hash to change after workflow update, but both were %q", firstHash)
}
}