From 419f8415adf6ce0ff91dfa271ba4480bb1f941ef Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 23 Feb 2026 19:56:01 +0000 Subject: [PATCH 1/2] fix frontmatter hash --- docs/src/content/docs/reference/auth.mdx | 4 +- pkg/cli/run_push.go | 14 ++++- pkg/cli/run_push_test.go | 71 ++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/reference/auth.mdx b/docs/src/content/docs/reference/auth.mdx index e4dddfc820..9b0614136d 100644 --- a/docs/src/content/docs/reference/auth.mdx +++ b/docs/src/content/docs/reference/auth.mdx @@ -258,8 +258,8 @@ This token is used to authenticate GitHub API operations related to [GitHub Proj **When Required**: -- Read access to projects -- Safe outputs to update or create projects +- Your workflow requires read access to projects +- Your workflow uses safe outputs to update or create projects **Setup**: diff --git a/pkg/cli/run_push.go b/pkg/cli/run_push.go index d90f3743cd..547270d055 100644 --- a/pkg/cli/run_push.go +++ b/pkg/cli/run_push.go @@ -16,6 +16,7 @@ import ( "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/logger" "github.com/github/gh-aw/pkg/parser" + "github.com/github/gh-aw/pkg/workflow" ) var runPushLog = logger.New("cli:run_push") @@ -638,9 +639,18 @@ func checkFrontmatterHashMismatch(workflowPath, lockFilePath string) (bool, erro return mismatch, nil } -// extractHashFromLockFile extracts the frontmatter-hash from a lock file content +// extractHashFromLockFile extracts the frontmatter-hash from a lock file content. +// Supports both the new JSON metadata format (# gh-aw-metadata: {...}) +// and the legacy format (# frontmatter-hash: ). func extractHashFromLockFile(content string) string { - // Look for: # frontmatter-hash: + // First, try to extract from JSON metadata format using the proper workflow package function + if metadata, _, err := workflow.ExtractMetadataFromLockFile(content); err == nil && metadata != nil { + if metadata.FrontmatterHash != "" { + return metadata.FrontmatterHash + } + } + + // Fallback to legacy format: # frontmatter-hash: lines := strings.SplitSeq(content, "\n") for line := range lines { if len(line) > 20 && line[:20] == "# frontmatter-hash: " { diff --git a/pkg/cli/run_push_test.go b/pkg/cli/run_push_test.go index 88d2048767..0e102c276b 100644 --- a/pkg/cli/run_push_test.go +++ b/pkg/cli/run_push_test.go @@ -391,6 +391,77 @@ jobs: assert.False(t, mismatch, "Should not detect mismatch when hashes match") } +func TestCheckFrontmatterHashMismatch_JSONMetadataFormat(t *testing.T) { + // Test that the new JSON metadata format is correctly parsed + tmpDir := t.TempDir() + + // Create a workflow file + workflowPath := filepath.Join(tmpDir, "test-workflow.md") + workflowContent := `--- +name: Test Workflow JSON +engine: copilot +on: workflow_dispatch +--- +# Test Workflow +This is a test workflow for JSON metadata format. +` + err := os.WriteFile(workflowPath, []byte(workflowContent), 0644) + require.NoError(t, err) + + // Compute the correct hash for this workflow + cache := parser.NewImportCache("") + correctHash, err := parser.ComputeFrontmatterHashFromFile(workflowPath, cache) + require.NoError(t, err) + require.NotEmpty(t, correctHash) + + // Create a lock file with JSON metadata format (as written by current compiler) + // Uses snake_case field names matching the Go struct JSON tags + lockFilePath := filepath.Join(tmpDir, "test-workflow.lock.yml") + lockContent := fmt.Sprintf(`# This workflow was auto-generated by gh-aw +# +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"%s","stop_time":""} + +name: Test Workflow JSON +"on": workflow_dispatch +jobs: + agent: + runs-on: ubuntu-latest + steps: + - name: Test + run: echo "test" +`, correctHash) + err = os.WriteFile(lockFilePath, []byte(lockContent), 0644) + require.NoError(t, err) + + // Test that matching hash in JSON format is correctly detected + mismatch, err := checkFrontmatterHashMismatch(workflowPath, lockFilePath) + require.NoError(t, err) + assert.False(t, mismatch, "Should not detect mismatch when JSON metadata hash matches") + + // Now test with a wrong hash in JSON format + wrongHash := "0000000000000000000000000000000000000000000000000000000000000000" + lockContentWrong := fmt.Sprintf(`# This workflow was auto-generated by gh-aw +# +# gh-aw-metadata: {"schema_version":"v1","frontmatter_hash":"%s","stop_time":""} + +name: Test Workflow JSON +"on": workflow_dispatch +jobs: + agent: + runs-on: ubuntu-latest + steps: + - name: Test + run: echo "test" +`, wrongHash) + err = os.WriteFile(lockFilePath, []byte(lockContentWrong), 0644) + require.NoError(t, err) + + // Test that mismatched hash in JSON format is correctly detected + mismatch, err = checkFrontmatterHashMismatch(workflowPath, lockFilePath) + require.NoError(t, err) + assert.True(t, mismatch, "Should detect mismatch when JSON metadata hash differs") +} + func TestPushWorkflowFiles_WithStagedFiles(t *testing.T) { // Create a temporary directory for testing tmpDir := t.TempDir() From 47e465bf6477ff897461d9ed85e2584ae92ee0ac Mon Sep 17 00:00:00 2001 From: Don Syme Date: Mon, 23 Feb 2026 20:22:00 +0000 Subject: [PATCH 2/2] simplify front matter hash code --- pkg/cli/run_push.go | 29 +++---------- .../frontmatter_hash_repository_test.go | 42 +++++++++---------- pkg/workflow/lock_schema.go | 5 ++- pkg/workflow/lock_schema_test.go | 11 +++-- 4 files changed, 36 insertions(+), 51 deletions(-) diff --git a/pkg/cli/run_push.go b/pkg/cli/run_push.go index 547270d055..a995c1d1cc 100644 --- a/pkg/cli/run_push.go +++ b/pkg/cli/run_push.go @@ -611,8 +611,12 @@ func checkFrontmatterHashMismatch(workflowPath, lockFilePath string) (bool, erro return false, fmt.Errorf("failed to read lock file: %w", err) } - // Extract hash from lock file - existingHash := extractHashFromLockFile(string(lockContent)) + // Extract hash from lock file using the workflow package function + metadata, _, err := workflow.ExtractMetadataFromLockFile(string(lockContent)) + var existingHash string + if err == nil && metadata != nil { + existingHash = metadata.FrontmatterHash + } if existingHash == "" { runPushLog.Print("No frontmatter-hash found in lock file") // No hash in lock file - consider it stale to regenerate with hash @@ -638,24 +642,3 @@ func checkFrontmatterHashMismatch(workflowPath, lockFilePath string) (bool, erro return mismatch, nil } - -// extractHashFromLockFile extracts the frontmatter-hash from a lock file content. -// Supports both the new JSON metadata format (# gh-aw-metadata: {...}) -// and the legacy format (# frontmatter-hash: ). -func extractHashFromLockFile(content string) string { - // First, try to extract from JSON metadata format using the proper workflow package function - if metadata, _, err := workflow.ExtractMetadataFromLockFile(content); err == nil && metadata != nil { - if metadata.FrontmatterHash != "" { - return metadata.FrontmatterHash - } - } - - // Fallback to legacy format: # frontmatter-hash: - lines := strings.SplitSeq(content, "\n") - for line := range lines { - if len(line) > 20 && line[:20] == "# frontmatter-hash: " { - return strings.TrimSpace(line[20:]) - } - } - return "" -} diff --git a/pkg/parser/frontmatter_hash_repository_test.go b/pkg/parser/frontmatter_hash_repository_test.go index 4a3d8df926..041d6279b9 100644 --- a/pkg/parser/frontmatter_hash_repository_test.go +++ b/pkg/parser/frontmatter_hash_repository_test.go @@ -3,8 +3,10 @@ package parser import ( + "encoding/json" "os" "path/filepath" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -117,7 +119,7 @@ func TestHashConsistencyAcrossLockFiles(t *testing.T) { require.NoError(t, err, "Should read lock file for %s", filepath.Base(lockFile)) // Extract hash from lock file comment - lockHash := extractHashFromLockFile(string(lockContent)) + lockHash := extractHashFromLockFileContent(string(lockContent)) if lockHash == "" { t.Logf(" ⚠ %s: No hash in lock file (may need recompilation)", filepath.Base(mdFile)) @@ -138,29 +140,25 @@ func TestHashConsistencyAcrossLockFiles(t *testing.T) { t.Logf("\nVerified hash consistency for %d workflows", checkedCount) } -// extractHashFromLockFile extracts the frontmatter-hash from a lock file -func extractHashFromLockFile(content string) string { - // Look for: # frontmatter-hash: - lines := splitLines(content) - for _, line := range lines { - if len(line) > 20 && line[:20] == "# frontmatter-hash: " { - return line[20:] +// extractHashFromLockFileContent extracts the frontmatter-hash from lock file content. +// Supports both JSON metadata format (# gh-aw-metadata: {...}) and legacy format. +func extractHashFromLockFileContent(content string) string { + // Try JSON metadata format first: # gh-aw-metadata: {...} + metadataPattern := regexp.MustCompile(`#\s*gh-aw-metadata:\s*(\{.+\})`) + if matches := metadataPattern.FindStringSubmatch(content); len(matches) >= 2 { + var metadata struct { + FrontmatterHash string `json:"frontmatter_hash"` } - } - return "" -} - -func splitLines(s string) []string { - var lines []string - start := 0 - for i := range len(s) { - if s[i] == '\n' { - lines = append(lines, s[start:i]) - start = i + 1 + if err := json.Unmarshal([]byte(matches[1]), &metadata); err == nil && metadata.FrontmatterHash != "" { + return metadata.FrontmatterHash } } - if start < len(s) { - lines = append(lines, s[start:]) + + // Fallback to legacy format: # frontmatter-hash: + hashPattern := regexp.MustCompile(`#\s*frontmatter-hash:\s*([0-9a-f]{64})`) + if matches := hashPattern.FindStringSubmatch(content); len(matches) >= 2 { + return matches[1] } - return lines + + return "" } diff --git a/pkg/workflow/lock_schema.go b/pkg/workflow/lock_schema.go index 755857197e..86e5e16cd6 100644 --- a/pkg/workflow/lock_schema.go +++ b/pkg/workflow/lock_schema.go @@ -58,9 +58,10 @@ func ExtractMetadataFromLockFile(content string) (*LockMetadata, bool, error) { // Legacy format: look for frontmatter-hash without JSON metadata hashPattern := regexp.MustCompile(`#\s*frontmatter-hash:\s*([0-9a-f]{64})`) - if hashPattern.MatchString(content) { + if matches := hashPattern.FindStringSubmatch(content); len(matches) >= 2 { lockSchemaLog.Print("Legacy lock file detected (no schema version)") - return nil, true, nil + // Return a minimal metadata struct with just the hash for legacy files + return &LockMetadata{FrontmatterHash: matches[1]}, true, nil } // No metadata found at all diff --git a/pkg/workflow/lock_schema_test.go b/pkg/workflow/lock_schema_test.go index 73ceea622e..984a2b4eec 100644 --- a/pkg/workflow/lock_schema_test.go +++ b/pkg/workflow/lock_schema_test.go @@ -46,9 +46,11 @@ name: test content: `# frontmatter-hash: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef name: test `, - expectMetadata: nil, - expectLegacy: true, - expectError: false, + expectMetadata: &LockMetadata{ + FrontmatterHash: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + expectLegacy: true, + expectError: false, }, { name: "no metadata", @@ -426,7 +428,8 @@ on: push metadata, isLegacy, err := ExtractMetadataFromLockFile(content) require.NoError(t, err, "Should parse legacy lock file") assert.True(t, isLegacy, "Should detect as legacy") - assert.Nil(t, metadata, "Should not extract metadata from legacy") + require.NotNil(t, metadata, "Should extract metadata with hash from legacy") + assert.Equal(t, "49266e50774d7e6a8b1c50f64b2f790c214dcdcf7b75b6bc8478bb43257b9863", metadata.FrontmatterHash, "Should extract hash from legacy") // Legacy format should validate successfully err = ValidateLockSchemaCompatibility(content, "legacy.lock.yml")