From 72b482130307292553243080515be97ed8acd780 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:37:26 +0000 Subject: [PATCH 1/5] Initial plan From ac33ff52ce35810032b3a518309756ac97e37da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:47:40 +0000 Subject: [PATCH 2/5] Add support for resolving shared workflow paths in mcp inspect Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/resolver.go | 39 +++++++++++++++++++++++++++++--- pkg/cli/resolver_test.go | 48 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/pkg/cli/resolver.go b/pkg/cli/resolver.go index 6b542a94cf7..053257f15c4 100644 --- a/pkg/cli/resolver.go +++ b/pkg/cli/resolver.go @@ -10,10 +10,13 @@ import ( // ResolveWorkflowPath resolves a workflow file path from various formats: // - Absolute path to .md file // - Relative path to .md file -// - Workflow name (adds .md extension and looks in .github/workflows) +// - Workflow name (adds .md extension and looks in .github/workflows, shared/, and shared/mcp/) // - Workflow name with .md extension func ResolveWorkflowPath(workflowFile string) (string, error) { workflowsDir := ".github/workflows" + sharedDir := filepath.Join(workflowsDir, "shared") + sharedMCPDir := filepath.Join(sharedDir, "mcp") + var workflowPath string if strings.HasSuffix(workflowFile, ".md") { @@ -21,12 +24,42 @@ func ResolveWorkflowPath(workflowFile string) (string, error) { if _, err := os.Stat(workflowFile); err == nil { workflowPath = workflowFile } else { - // Try in workflows directory + // Try in workflows directory first workflowPath = filepath.Join(workflowsDir, workflowFile) + + // If not found, try shared directories + if _, err := os.Stat(workflowPath); os.IsNotExist(err) { + // Try shared directory + sharedPath := filepath.Join(sharedDir, workflowFile) + if _, err := os.Stat(sharedPath); err == nil { + workflowPath = sharedPath + } else { + // Try shared/mcp directory + sharedMCPPath := filepath.Join(sharedMCPDir, workflowFile) + if _, err := os.Stat(sharedMCPPath); err == nil { + workflowPath = sharedMCPPath + } + } + } } } else { - // Add .md extension and look in workflows directory + // Add .md extension and look in workflows directory first workflowPath = filepath.Join(workflowsDir, workflowFile+".md") + + // If not found, try shared directories + if _, err := os.Stat(workflowPath); os.IsNotExist(err) { + // Try shared directory + sharedPath := filepath.Join(sharedDir, workflowFile+".md") + if _, err := os.Stat(sharedPath); err == nil { + workflowPath = sharedPath + } else { + // Try shared/mcp directory + sharedMCPPath := filepath.Join(sharedMCPDir, workflowFile+".md") + if _, err := os.Stat(sharedMCPPath); err == nil { + workflowPath = sharedMCPPath + } + } + } } // Verify the workflow file exists diff --git a/pkg/cli/resolver_test.go b/pkg/cli/resolver_test.go index a2cb464e085..aeddb8b9669 100644 --- a/pkg/cli/resolver_test.go +++ b/pkg/cli/resolver_test.go @@ -27,18 +27,38 @@ func TestResolveWorkflowPath(t *testing.T) { t.Fatalf("Failed to change to temp directory: %v", err) } - // Create .github/workflows directory + // Create .github/workflows directory structure workflowsDir := filepath.Join(constants.GetWorkflowDir()) if err := os.MkdirAll(workflowsDir, 0755); err != nil { t.Fatalf("Failed to create workflows directory: %v", err) } - // Create test workflow files + sharedDir := filepath.Join(workflowsDir, "shared") + if err := os.MkdirAll(sharedDir, 0755); err != nil { + t.Fatalf("Failed to create shared directory: %v", err) + } + + sharedMCPDir := filepath.Join(sharedDir, "mcp") + if err := os.MkdirAll(sharedMCPDir, 0755); err != nil { + t.Fatalf("Failed to create shared/mcp directory: %v", err) + } + + // Create test workflow files in different locations testWorkflow := filepath.Join(workflowsDir, "test-workflow.md") if err := os.WriteFile(testWorkflow, []byte("# Test"), 0644); err != nil { t.Fatalf("Failed to create test workflow: %v", err) } + sharedWorkflow := filepath.Join(sharedDir, "shared-workflow.md") + if err := os.WriteFile(sharedWorkflow, []byte("# Shared"), 0644); err != nil { + t.Fatalf("Failed to create shared workflow: %v", err) + } + + mcpWorkflow := filepath.Join(sharedMCPDir, "serena.md") + if err := os.WriteFile(mcpWorkflow, []byte("# MCP"), 0644); err != nil { + t.Fatalf("Failed to create MCP workflow: %v", err) + } + tests := []struct { name string input string @@ -46,15 +66,35 @@ func TestResolveWorkflowPath(t *testing.T) { expectError bool }{ { - name: "workflow name without extension", + name: "workflow name without extension in workflows dir", input: "test-workflow", expected: testWorkflow, }, { - name: "workflow name with extension", + name: "workflow name with extension in workflows dir", input: "test-workflow.md", expected: testWorkflow, }, + { + name: "workflow name without extension in shared dir", + input: "shared-workflow", + expected: sharedWorkflow, + }, + { + name: "workflow name with extension in shared dir", + input: "shared-workflow.md", + expected: sharedWorkflow, + }, + { + name: "workflow name without extension in shared/mcp dir", + input: "serena", + expected: mcpWorkflow, + }, + { + name: "workflow name with extension in shared/mcp dir", + input: "serena.md", + expected: mcpWorkflow, + }, { name: "nonexistent workflow", input: "nonexistent", From 6fe7a5467795669c3ea379d39bf27e8c8a0e8d15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Oct 2025 23:52:48 +0000 Subject: [PATCH 3/5] Fix inspector flag to use ResolveWorkflowPath for shared workflows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/mcp_inspect.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pkg/cli/mcp_inspect.go b/pkg/cli/mcp_inspect.go index 77d3679efb1..4efdd8e6bd0 100644 --- a/pkg/cli/mcp_inspect.go +++ b/pkg/cli/mcp_inspect.go @@ -10,7 +10,6 @@ import ( "time" "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/spf13/cobra" @@ -356,14 +355,13 @@ func spawnMCPInspector(workflowFile string, serverFilter string, verbose bool) e // If workflow file is specified, extract MCP configurations and start servers if workflowFile != "" { - workflowsDir := constants.GetWorkflowDir() - - // Normalize the workflow file path - if !strings.HasSuffix(workflowFile, ".md") { - workflowFile += ".md" + // Resolve the workflow file path (supports shared workflows) + workflowPath, err := ResolveWorkflowPath(workflowFile) + if err != nil { + return err } - workflowPath := filepath.Join(workflowsDir, workflowFile) + // Convert to absolute path if needed if !filepath.IsAbs(workflowPath) { cwd, err := os.Getwd() if err != nil { @@ -372,11 +370,6 @@ func spawnMCPInspector(workflowFile string, serverFilter string, verbose bool) e workflowPath = filepath.Join(cwd, workflowPath) } - // Check if file exists - if _, err := os.Stat(workflowPath); os.IsNotExist(err) { - return fmt.Errorf("workflow file not found: %s", workflowPath) - } - // Parse the workflow file to extract MCP configurations content, err := os.ReadFile(workflowPath) if err != nil { From aa8758a662db935e9cfcc983be9010d6c2b7d4a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:09:27 +0000 Subject: [PATCH 4/5] Refactor ResolveWorkflowPath to support recursive subpath matching - Walk entire .github/workflows tree to find workflow files - Support full relative path matching (e.g., shared/mcp/serena.md) - Support subpath matching (e.g., mcp/serena matches shared/mcp/serena.md) - Support basename matching (e.g., serena matches any serena.md in subdirs) - Resolution priority: exact path > exact relative path > subpath match > basename match Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/resolver.go | 132 ++++++++++++++++++++++++--------------- pkg/cli/resolver_test.go | 38 +++++++++-- 2 files changed, 114 insertions(+), 56 deletions(-) diff --git a/pkg/cli/resolver.go b/pkg/cli/resolver.go index 053257f15c4..de2e7def825 100644 --- a/pkg/cli/resolver.go +++ b/pkg/cli/resolver.go @@ -10,64 +10,96 @@ import ( // ResolveWorkflowPath resolves a workflow file path from various formats: // - Absolute path to .md file // - Relative path to .md file -// - Workflow name (adds .md extension and looks in .github/workflows, shared/, and shared/mcp/) -// - Workflow name with .md extension +// - Workflow name with subpath (e.g., "shared/serena" or "shared/mcp/serena") +// - Workflow name (searches recursively in .github/workflows) +// +// Resolution order: +// 1. If path exists as-is, use it +// 2. Try exact relative path match under .github/workflows (e.g., "shared/b.md" -> ".github/workflows/shared/b.md") +// 3. Search recursively for files ending with the input path (subpath matching) +// 4. Search recursively for files with matching basename func ResolveWorkflowPath(workflowFile string) (string, error) { workflowsDir := ".github/workflows" - sharedDir := filepath.Join(workflowsDir, "shared") - sharedMCPDir := filepath.Join(sharedDir, "mcp") - - var workflowPath string - - if strings.HasSuffix(workflowFile, ".md") { - // If it's already a .md file, use it directly if it exists - if _, err := os.Stat(workflowFile); err == nil { - workflowPath = workflowFile - } else { - // Try in workflows directory first - workflowPath = filepath.Join(workflowsDir, workflowFile) - - // If not found, try shared directories - if _, err := os.Stat(workflowPath); os.IsNotExist(err) { - // Try shared directory - sharedPath := filepath.Join(sharedDir, workflowFile) - if _, err := os.Stat(sharedPath); err == nil { - workflowPath = sharedPath - } else { - // Try shared/mcp directory - sharedMCPPath := filepath.Join(sharedMCPDir, workflowFile) - if _, err := os.Stat(sharedMCPPath); err == nil { - workflowPath = sharedMCPPath - } - } - } + + // Add .md extension if not present + searchPath := workflowFile + if !strings.HasSuffix(searchPath, ".md") { + searchPath += ".md" + } + + // 1. If it's a path that exists as-is (absolute or relative), use it + if _, err := os.Stat(searchPath); err == nil { + return searchPath, nil + } + + // 2. Try exact relative path under .github/workflows + exactPath := filepath.Join(workflowsDir, searchPath) + if _, err := os.Stat(exactPath); err == nil { + return exactPath, nil + } + + // 3 & 4. Search recursively through .github/workflows + var matches []string + var exactSubpathMatches []string + + err := filepath.Walk(workflowsDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Skip errors, continue walking + } + + // Skip directories + if info.IsDir() { + return nil } - } else { - // Add .md extension and look in workflows directory first - workflowPath = filepath.Join(workflowsDir, workflowFile+".md") - - // If not found, try shared directories - if _, err := os.Stat(workflowPath); os.IsNotExist(err) { - // Try shared directory - sharedPath := filepath.Join(sharedDir, workflowFile+".md") - if _, err := os.Stat(sharedPath); err == nil { - workflowPath = sharedPath - } else { - // Try shared/mcp directory - sharedMCPPath := filepath.Join(sharedMCPDir, workflowFile+".md") - if _, err := os.Stat(sharedMCPPath); err == nil { - workflowPath = sharedMCPPath - } - } + + // Only consider .md files + if !strings.HasSuffix(path, ".md") { + return nil } + + // Get relative path from workflows directory + relPath, err := filepath.Rel(workflowsDir, path) + if err != nil { + return nil + } + + // Check for exact subpath match (e.g., "shared/mcp/serena.md" matches "shared/mcp/serena.md") + if relPath == searchPath { + exactSubpathMatches = append(exactSubpathMatches, path) + return nil + } + + // Check for suffix match (e.g., "serena.md" matches ".../shared/mcp/serena.md") + if strings.HasSuffix(relPath, searchPath) { + matches = append(matches, path) + return nil + } + + // Check for basename match (e.g., "serena.md" matches any "serena.md" in subdirs) + if filepath.Base(path) == filepath.Base(searchPath) { + matches = append(matches, path) + return nil + } + + return nil + }) + + if err != nil { + return "", fmt.Errorf("error searching for workflow file: %w", err) + } + + // Return exact subpath match if found (highest priority) + if len(exactSubpathMatches) > 0 { + return exactSubpathMatches[0], nil } - // Verify the workflow file exists - if _, err := os.Stat(workflowPath); os.IsNotExist(err) { - return "", fmt.Errorf("workflow file not found: %s", workflowPath) + // Return first match if any found + if len(matches) > 0 { + return matches[0], nil } - return workflowPath, nil + // No matches found + return "", fmt.Errorf("workflow file not found: %s", searchPath) } // NormalizeWorkflowFile normalizes a workflow file name by adding .md extension if missing diff --git a/pkg/cli/resolver_test.go b/pkg/cli/resolver_test.go index aeddb8b9669..b01721dd0fd 100644 --- a/pkg/cli/resolver_test.go +++ b/pkg/cli/resolver_test.go @@ -59,6 +59,12 @@ func TestResolveWorkflowPath(t *testing.T) { t.Fatalf("Failed to create MCP workflow: %v", err) } + // Create another file in shared/mcp + tavilyWorkflow := filepath.Join(sharedMCPDir, "tavily.md") + if err := os.WriteFile(tavilyWorkflow, []byte("# Tavily"), 0644); err != nil { + t.Fatalf("Failed to create Tavily workflow: %v", err) + } + tests := []struct { name string input string @@ -76,25 +82,45 @@ func TestResolveWorkflowPath(t *testing.T) { expected: testWorkflow, }, { - name: "workflow name without extension in shared dir", - input: "shared-workflow", + name: "full relative path to shared workflow", + input: "shared/shared-workflow.md", expected: sharedWorkflow, }, { - name: "workflow name with extension in shared dir", - input: "shared-workflow.md", + name: "full relative path to shared workflow without extension", + input: "shared/shared-workflow", expected: sharedWorkflow, }, { - name: "workflow name without extension in shared/mcp dir", + name: "full relative path to shared/mcp workflow", + input: "shared/mcp/serena.md", + expected: mcpWorkflow, + }, + { + name: "full relative path to shared/mcp workflow without extension", + input: "shared/mcp/serena", + expected: mcpWorkflow, + }, + { + name: "basename match for serena", input: "serena", expected: mcpWorkflow, }, { - name: "workflow name with extension in shared/mcp dir", + name: "basename match for serena with extension", input: "serena.md", expected: mcpWorkflow, }, + { + name: "basename match for tavily", + input: "tavily", + expected: tavilyWorkflow, + }, + { + name: "subpath match mcp/serena", + input: "mcp/serena", + expected: mcpWorkflow, + }, { name: "nonexistent workflow", input: "nonexistent", From dff9f0cd81199aa16b08c597f367356d5a587db4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:19:26 +0000 Subject: [PATCH 5/5] Remove recursive subpath matching from ResolveWorkflowPath Simplified to only support exact relative paths under .github/workflows: - a.md -> .github/workflows/a.md - shared/b.md -> .github/workflows/shared/b.md Removed recursive directory walking and fuzzy matching (basename/suffix). Users must provide full relative path to access nested workflows. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/resolver.go | 77 +++------------------------------------- pkg/cli/resolver_test.go | 28 ++++----------- 2 files changed, 11 insertions(+), 94 deletions(-) diff --git a/pkg/cli/resolver.go b/pkg/cli/resolver.go index de2e7def825..8916ca4b506 100644 --- a/pkg/cli/resolver.go +++ b/pkg/cli/resolver.go @@ -10,14 +10,7 @@ import ( // ResolveWorkflowPath resolves a workflow file path from various formats: // - Absolute path to .md file // - Relative path to .md file -// - Workflow name with subpath (e.g., "shared/serena" or "shared/mcp/serena") -// - Workflow name (searches recursively in .github/workflows) -// -// Resolution order: -// 1. If path exists as-is, use it -// 2. Try exact relative path match under .github/workflows (e.g., "shared/b.md" -> ".github/workflows/shared/b.md") -// 3. Search recursively for files ending with the input path (subpath matching) -// 4. Search recursively for files with matching basename +// - Workflow name or subpath (e.g., "a.md" -> ".github/workflows/a.md", "shared/b.md" -> ".github/workflows/shared/b.md") func ResolveWorkflowPath(workflowFile string) (string, error) { workflowsDir := ".github/workflows" @@ -33,73 +26,13 @@ func ResolveWorkflowPath(workflowFile string) (string, error) { } // 2. Try exact relative path under .github/workflows - exactPath := filepath.Join(workflowsDir, searchPath) - if _, err := os.Stat(exactPath); err == nil { - return exactPath, nil - } - - // 3 & 4. Search recursively through .github/workflows - var matches []string - var exactSubpathMatches []string - - err := filepath.Walk(workflowsDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil // Skip errors, continue walking - } - - // Skip directories - if info.IsDir() { - return nil - } - - // Only consider .md files - if !strings.HasSuffix(path, ".md") { - return nil - } - - // Get relative path from workflows directory - relPath, err := filepath.Rel(workflowsDir, path) - if err != nil { - return nil - } - - // Check for exact subpath match (e.g., "shared/mcp/serena.md" matches "shared/mcp/serena.md") - if relPath == searchPath { - exactSubpathMatches = append(exactSubpathMatches, path) - return nil - } - - // Check for suffix match (e.g., "serena.md" matches ".../shared/mcp/serena.md") - if strings.HasSuffix(relPath, searchPath) { - matches = append(matches, path) - return nil - } - - // Check for basename match (e.g., "serena.md" matches any "serena.md" in subdirs) - if filepath.Base(path) == filepath.Base(searchPath) { - matches = append(matches, path) - return nil - } - - return nil - }) - - if err != nil { - return "", fmt.Errorf("error searching for workflow file: %w", err) - } - - // Return exact subpath match if found (highest priority) - if len(exactSubpathMatches) > 0 { - return exactSubpathMatches[0], nil - } - - // Return first match if any found - if len(matches) > 0 { - return matches[0], nil + workflowPath := filepath.Join(workflowsDir, searchPath) + if _, err := os.Stat(workflowPath); err == nil { + return workflowPath, nil } // No matches found - return "", fmt.Errorf("workflow file not found: %s", searchPath) + return "", fmt.Errorf("workflow file not found: %s", workflowPath) } // NormalizeWorkflowFile normalizes a workflow file name by adding .md extension if missing diff --git a/pkg/cli/resolver_test.go b/pkg/cli/resolver_test.go index b01721dd0fd..509ba8a1c2f 100644 --- a/pkg/cli/resolver_test.go +++ b/pkg/cli/resolver_test.go @@ -59,12 +59,6 @@ func TestResolveWorkflowPath(t *testing.T) { t.Fatalf("Failed to create MCP workflow: %v", err) } - // Create another file in shared/mcp - tavilyWorkflow := filepath.Join(sharedMCPDir, "tavily.md") - if err := os.WriteFile(tavilyWorkflow, []byte("# Tavily"), 0644); err != nil { - t.Fatalf("Failed to create Tavily workflow: %v", err) - } - tests := []struct { name string input string @@ -102,24 +96,14 @@ func TestResolveWorkflowPath(t *testing.T) { expected: mcpWorkflow, }, { - name: "basename match for serena", - input: "serena", - expected: mcpWorkflow, - }, - { - name: "basename match for serena with extension", - input: "serena.md", - expected: mcpWorkflow, - }, - { - name: "basename match for tavily", - input: "tavily", - expected: tavilyWorkflow, + name: "basename only (no recursive matching)", + input: "serena", + expectError: true, }, { - name: "subpath match mcp/serena", - input: "mcp/serena", - expected: mcpWorkflow, + name: "partial subpath (no recursive matching)", + input: "mcp/serena", + expectError: true, }, { name: "nonexistent workflow",