Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/src/content/docs/reference/auth.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,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**:

Expand Down
21 changes: 7 additions & 14 deletions pkg/cli/run_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -610,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
Expand All @@ -637,15 +642,3 @@ func checkFrontmatterHashMismatch(workflowPath, lockFilePath string) (bool, erro

return mismatch, nil
}

// extractHashFromLockFile extracts the frontmatter-hash from a lock file content
func extractHashFromLockFile(content string) string {
// Look for: # frontmatter-hash: <hash>
lines := strings.SplitSeq(content, "\n")
for line := range lines {
if len(line) > 20 && line[:20] == "# frontmatter-hash: " {
return strings.TrimSpace(line[20:])
}
}
return ""
}
71 changes: 71 additions & 0 deletions pkg/cli/run_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
42 changes: 20 additions & 22 deletions pkg/parser/frontmatter_hash_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
package parser

import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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))
Expand All @@ -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: <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: <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 ""
}
5 changes: 3 additions & 2 deletions pkg/workflow/lock_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions pkg/workflow/lock_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down
Loading