-
Notifications
You must be signed in to change notification settings - Fork 296
Add source field to track workflow origin in frontmatter #1234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fb220b8
b689606
3c8cc36
c91cea1
6e25985
3f30514
42701af
a6ab99b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,9 @@ import ( | |
|
|
||
| "github.com/githubnext/gh-aw/pkg/console" | ||
| "github.com/githubnext/gh-aw/pkg/constants" | ||
| "github.com/githubnext/gh-aw/pkg/parser" | ||
| "github.com/githubnext/gh-aw/pkg/workflow" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
|
|
@@ -442,6 +444,19 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e | |
| content = updateWorkflowTitle(content, i) | ||
| } | ||
|
|
||
| // Add source field to frontmatter | ||
| sourceString := buildSourceString(workflow) | ||
| if sourceString != "" { | ||
| updatedContent, err := addSourceToWorkflow(content, sourceString, verbose) | ||
| if err != nil { | ||
| if verbose { | ||
| fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to add source field: %v", err))) | ||
| } | ||
| } else { | ||
| content = updatedContent | ||
| } | ||
| } | ||
|
|
||
| // Track the file based on whether it existed before (if tracker is available) | ||
| if tracker != nil { | ||
| if fileExists { | ||
|
|
@@ -804,3 +819,59 @@ func createPR(branchName, title, body string, verbose bool) error { | |
|
|
||
| return nil | ||
| } | ||
|
|
||
| // buildSourceString builds the source string in the format owner/repo/path@ref | ||
| func buildSourceString(workflow *WorkflowSpec) string { | ||
| if workflow.Repo == "" || workflow.WorkflowPath == "" { | ||
| return "" | ||
| } | ||
|
|
||
| // Format: owner/repo/path@ref (consistent with add command syntax) | ||
| source := workflow.Repo + "/" + workflow.WorkflowPath | ||
| if workflow.Version != "" { | ||
| source += "@" + workflow.Version | ||
| } | ||
|
|
||
| return source | ||
| } | ||
|
|
||
| // addSourceToWorkflow adds the source field to the workflow's frontmatter | ||
| func addSourceToWorkflow(content, source string, verbose bool) (string, error) { | ||
| // Parse frontmatter using parser package | ||
| result, err := parser.ExtractFrontmatterFromContent(content) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to parse frontmatter: %w", err) | ||
| } | ||
|
|
||
| // Initialize frontmatter if it doesn't exist | ||
| if result.Frontmatter == nil { | ||
| result.Frontmatter = make(map[string]any) | ||
| } | ||
|
|
||
| // Add source field (will be last in YAML output due to alphabetical sorting) | ||
| result.Frontmatter["source"] = source | ||
|
|
||
| // Convert back to YAML | ||
| updatedFrontmatter, err := yaml.Marshal(result.Frontmatter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot use helper that fixes yaml marshaling issues with "on"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied UnquoteYAMLKey helper to fix YAML marshaling issues with 'on' keyword. Changes:
The "on" keyword is now properly unquoted when adding source field to workflows. Commit: 3ee8a9c |
||
| if err != nil { | ||
| return "", fmt.Errorf("failed to marshal updated frontmatter: %w", err) | ||
| } | ||
|
|
||
| // Clean up quoted keys - replace "on": with on: at the start of a line | ||
| // This handles cases where YAML marshaling adds unnecessary quotes around reserved words like "on" | ||
| frontmatterStr := strings.TrimSuffix(string(updatedFrontmatter), "\n") | ||
| frontmatterStr = workflow.UnquoteYAMLKey(frontmatterStr, "on") | ||
|
|
||
| // Reconstruct the file | ||
| var lines []string | ||
| lines = append(lines, "---") | ||
| if frontmatterStr != "" { | ||
| lines = append(lines, strings.Split(frontmatterStr, "\n")...) | ||
| } | ||
| lines = append(lines, "---") | ||
pelikhan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if result.Markdown != "" { | ||
| lines = append(lines, result.Markdown) | ||
| } | ||
|
|
||
| return strings.Join(lines, "\n"), nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| package cli | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestBuildSourceString tests the buildSourceString function | ||
| func TestBuildSourceString(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| workflow *WorkflowSpec | ||
| expected string | ||
| }{ | ||
| { | ||
| name: "full_spec_with_version", | ||
| workflow: &WorkflowSpec{ | ||
| Repo: "githubnext/agentics", | ||
| WorkflowPath: "workflows/ci-doctor.md", | ||
| Version: "v1.0.0", | ||
| }, | ||
| expected: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0", | ||
| }, | ||
| { | ||
| name: "spec_without_version", | ||
| workflow: &WorkflowSpec{ | ||
| Repo: "githubnext/agentics", | ||
| WorkflowPath: "workflows/ci-doctor.md", | ||
| Version: "", | ||
| }, | ||
| expected: "githubnext/agentics/workflows/ci-doctor.md", | ||
| }, | ||
| { | ||
| name: "spec_with_branch", | ||
| workflow: &WorkflowSpec{ | ||
| Repo: "githubnext/agentics", | ||
| WorkflowPath: "workflows/daily-plan.md", | ||
| Version: "main", | ||
| }, | ||
| expected: "githubnext/agentics/workflows/daily-plan.md@main", | ||
| }, | ||
| { | ||
| name: "empty_repo", | ||
| workflow: &WorkflowSpec{ | ||
| Repo: "", | ||
| WorkflowPath: "workflows/test.md", | ||
| Version: "v1.0.0", | ||
| }, | ||
| expected: "", | ||
| }, | ||
| { | ||
| name: "empty_workflow_path", | ||
| workflow: &WorkflowSpec{ | ||
| Repo: "githubnext/agentics", | ||
| WorkflowPath: "", | ||
| Version: "v1.0.0", | ||
| }, | ||
| expected: "", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| result := buildSourceString(tt.workflow) | ||
| if result != tt.expected { | ||
| t.Errorf("buildSourceString() = %v, want %v", result, tt.expected) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestAddSourceToWorkflow tests the addSourceToWorkflow function | ||
| func TestAddSourceToWorkflow(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| content string | ||
| source string | ||
| expectError bool | ||
| checkSource bool | ||
| }{ | ||
| { | ||
| name: "add_source_to_workflow_with_frontmatter", | ||
| content: `--- | ||
| on: push | ||
| permissions: | ||
| contents: read | ||
| engine: claude | ||
| --- | ||
|
|
||
| # Test Workflow | ||
|
|
||
| This is a test workflow.`, | ||
| source: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0", | ||
| expectError: false, | ||
| checkSource: true, | ||
| }, | ||
| { | ||
| name: "add_source_to_workflow_without_frontmatter", | ||
| content: `# Test Workflow | ||
|
|
||
| This is a test workflow without frontmatter.`, | ||
| source: "githubnext/agentics/workflows/test.md@main", | ||
| expectError: false, | ||
| checkSource: true, | ||
| }, | ||
| { | ||
| name: "add_source_to_existing_workflow_with_fields", | ||
| content: `--- | ||
| description: "Test workflow description" | ||
| on: push | ||
| permissions: | ||
| contents: read | ||
| engine: claude | ||
| tools: | ||
| github: | ||
| allowed: [list_commits] | ||
| --- | ||
|
|
||
| # Test Workflow | ||
|
|
||
| This is a test workflow.`, | ||
| source: "githubnext/agentics/workflows/complex.md@v1.0.0", | ||
| expectError: false, | ||
| checkSource: true, | ||
| }, | ||
| { | ||
| name: "verify_on_keyword_not_quoted", | ||
| content: `--- | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| types: [opened] | ||
| permissions: | ||
| contents: read | ||
| engine: claude | ||
| --- | ||
|
|
||
| # Test Workflow | ||
|
|
||
| This workflow has complex 'on' triggers.`, | ||
| source: "githubnext/agentics/workflows/test.md@v1.0.0", | ||
| expectError: false, | ||
| checkSource: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| result, err := addSourceToWorkflow(tt.content, tt.source, false) | ||
|
|
||
| if tt.expectError && err == nil { | ||
| t.Errorf("addSourceToWorkflow() expected error, got nil") | ||
| return | ||
| } | ||
|
|
||
| if !tt.expectError && err != nil { | ||
| t.Errorf("addSourceToWorkflow() error = %v", err) | ||
| return | ||
| } | ||
|
|
||
| if !tt.expectError && tt.checkSource { | ||
| // Verify that the source field is present in the result | ||
| if !strings.Contains(result, "source:") { | ||
| t.Errorf("addSourceToWorkflow() result does not contain 'source:' field") | ||
| } | ||
| if !strings.Contains(result, tt.source) { | ||
| t.Errorf("addSourceToWorkflow() result does not contain source value '%s'", tt.source) | ||
| } | ||
|
|
||
| // Verify that frontmatter delimiters are present | ||
| if !strings.Contains(result, "---") { | ||
| t.Errorf("addSourceToWorkflow() result does not contain frontmatter delimiters") | ||
| } | ||
|
|
||
| // Verify that markdown content is preserved | ||
| if strings.Contains(tt.content, "# Test Workflow") && !strings.Contains(result, "# Test Workflow") { | ||
| t.Errorf("addSourceToWorkflow() result does not preserve markdown content") | ||
| } | ||
|
|
||
| // Verify that "on" keyword is not quoted | ||
| if strings.Contains(result, `"on":`) { | ||
| t.Errorf("addSourceToWorkflow() result contains quoted 'on' keyword, should be unquoted. Result:\n%s", result) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.