From d845e3acebf7dcbe25c0e35b83b27ae9b4ed40c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 06:37:45 +0000 Subject: [PATCH 1/3] feat: handle directory and GitHub URL arguments in compile command When a directory path or a GitHub URL (tree/blob) is passed as a compile argument, expand it to the constituent .md workflow files instead of failing with a "workflow not found" error. - New compile_args.go: resolveCompileArgs, expandCompileArg, expandURLArg, expandDirectoryArg functions - compile_orchestrator.go: preprocess MarkdownFiles with resolveCompileArgs before starting compilation - compile_args_test.go: unit tests for all expansion paths - main.go: updated help text with directory and URL usage examples Agent-Logs-Url: https://github.com/github/gh-aw/sessions/771b5545-fcc4-40cc-9a3a-4e9979778509 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- cmd/gh-aw/main.go | 2 + pkg/cli/compile_args.go | 129 ++++++++++++++++++++++++++++++++ pkg/cli/compile_args_test.go | 115 ++++++++++++++++++++++++++++ pkg/cli/compile_orchestrator.go | 9 +++ 4 files changed, 255 insertions(+) create mode 100644 pkg/cli/compile_args.go create mode 100644 pkg/cli/compile_args_test.go diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index a4fbc2800b3..0c787ae2784 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -253,6 +253,8 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` compile ci-doctor # Compile a specific workflow ` + string(constants.CLIExtensionPrefix) + ` compile ci-doctor daily-plan # Compile multiple workflows ` + string(constants.CLIExtensionPrefix) + ` compile workflow.md # Compile by file path + ` + string(constants.CLIExtensionPrefix) + ` compile .github/workflows # Compile all workflows in a directory + ` + string(constants.CLIExtensionPrefix) + ` compile https://github.com/org/repo/tree/main/.github/workflows # Compile from folder URL ` + string(constants.CLIExtensionPrefix) + ` compile --dir custom/workflows # Compile from custom directory ` + string(constants.CLIExtensionPrefix) + ` compile --watch ci-doctor # Watch and auto-compile ` + string(constants.CLIExtensionPrefix) + ` compile --trial --logical-repo owner/repo # Compile for trial mode diff --git a/pkg/cli/compile_args.go b/pkg/cli/compile_args.go new file mode 100644 index 00000000000..3cb619062fe --- /dev/null +++ b/pkg/cli/compile_args.go @@ -0,0 +1,129 @@ +// This file provides argument preprocessing for the compile command. +// +// It handles expansion of directory paths and GitHub URLs into their +// constituent workflow .md files so that the rest of the compilation +// pipeline only needs to deal with concrete file paths. +// +// # Key Functions +// +// - resolveCompileArgs() - Expand a list of compile arguments +// - expandCompileArg() - Expand a single argument (URL, directory, or file) +// - expandURLArg() - Parse a GitHub URL and resolve its local path +// - expandDirectoryArg() - Return all .md workflow files inside a directory + +package cli + +import ( + "fmt" + "os" + "strings" + + "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/parser" +) + +var compileArgsLog = logger.New("cli:compile_args") + +// resolveCompileArgs preprocesses compile command arguments to handle +// directory paths and GitHub URLs. When an argument is a directory or a +// GitHub URL pointing to a folder, it is expanded to all .md workflow files +// in that directory. +func resolveCompileArgs(args []string, verbose bool) ([]string, error) { + if len(args) == 0 { + return args, nil + } + + var result []string + for _, arg := range args { + expanded, err := expandCompileArg(arg, verbose) + if err != nil { + return nil, err + } + result = append(result, expanded...) + } + return result, nil +} + +// expandCompileArg expands a single compile argument: +// - GitHub URLs (http:// or https://) pointing to a directory: expand to all .md files +// - GitHub URLs pointing to a specific .md file: return the extracted local path +// - Local directory paths: expand to all .md files in that directory +// - Everything else: return as-is for the existing resolver to handle +func expandCompileArg(arg string, verbose bool) ([]string, error) { + compileArgsLog.Printf("Processing compile argument: %s", arg) + + // Handle GitHub URLs + if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + return expandURLArg(arg, verbose) + } + + // Handle local directory paths + info, err := os.Stat(arg) + if err == nil && info.IsDir() { + return expandDirectoryArg(arg, verbose) + } + + // Return as-is (regular file path or workflow name) + return []string{arg}, nil +} + +// expandURLArg handles a GitHub URL argument for the compile command. +// For tree (directory) URLs, it compiles all workflows in the corresponding +// local directory. For blob/raw (file) URLs, it returns the extracted local +// file path so that the standard resolver can locate the file. +func expandURLArg(urlArg string, verbose bool) ([]string, error) { + compileArgsLog.Printf("Parsing GitHub URL argument: %s", urlArg) + + components, err := parser.ParseGitHubURL(urlArg) + if err != nil { + compileArgsLog.Printf("Failed to parse URL %s: %v - using as-is", urlArg, err) + // Return the URL as-is; the standard resolver will produce a clear error + return []string{urlArg}, nil + } + + localPath := components.Path + if localPath == "" { + compileArgsLog.Printf("No path extracted from URL %s", urlArg) + return []string{urlArg}, nil + } + + compileArgsLog.Printf("Extracted local path from URL: %s (type=%s)", localPath, components.Type) + + // For tree (directory) URLs, compile all .md files in that directory + if components.Type == parser.URLTypeTree { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Compiling all workflows in directory: %s", localPath))) + } + return expandDirectoryArg(localPath, verbose) + } + + // For blob/raw (file) URLs, return the extracted local path + return []string{localPath}, nil +} + +// expandDirectoryArg expands a directory path to all .md workflow files in it. +func expandDirectoryArg(dirPath string, verbose bool) ([]string, error) { + compileArgsLog.Printf("Expanding directory argument: %s", dirPath) + + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Compiling all workflows in directory: "+dirPath)) + } + + mdFiles, err := getMarkdownWorkflowFiles(dirPath) + if err != nil { + return nil, fmt.Errorf("failed to find workflow files in %s: %w", dirPath, err) + } + + mdFiles, err = filterMarkdownFilesWithFrontmatter(mdFiles) + if err != nil { + return nil, fmt.Errorf("failed to filter workflow files in %s: %w", dirPath, err) + } + + if len(mdFiles) == 0 { + return nil, fmt.Errorf("no workflow markdown files found in %s (workflow files must start with a frontmatter opener on the first line)", dirPath) + } + + compileArgsLog.Printf("Found %d workflow files in directory %s", len(mdFiles), dirPath) + return mdFiles, nil +} diff --git a/pkg/cli/compile_args_test.go b/pkg/cli/compile_args_test.go new file mode 100644 index 00000000000..86992fb6dde --- /dev/null +++ b/pkg/cli/compile_args_test.go @@ -0,0 +1,115 @@ +//go:build !integration + +package cli + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExpandCompileArg_RegularFile(t *testing.T) { + // A plain workflow name should be returned as-is + result, err := expandCompileArg("my-workflow", false) + require.NoError(t, err, "plain workflow name should not error") + assert.Equal(t, []string{"my-workflow"}, result, "plain workflow name should be returned unchanged") +} + +func TestExpandCompileArg_FilePath(t *testing.T) { + // A non-existent file path should be returned as-is + result, err := expandCompileArg(".github/workflows/my-workflow.md", false) + require.NoError(t, err, "non-existent file path should not error") + assert.Equal(t, []string{".github/workflows/my-workflow.md"}, result, "file path should be returned unchanged") +} + +func TestExpandCompileArg_LocalDirectory(t *testing.T) { + // Create a temp directory with workflow files + tmpDir := t.TempDir() + writeWorkflowFile(t, tmpDir, "workflow-a.md") + writeWorkflowFile(t, tmpDir, "workflow-b.md") + + result, err := expandCompileArg(tmpDir, false) + require.NoError(t, err, "local directory should expand without error") + assert.Len(t, result, 2, "should return all .md files in the directory") +} + +func TestExpandCompileArg_LocalDirectory_Empty(t *testing.T) { + // Directory with no .md files should error + tmpDir := t.TempDir() + _, err := expandCompileArg(tmpDir, false) + assert.Error(t, err, "empty directory should return an error") + assert.Contains(t, err.Error(), "no workflow markdown files found", "error should mention no workflow files") +} + +func TestExpandCompileArg_TreeURL(t *testing.T) { + // Create a local .github/workflows directory so that the extracted path exists + tmpDir := t.TempDir() + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + require.NoError(t, os.MkdirAll(workflowsDir, 0755), "create workflows dir") + writeWorkflowFile(t, workflowsDir, "workflow-a.md") + writeWorkflowFile(t, workflowsDir, "workflow-b.md") + + // Temporarily change working directory so relative paths work + originalWd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(tmpDir)) + defer func() { _ = os.Chdir(originalWd) }() + + url := "https://github.com/org/repo/tree/main/.github/workflows" + result, err := expandCompileArg(url, false) + require.NoError(t, err, "tree URL should expand to local directory files") + assert.Len(t, result, 2, "should return all .md files in the extracted directory") + for _, f := range result { + assert.True(t, strings.HasSuffix(f, ".md"), "each result should be a .md file") + } +} + +func TestExpandCompileArg_BlobURL(t *testing.T) { + url := "https://github.com/org/repo/blob/main/.github/workflows/my-workflow.md" + result, err := expandCompileArg(url, false) + require.NoError(t, err, "blob URL should not error") + assert.Equal(t, []string{".github/workflows/my-workflow.md"}, result, + "blob URL should return the extracted local file path") +} + +func TestExpandCompileArg_InvalidURL(t *testing.T) { + // An unparseable URL should be returned as-is (let the resolver handle it) + url := "https://not-github.com/some/path" + result, err := expandCompileArg(url, false) + require.NoError(t, err, "unrecognized URL should not error") + assert.Equal(t, []string{url}, result, "unrecognized URL should be returned unchanged") +} + +func TestResolveCompileArgs_Empty(t *testing.T) { + result, err := resolveCompileArgs(nil, false) + require.NoError(t, err) + assert.Empty(t, result) +} + +func TestResolveCompileArgs_Mixed(t *testing.T) { + // Create a temp directory with a workflow file + tmpDir := t.TempDir() + writeWorkflowFile(t, tmpDir, "workflow-a.md") + + // Mix: a plain workflow name + a directory + result, err := resolveCompileArgs([]string{"plain-workflow", tmpDir}, false) + require.NoError(t, err, "mixed args should expand without error") + require.Len(t, result, 2, "should have one plain name + one expanded file") + + // The plain workflow name should be unchanged + assert.Equal(t, "plain-workflow", result[0], "first arg should be unchanged") + // The directory arg should be the single .md file in tmpDir + assert.True(t, strings.HasSuffix(result[1], "workflow-a.md"), "second arg should be the expanded .md file") +} + +// writeWorkflowFile creates a minimal workflow .md file in dir with the given name. +func writeWorkflowFile(t *testing.T, dir, name string) { + t.Helper() + content := "---\non: push\n---\n\n# Test Workflow\n" + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0600), + "write workflow file %s", name) +} diff --git a/pkg/cli/compile_orchestrator.go b/pkg/cli/compile_orchestrator.go index 99fe2c27c85..6a38561d528 100644 --- a/pkg/cli/compile_orchestrator.go +++ b/pkg/cli/compile_orchestrator.go @@ -57,6 +57,15 @@ func CompileWorkflows(ctx context.Context, config CompileConfig) ([]*workflow.Wo compileOrchestratorLog.Printf("Using custom workflow directory: %s", workflowDir) } + // Preprocess args: expand directory paths and GitHub URLs to constituent workflow files + if len(config.MarkdownFiles) > 0 { + expandedFiles, err := resolveCompileArgs(config.MarkdownFiles, config.Verbose) + if err != nil { + return nil, err + } + config.MarkdownFiles = expandedFiles + } + // Create and configure compiler compiler := createAndConfigureCompiler(config) From 6e07cf272d7217842a5e14953f052e7f48c95cff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 06:39:31 +0000 Subject: [PATCH 2/3] fix: remove duplicate verbose message and use 0644 permission in tests Agent-Logs-Url: https://github.com/github/gh-aw/sessions/771b5545-fcc4-40cc-9a3a-4e9979778509 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_args.go | 3 --- pkg/cli/compile_args_test.go | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/cli/compile_args.go b/pkg/cli/compile_args.go index 3cb619062fe..52d30b7d4ec 100644 --- a/pkg/cli/compile_args.go +++ b/pkg/cli/compile_args.go @@ -92,9 +92,6 @@ func expandURLArg(urlArg string, verbose bool) ([]string, error) { // For tree (directory) URLs, compile all .md files in that directory if components.Type == parser.URLTypeTree { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Compiling all workflows in directory: %s", localPath))) - } return expandDirectoryArg(localPath, verbose) } diff --git a/pkg/cli/compile_args_test.go b/pkg/cli/compile_args_test.go index 86992fb6dde..6ef25fdf570 100644 --- a/pkg/cli/compile_args_test.go +++ b/pkg/cli/compile_args_test.go @@ -110,6 +110,6 @@ func TestResolveCompileArgs_Mixed(t *testing.T) { func writeWorkflowFile(t *testing.T, dir, name string) { t.Helper() content := "---\non: push\n---\n\n# Test Workflow\n" - require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0600), + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0644), "write workflow file %s", name) } From b9e5e6fb39b3d47240fe56af9289270dc13b2ba8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 07:13:43 +0000 Subject: [PATCH 3/3] refactor: remove URL support from compile args, keep local directory only Per feedback, URL expansion is removed. The compile command now only expands local directory paths to their constituent .md workflow files. - compile_args.go: remove expandURLArg and the URL detection branch - compile_args_test.go: replace URL tests with a URL-passthrough test - main.go: remove the GitHub URL example from help text Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d64afd8b-17e9-425a-8385-27a057edc074 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- cmd/gh-aw/main.go | 1 - pkg/cli/compile_args.go | 54 ++++-------------------------------- pkg/cli/compile_args_test.go | 39 +++----------------------- 3 files changed, 10 insertions(+), 84 deletions(-) diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index 0c787ae2784..12b29b4cab6 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -254,7 +254,6 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` compile ci-doctor daily-plan # Compile multiple workflows ` + string(constants.CLIExtensionPrefix) + ` compile workflow.md # Compile by file path ` + string(constants.CLIExtensionPrefix) + ` compile .github/workflows # Compile all workflows in a directory - ` + string(constants.CLIExtensionPrefix) + ` compile https://github.com/org/repo/tree/main/.github/workflows # Compile from folder URL ` + string(constants.CLIExtensionPrefix) + ` compile --dir custom/workflows # Compile from custom directory ` + string(constants.CLIExtensionPrefix) + ` compile --watch ci-doctor # Watch and auto-compile ` + string(constants.CLIExtensionPrefix) + ` compile --trial --logical-repo owner/repo # Compile for trial mode diff --git a/pkg/cli/compile_args.go b/pkg/cli/compile_args.go index 52d30b7d4ec..398bc215a66 100644 --- a/pkg/cli/compile_args.go +++ b/pkg/cli/compile_args.go @@ -1,14 +1,13 @@ // This file provides argument preprocessing for the compile command. // -// It handles expansion of directory paths and GitHub URLs into their -// constituent workflow .md files so that the rest of the compilation -// pipeline only needs to deal with concrete file paths. +// It handles expansion of local directory paths into their constituent +// workflow .md files so that the rest of the compilation pipeline only +// needs to deal with concrete file paths. // // # Key Functions // // - resolveCompileArgs() - Expand a list of compile arguments -// - expandCompileArg() - Expand a single argument (URL, directory, or file) -// - expandURLArg() - Parse a GitHub URL and resolve its local path +// - expandCompileArg() - Expand a single argument (directory or file) // - expandDirectoryArg() - Return all .md workflow files inside a directory package cli @@ -16,19 +15,16 @@ package cli import ( "fmt" "os" - "strings" "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/logger" - "github.com/github/gh-aw/pkg/parser" ) var compileArgsLog = logger.New("cli:compile_args") // resolveCompileArgs preprocesses compile command arguments to handle -// directory paths and GitHub URLs. When an argument is a directory or a -// GitHub URL pointing to a folder, it is expanded to all .md workflow files -// in that directory. +// local directory paths. When an argument is a directory it is expanded +// to all .md workflow files in that directory. func resolveCompileArgs(args []string, verbose bool) ([]string, error) { if len(args) == 0 { return args, nil @@ -46,18 +42,11 @@ func resolveCompileArgs(args []string, verbose bool) ([]string, error) { } // expandCompileArg expands a single compile argument: -// - GitHub URLs (http:// or https://) pointing to a directory: expand to all .md files -// - GitHub URLs pointing to a specific .md file: return the extracted local path // - Local directory paths: expand to all .md files in that directory // - Everything else: return as-is for the existing resolver to handle func expandCompileArg(arg string, verbose bool) ([]string, error) { compileArgsLog.Printf("Processing compile argument: %s", arg) - // Handle GitHub URLs - if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { - return expandURLArg(arg, verbose) - } - // Handle local directory paths info, err := os.Stat(arg) if err == nil && info.IsDir() { @@ -68,37 +57,6 @@ func expandCompileArg(arg string, verbose bool) ([]string, error) { return []string{arg}, nil } -// expandURLArg handles a GitHub URL argument for the compile command. -// For tree (directory) URLs, it compiles all workflows in the corresponding -// local directory. For blob/raw (file) URLs, it returns the extracted local -// file path so that the standard resolver can locate the file. -func expandURLArg(urlArg string, verbose bool) ([]string, error) { - compileArgsLog.Printf("Parsing GitHub URL argument: %s", urlArg) - - components, err := parser.ParseGitHubURL(urlArg) - if err != nil { - compileArgsLog.Printf("Failed to parse URL %s: %v - using as-is", urlArg, err) - // Return the URL as-is; the standard resolver will produce a clear error - return []string{urlArg}, nil - } - - localPath := components.Path - if localPath == "" { - compileArgsLog.Printf("No path extracted from URL %s", urlArg) - return []string{urlArg}, nil - } - - compileArgsLog.Printf("Extracted local path from URL: %s (type=%s)", localPath, components.Type) - - // For tree (directory) URLs, compile all .md files in that directory - if components.Type == parser.URLTypeTree { - return expandDirectoryArg(localPath, verbose) - } - - // For blob/raw (file) URLs, return the extracted local path - return []string{localPath}, nil -} - // expandDirectoryArg expands a directory path to all .md workflow files in it. func expandDirectoryArg(dirPath string, verbose bool) ([]string, error) { compileArgsLog.Printf("Expanding directory argument: %s", dirPath) diff --git a/pkg/cli/compile_args_test.go b/pkg/cli/compile_args_test.go index 6ef25fdf570..2253a2a2cf9 100644 --- a/pkg/cli/compile_args_test.go +++ b/pkg/cli/compile_args_test.go @@ -45,43 +45,12 @@ func TestExpandCompileArg_LocalDirectory_Empty(t *testing.T) { assert.Contains(t, err.Error(), "no workflow markdown files found", "error should mention no workflow files") } -func TestExpandCompileArg_TreeURL(t *testing.T) { - // Create a local .github/workflows directory so that the extracted path exists - tmpDir := t.TempDir() - workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - require.NoError(t, os.MkdirAll(workflowsDir, 0755), "create workflows dir") - writeWorkflowFile(t, workflowsDir, "workflow-a.md") - writeWorkflowFile(t, workflowsDir, "workflow-b.md") - - // Temporarily change working directory so relative paths work - originalWd, err := os.Getwd() - require.NoError(t, err) - require.NoError(t, os.Chdir(tmpDir)) - defer func() { _ = os.Chdir(originalWd) }() - +func TestExpandCompileArg_URLPassthrough(t *testing.T) { + // URLs should be returned as-is (not processed) url := "https://github.com/org/repo/tree/main/.github/workflows" result, err := expandCompileArg(url, false) - require.NoError(t, err, "tree URL should expand to local directory files") - assert.Len(t, result, 2, "should return all .md files in the extracted directory") - for _, f := range result { - assert.True(t, strings.HasSuffix(f, ".md"), "each result should be a .md file") - } -} - -func TestExpandCompileArg_BlobURL(t *testing.T) { - url := "https://github.com/org/repo/blob/main/.github/workflows/my-workflow.md" - result, err := expandCompileArg(url, false) - require.NoError(t, err, "blob URL should not error") - assert.Equal(t, []string{".github/workflows/my-workflow.md"}, result, - "blob URL should return the extracted local file path") -} - -func TestExpandCompileArg_InvalidURL(t *testing.T) { - // An unparseable URL should be returned as-is (let the resolver handle it) - url := "https://not-github.com/some/path" - result, err := expandCompileArg(url, false) - require.NoError(t, err, "unrecognized URL should not error") - assert.Equal(t, []string{url}, result, "unrecognized URL should be returned unchanged") + require.NoError(t, err, "URL should not error") + assert.Equal(t, []string{url}, result, "URL should be returned unchanged") } func TestResolveCompileArgs_Empty(t *testing.T) {