From a4d0cfa00c9748a7cd0552964411b622d66022ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:24:31 +0000 Subject: [PATCH 1/7] Initial plan From b88fbb3a799faa9340c9e98a6e8e5235b278973e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:31:47 +0000 Subject: [PATCH 2/7] Initial exploration: understand copilot agent MCP configuration Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/go-logger.lock.yml | 2 +- .github/workflows/issue-classifier.lock.yml | 2 +- .github/workflows/technical-doc-writer.lock.yml | 2 +- .github/workflows/tidy.lock.yml | 2 +- .github/workflows/unbloat-docs.lock.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index a62fc2d4dcb..b75153f7944 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -88,7 +88,7 @@ jobs: with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 with: cache: npm cache-dependency-path: pkg/workflow/js/package-lock.json diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index b839b5016c5..3fbc1b1066a 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -1960,7 +1960,7 @@ jobs: path: /tmp/gh-aw/aw_info.json if-no-files-found: warn - name: Run AI Inference - uses: actions/ai-inference@v1 + uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 env: GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index d57b6453f0e..6f6dcedaf16 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -493,7 +493,7 @@ jobs: with: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 with: cache: npm cache-dependency-path: docs/package-lock.json diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index d4527907b50..573af9eaef0 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -452,7 +452,7 @@ jobs: with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 with: cache: npm cache-dependency-path: pkg/workflow/js/package-lock.json diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 0815416ee88..01f2b04e7fa 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -846,7 +846,7 @@ jobs: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 with: cache: npm cache-dependency-path: docs/package-lock.json From d632411a0eefad787c9fd2c32f6ed88028fdf473 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:39:20 +0000 Subject: [PATCH 3/7] Add --mcp flag to init command for Copilot Agent MCP configuration Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/go-logger.lock.yml | 2 +- .github/workflows/issue-classifier.lock.yml | 2 +- .../workflows/technical-doc-writer.lock.yml | 2 +- .github/workflows/tidy.lock.yml | 2 +- .github/workflows/unbloat-docs.lock.yml | 2 +- pkg/cli/copilot_setup.go | 110 ++++++++ pkg/cli/init.go | 29 +- pkg/cli/init_command.go | 16 +- pkg/cli/init_mcp_test.go | 264 ++++++++++++++++++ pkg/cli/init_test.go | 8 +- pkg/cli/mcp_config_file.go | 89 ++++++ 11 files changed, 513 insertions(+), 13 deletions(-) create mode 100644 pkg/cli/copilot_setup.go create mode 100644 pkg/cli/init_mcp_test.go create mode 100644 pkg/cli/mcp_config_file.go diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index b75153f7944..a62fc2d4dcb 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -88,7 +88,7 @@ jobs: with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: cache: npm cache-dependency-path: pkg/workflow/js/package-lock.json diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index 3fbc1b1066a..b839b5016c5 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -1960,7 +1960,7 @@ jobs: path: /tmp/gh-aw/aw_info.json if-no-files-found: warn - name: Run AI Inference - uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 + uses: actions/ai-inference@v1 env: GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 6f6dcedaf16..d57b6453f0e 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -493,7 +493,7 @@ jobs: with: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: cache: npm cache-dependency-path: docs/package-lock.json diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 573af9eaef0..d4527907b50 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -452,7 +452,7 @@ jobs: with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: cache: npm cache-dependency-path: pkg/workflow/js/package-lock.json diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 01f2b04e7fa..0815416ee88 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -846,7 +846,7 @@ jobs: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Setup Node.js - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: cache: npm cache-dependency-path: docs/package-lock.json diff --git a/pkg/cli/copilot_setup.go b/pkg/cli/copilot_setup.go new file mode 100644 index 00000000000..04b88bfebf4 --- /dev/null +++ b/pkg/cli/copilot_setup.go @@ -0,0 +1,110 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/githubnext/gh-aw/pkg/logger" +) + +var copilotSetupLog = logger.New("cli:copilot_setup") + +const copilotSetupStepsYAML = `name: "Copilot Setup Steps" + +# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set minimal permissions for setup steps + # Copilot Agent receives its own token with appropriate permissions + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Install gh CLI + run: | + if ! command -v gh &> /dev/null; then + echo "Installing GitHub CLI..." + type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) + curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ + && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y + else + echo "GitHub CLI already installed" + fi + + - name: Install gh-aw extension + run: | + # Install or update the gh-aw extension + if gh extension list | grep -q githubnext/gh-aw; then + echo "Upgrading gh-aw extension..." + gh extension upgrade githubnext/gh-aw + else + echo "Installing gh-aw extension..." + gh extension install githubnext/gh-aw + fi + env: + GH_TOKEN: ${{ github.token }} + + - name: Build gh-aw from source + run: | + echo "Building gh-aw from source for latest features..." + make build + continue-on-error: true + + - name: Verify gh-aw installation + run: | + gh aw version || ./gh-aw version +` + +// ensureCopilotSetupSteps creates or updates .github/workflows/copilot-setup-steps.yml +func ensureCopilotSetupSteps(verbose bool) error { + copilotSetupLog.Print("Creating copilot-setup-steps.yml") + + // Create .github/workflows directory if it doesn't exist + workflowsDir := filepath.Join(".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + return fmt.Errorf("failed to create workflows directory: %w", err) + } + copilotSetupLog.Printf("Ensured directory exists: %s", workflowsDir) + + // Write copilot-setup-steps.yml + setupStepsPath := filepath.Join(workflowsDir, "copilot-setup-steps.yml") + + // Check if file already exists + if _, err := os.Stat(setupStepsPath); err == nil { + copilotSetupLog.Printf("File already exists: %s", setupStepsPath) + // File exists, skip creation to avoid overwriting user customizations + if verbose { + fmt.Fprintf(os.Stderr, "Skipping %s (already exists)\n", setupStepsPath) + } + return nil + } + + if err := os.WriteFile(setupStepsPath, []byte(copilotSetupStepsYAML), 0644); err != nil { + return fmt.Errorf("failed to write copilot-setup-steps.yml: %w", err) + } + copilotSetupLog.Printf("Created file: %s", setupStepsPath) + + return nil +} diff --git a/pkg/cli/init.go b/pkg/cli/init.go index fb6a514fe82..ae81b32cf0c 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -12,7 +12,7 @@ import ( var initLog = logger.New("cli:init") // InitRepository initializes the repository for agentic workflows -func InitRepository(verbose bool) error { +func InitRepository(verbose bool, mcp bool) error { initLog.Print("Starting repository initialization for agentic workflows") // Ensure we're in a git repository @@ -72,12 +72,39 @@ func InitRepository(verbose bool) error { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created getting started guide")) } + // Configure MCP if requested + if mcp { + initLog.Print("Configuring GitHub Copilot Agent MCP integration") + + // Create copilot-setup-steps.yml + if err := ensureCopilotSetupSteps(verbose); err != nil { + initLog.Printf("Failed to create copilot-setup-steps.yml: %v", err) + return fmt.Errorf("failed to create copilot-setup-steps.yml: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created .github/workflows/copilot-setup-steps.yml")) + } + + // Create .vscode/mcp.json + if err := ensureMCPConfig(verbose); err != nil { + initLog.Printf("Failed to create MCP config: %v", err) + return fmt.Errorf("failed to create MCP config: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created .vscode/mcp.json")) + } + } + initLog.Print("Repository initialization completed successfully") // Display success message with next steps fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Repository initialized for agentic workflows!")) fmt.Fprintln(os.Stderr, "") + if mcp { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("✓ GitHub Copilot Agent MCP integration configured")) + fmt.Fprintln(os.Stderr, "") + } fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Start a chat and copy the following prompt to create a new workflow:")) fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, " activate @.github/prompts/create-agentic-workflow.prompt.md") diff --git a/pkg/cli/init_command.go b/pkg/cli/init_command.go index 0e6d2d2dc3b..5422f51ff48 100644 --- a/pkg/cli/init_command.go +++ b/pkg/cli/init_command.go @@ -11,7 +11,7 @@ import ( // NewInitCommand creates the init command func NewInitCommand() *cobra.Command { - return &cobra.Command{ + cmd := &cobra.Command{ Use: "init", Short: "Initialize repository for agentic workflows", Long: `Initialize the repository for agentic workflows by configuring .gitattributes and creating GitHub Copilot instruction files. @@ -21,6 +21,10 @@ This command: - Creates GitHub Copilot custom instructions at .github/instructions/github-agentic-workflows.instructions.md - Creates the /create-agentic-workflow prompt at .github/prompts/create-agentic-workflow.prompt.md +With --mcp flag: +- Creates .github/workflows/copilot-setup-steps.yml with gh-aw installation steps +- Creates .vscode/mcp.json with gh-aw MCP server configuration + After running this command, you can: - Use GitHub Copilot Chat with /create-agentic-workflow to create workflows interactively - Add workflows from the catalog with: ` + constants.CLIExtensionPrefix + ` add @@ -28,13 +32,19 @@ After running this command, you can: Examples: ` + constants.CLIExtensionPrefix + ` init - ` + constants.CLIExtensionPrefix + ` init -v`, + ` + constants.CLIExtensionPrefix + ` init -v + ` + constants.CLIExtensionPrefix + ` init --mcp`, Run: func(cmd *cobra.Command, args []string) { verbose, _ := cmd.Flags().GetBool("verbose") - if err := InitRepository(verbose); err != nil { + mcp, _ := cmd.Flags().GetBool("mcp") + if err := InitRepository(verbose, mcp); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } }, } + + cmd.Flags().Bool("mcp", false, "Configure GitHub Copilot Agent MCP server integration") + + return cmd } diff --git a/pkg/cli/init_mcp_test.go b/pkg/cli/init_mcp_test.go new file mode 100644 index 00000000000..c612f6105c7 --- /dev/null +++ b/pkg/cli/init_mcp_test.go @@ -0,0 +1,264 @@ +package cli + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestInitRepository_WithMCP(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(oldWd) + }() + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // Initialize git repo + if err := exec.Command("git", "init").Run(); err != nil { + t.Fatalf("Failed to init git repo: %v", err) + } + + // Create go.mod for the copilot-setup-steps.yml to reference + goModContent := []byte("module github.com/test/repo\n\ngo 1.23\n") + if err := os.WriteFile("go.mod", goModContent, 0644); err != nil { + t.Fatalf("Failed to create go.mod: %v", err) + } + + // Call the function with MCP flag + err = InitRepository(false, true) + if err != nil { + t.Fatalf("InitRepository() with MCP returned error: %v", err) + } + + // Verify standard files were created + gitAttributesPath := filepath.Join(tempDir, ".gitattributes") + if _, err := os.Stat(gitAttributesPath); os.IsNotExist(err) { + t.Errorf("Expected .gitattributes file to exist") + } + + // Verify copilot-setup-steps.yml was created + setupStepsPath := filepath.Join(tempDir, ".github", "workflows", "copilot-setup-steps.yml") + if _, err := os.Stat(setupStepsPath); os.IsNotExist(err) { + t.Errorf("Expected copilot-setup-steps.yml to exist") + } else { + // Verify content contains key elements + content, err := os.ReadFile(setupStepsPath) + if err != nil { + t.Fatalf("Failed to read copilot-setup-steps.yml: %v", err) + } + contentStr := string(content) + + if !strings.Contains(contentStr, "name: \"Copilot Setup Steps\"") { + t.Errorf("Expected copilot-setup-steps.yml to contain workflow name") + } + if !strings.Contains(contentStr, "copilot-setup-steps:") { + t.Errorf("Expected copilot-setup-steps.yml to contain job name") + } + if !strings.Contains(contentStr, "gh extension install githubnext/gh-aw") { + t.Errorf("Expected copilot-setup-steps.yml to contain gh-aw installation steps") + } + } + + // Verify .vscode/mcp.json was created + mcpConfigPath := filepath.Join(tempDir, ".vscode", "mcp.json") + if _, err := os.Stat(mcpConfigPath); os.IsNotExist(err) { + t.Errorf("Expected .vscode/mcp.json to exist") + } else { + // Verify content is valid JSON with gh-aw server + content, err := os.ReadFile(mcpConfigPath) + if err != nil { + t.Fatalf("Failed to read .vscode/mcp.json: %v", err) + } + + var config MCPConfig + if err := json.Unmarshal(content, &config); err != nil { + t.Fatalf("Failed to parse .vscode/mcp.json: %v", err) + } + + if _, exists := config.Servers["github-agentic-workflows"]; !exists { + t.Errorf("Expected .vscode/mcp.json to contain github-agentic-workflows server") + } + + server := config.Servers["github-agentic-workflows"] + if server.Command != "gh" { + t.Errorf("Expected command to be 'gh', got %s", server.Command) + } + if len(server.Args) != 2 || server.Args[0] != "aw" || server.Args[1] != "mcp-server" { + t.Errorf("Expected args to be ['aw', 'mcp-server'], got %v", server.Args) + } + } +} + +func TestInitRepository_MCP_Idempotent(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(oldWd) + }() + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // Initialize git repo + if err := exec.Command("git", "init").Run(); err != nil { + t.Fatalf("Failed to init git repo: %v", err) + } + + // Create go.mod + goModContent := []byte("module github.com/test/repo\n\ngo 1.23\n") + if err := os.WriteFile("go.mod", goModContent, 0644); err != nil { + t.Fatalf("Failed to create go.mod: %v", err) + } + + // Call the function first time with MCP + err = InitRepository(false, true) + if err != nil { + t.Fatalf("InitRepository() with MCP returned error on first call: %v", err) + } + + // Call the function second time with MCP + err = InitRepository(false, true) + if err != nil { + t.Fatalf("InitRepository() with MCP returned error on second call: %v", err) + } + + // Verify files still exist + setupStepsPath := filepath.Join(tempDir, ".github", "workflows", "copilot-setup-steps.yml") + if _, err := os.Stat(setupStepsPath); os.IsNotExist(err) { + t.Errorf("Expected copilot-setup-steps.yml to exist after second call") + } + + mcpConfigPath := filepath.Join(tempDir, ".vscode", "mcp.json") + if _, err := os.Stat(mcpConfigPath); os.IsNotExist(err) { + t.Errorf("Expected .vscode/mcp.json to exist after second call") + } +} + +func TestEnsureMCPConfig_UpdatesExisting(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(oldWd) + }() + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // Create .vscode directory + if err := os.MkdirAll(".vscode", 0755); err != nil { + t.Fatalf("Failed to create .vscode directory: %v", err) + } + + // Create initial mcp.json with a different server + initialConfig := MCPConfig{ + Servers: map[string]MCPServerConfig{ + "other-server": { + Command: "other-command", + Args: []string{"arg1"}, + }, + }, + } + initialData, _ := json.MarshalIndent(initialConfig, "", " ") + mcpConfigPath := filepath.Join(tempDir, ".vscode", "mcp.json") + if err := os.WriteFile(mcpConfigPath, initialData, 0644); err != nil { + t.Fatalf("Failed to write initial mcp.json: %v", err) + } + + // Call ensureMCPConfig + if err := ensureMCPConfig(false); err != nil { + t.Fatalf("ensureMCPConfig() returned error: %v", err) + } + + // Verify the config now contains both servers + content, err := os.ReadFile(mcpConfigPath) + if err != nil { + t.Fatalf("Failed to read mcp.json: %v", err) + } + + var config MCPConfig + if err := json.Unmarshal(content, &config); err != nil { + t.Fatalf("Failed to parse mcp.json: %v", err) + } + + // Check both servers exist + if _, exists := config.Servers["other-server"]; !exists { + t.Errorf("Expected existing 'other-server' to be preserved") + } + + if _, exists := config.Servers["github-agentic-workflows"]; !exists { + t.Errorf("Expected 'github-agentic-workflows' server to be added") + } +} + +func TestEnsureCopilotSetupSteps_SkipsExisting(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(oldWd) + }() + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // Create .github/workflows directory + workflowsDir := filepath.Join(".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create workflows directory: %v", err) + } + + // Create custom copilot-setup-steps.yml + setupStepsPath := filepath.Join(workflowsDir, "copilot-setup-steps.yml") + customContent := []byte("# Custom setup steps\nname: Custom\n") + if err := os.WriteFile(setupStepsPath, customContent, 0644); err != nil { + t.Fatalf("Failed to write custom setup steps: %v", err) + } + + // Call ensureCopilotSetupSteps + if err := ensureCopilotSetupSteps(false); err != nil { + t.Fatalf("ensureCopilotSetupSteps() returned error: %v", err) + } + + // Verify the file was not overwritten + content, err := os.ReadFile(setupStepsPath) + if err != nil { + t.Fatalf("Failed to read setup steps file: %v", err) + } + + if string(content) != string(customContent) { + t.Errorf("Expected custom content to be preserved, but it was overwritten") + } +} diff --git a/pkg/cli/init_test.go b/pkg/cli/init_test.go index 6630f7d6acc..1c8a1a5420f 100644 --- a/pkg/cli/init_test.go +++ b/pkg/cli/init_test.go @@ -52,7 +52,7 @@ func TestInitRepository(t *testing.T) { } // Call the function - err = InitRepository(false) + err = InitRepository(false, false) // Check error expectation if tt.wantError { @@ -119,13 +119,13 @@ func TestInitRepository_Idempotent(t *testing.T) { } // Call the function first time - err = InitRepository(false) + err = InitRepository(false, false) if err != nil { t.Fatalf("InitRepository() returned error on first call: %v", err) } // Call the function second time - err = InitRepository(false) + err = InitRepository(false, false) if err != nil { t.Fatalf("InitRepository() returned error on second call: %v", err) } @@ -170,7 +170,7 @@ func TestInitRepository_Verbose(t *testing.T) { } // Call the function with verbose=true (should not error) - err = InitRepository(true) + err = InitRepository(true, false) if err != nil { t.Fatalf("InitRepository() returned error with verbose=true: %v", err) } diff --git a/pkg/cli/mcp_config_file.go b/pkg/cli/mcp_config_file.go new file mode 100644 index 00000000000..14b5a2c6594 --- /dev/null +++ b/pkg/cli/mcp_config_file.go @@ -0,0 +1,89 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/githubnext/gh-aw/pkg/logger" +) + +var mcpConfigLog = logger.New("cli:mcp_config_file") + +// MCPServerConfig represents a single MCP server configuration +type MCPServerConfig struct { + Command string `json:"command"` + Args []string `json:"args"` + CWD string `json:"cwd,omitempty"` +} + +// MCPConfig represents the structure of mcp.json +type MCPConfig struct { + Servers map[string]MCPServerConfig `json:"servers"` +} + +// ensureMCPConfig creates or updates .vscode/mcp.json with gh-aw MCP server configuration +func ensureMCPConfig(verbose bool) error { + mcpConfigLog.Print("Creating or updating .vscode/mcp.json") + + // Create .vscode directory if it doesn't exist + vscodeDir := ".vscode" + if err := os.MkdirAll(vscodeDir, 0755); err != nil { + return fmt.Errorf("failed to create .vscode directory: %w", err) + } + mcpConfigLog.Printf("Ensured directory exists: %s", vscodeDir) + + mcpConfigPath := filepath.Join(vscodeDir, "mcp.json") + + // Read existing config if it exists + var config MCPConfig + if data, err := os.ReadFile(mcpConfigPath); err == nil { + mcpConfigLog.Printf("Reading existing config from: %s", mcpConfigPath) + if err := json.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse existing mcp.json: %w", err) + } + } else { + mcpConfigLog.Print("No existing config found, creating new one") + config.Servers = make(map[string]MCPServerConfig) + } + + // Add or update gh-aw MCP server configuration + ghAwServerName := "github-agentic-workflows" + ghAwConfig := MCPServerConfig{ + Command: "gh", + Args: []string{"aw", "mcp-server"}, + CWD: "${workspaceFolder}", + } + + // Check if the server is already configured + if existingConfig, exists := config.Servers[ghAwServerName]; exists { + mcpConfigLog.Printf("Server '%s' already exists in config", ghAwServerName) + // Check if configuration is different + existingJSON, _ := json.Marshal(existingConfig) + newJSON, _ := json.Marshal(ghAwConfig) + if string(existingJSON) == string(newJSON) { + mcpConfigLog.Print("Configuration is identical, skipping update") + if verbose { + fmt.Fprintf(os.Stderr, "MCP server '%s' already configured in %s\n", ghAwServerName, mcpConfigPath) + } + return nil + } + mcpConfigLog.Print("Configuration differs, updating") + } + + config.Servers[ghAwServerName] = ghAwConfig + + // Write config file with proper indentation + data, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal mcp.json: %w", err) + } + + if err := os.WriteFile(mcpConfigPath, data, 0644); err != nil { + return fmt.Errorf("failed to write mcp.json: %w", err) + } + mcpConfigLog.Printf("Wrote config to: %s", mcpConfigPath) + + return nil +} From 88e7591d6193b1af2a4d0e8316c4877201129193 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:43:51 +0000 Subject: [PATCH 4/7] Update documentation for --mcp flag in init command Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/tools/cli.md | 11 +++++++ docs/src/content/docs/tools/mcp-server.md | 36 ++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/tools/cli.md b/docs/src/content/docs/tools/cli.md index d9e07217721..c6106975a88 100644 --- a/docs/src/content/docs/tools/cli.md +++ b/docs/src/content/docs/tools/cli.md @@ -86,8 +86,19 @@ The `init` command prepares your repository for agentic workflows by configuring ```bash gh aw init +gh aw init --mcp # Configure GitHub Copilot Agent MCP integration ``` +**What it does:** +- Configures `.gitattributes` to mark `.lock.yml` files as generated +- Creates GitHub Copilot custom instructions at `.github/instructions/github-agentic-workflows.instructions.md` +- Creates the `/create-agentic-workflow` prompt at `.github/prompts/create-agentic-workflow.prompt.md` + +**With `--mcp` flag:** +- Creates `.github/workflows/copilot-setup-steps.yml` with steps to install the gh-aw extension +- Creates `.vscode/mcp.json` with gh-aw MCP server configuration +- Enables the gh-aw MCP server in GitHub Copilot Agent, providing tools like `status`, `compile`, `logs`, and `audit` + After initialization, start a chat with an AI agent and use the following prompt to create a new workflow: ``` diff --git a/docs/src/content/docs/tools/mcp-server.md b/docs/src/content/docs/tools/mcp-server.md index 37c383e26a4..9419a061966 100644 --- a/docs/src/content/docs/tools/mcp-server.md +++ b/docs/src/content/docs/tools/mcp-server.md @@ -60,6 +60,26 @@ Use the `--port` flag to run the server with HTTP/SSE transport instead of stdio gh aw mcp-server --port 8080 ``` +## Configuring with GitHub Copilot Agent + +GitHub Copilot Agent can use the gh-aw MCP server in your workflows to manage agentic workflows directly. + +**Quick Setup:** + +Use the `init` command with the `--mcp` flag to automatically configure GitHub Copilot Agent: + +```bash +gh aw init --mcp +``` + +This creates `.github/workflows/copilot-setup-steps.yml` with: +- Go environment setup +- GitHub CLI installation +- gh-aw extension installation and build from source +- Verification steps + +The workflow runs before the Copilot Agent starts, ensuring gh-aw is available as an MCP server in all agent sessions. Once configured, the agent can use tools like `status`, `compile`, `logs`, and `audit` to manage workflows in your repository. + ## Configuring with Copilot CLI The GitHub Copilot CLI can use the gh-aw MCP server to access workflow management tools. @@ -76,7 +96,21 @@ This registers the server with Copilot CLI, making workflow management tools ava VS Code can use the gh-aw MCP server through the Copilot Chat extension. -Create or update `.vscode/mcp.json` in your repository: +**Quick Setup:** + +Use the `init` command with the `--mcp` flag to automatically configure VS Code and GitHub Copilot Agent: + +```bash +gh aw init --mcp +``` + +This creates: +- `.vscode/mcp.json` with gh-aw MCP server configuration +- `.github/workflows/copilot-setup-steps.yml` with gh-aw installation steps for GitHub Copilot Agent + +**Manual Configuration:** + +Alternatively, create or update `.vscode/mcp.json` in your repository: ```json { From 22ff7329ab352865f6215fb9773ac8e362ba06ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:43:21 +0000 Subject: [PATCH 5/7] Address PR feedback: remove gh CLI install, simplify extension install, add step injection - Remove "Install gh CLI" step (pre-installed in GitHub Actions) - Simplify extension installation to just install (no upgrade check) - Add logic to inject extension install step into existing files - Update tests to match new behavior Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/copilot_setup.go | 162 +++++++++++++++++++++++++++++++++------ pkg/cli/init_mcp_test.go | 107 ++++++++++++++++++++++++-- 2 files changed, 237 insertions(+), 32 deletions(-) diff --git a/pkg/cli/copilot_setup.go b/pkg/cli/copilot_setup.go index 04b88bfebf4..1cb8b252dfc 100644 --- a/pkg/cli/copilot_setup.go +++ b/pkg/cli/copilot_setup.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/githubnext/gh-aw/pkg/logger" ) @@ -39,30 +40,8 @@ jobs: go-version-file: go.mod cache: true - - name: Install gh CLI - run: | - if ! command -v gh &> /dev/null; then - echo "Installing GitHub CLI..." - type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) - curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ - && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && sudo apt update \ - && sudo apt install gh -y - else - echo "GitHub CLI already installed" - fi - - name: Install gh-aw extension - run: | - # Install or update the gh-aw extension - if gh extension list | grep -q githubnext/gh-aw; then - echo "Upgrading gh-aw extension..." - gh extension upgrade githubnext/gh-aw - else - echo "Installing gh-aw extension..." - gh extension install githubnext/gh-aw - fi + run: gh extension install githubnext/gh-aw env: GH_TOKEN: ${{ github.token }} @@ -94,9 +73,36 @@ func ensureCopilotSetupSteps(verbose bool) error { // Check if file already exists if _, err := os.Stat(setupStepsPath); err == nil { copilotSetupLog.Printf("File already exists: %s", setupStepsPath) - // File exists, skip creation to avoid overwriting user customizations + + // Read existing file to check if extension install step exists + content, err := os.ReadFile(setupStepsPath) + if err != nil { + return fmt.Errorf("failed to read existing copilot-setup-steps.yml: %w", err) + } + + contentStr := string(content) + + // Check if the extension install step is already present + if strings.Contains(contentStr, "gh extension install githubnext/gh-aw") || + strings.Contains(contentStr, "Install gh-aw extension") { + copilotSetupLog.Print("Extension install step already exists, skipping update") + if verbose { + fmt.Fprintf(os.Stderr, "Skipping %s (already has gh-aw extension install step)\n", setupStepsPath) + } + return nil + } + + // Inject the extension install step + copilotSetupLog.Print("Injecting extension install step into existing file") + updatedContent := injectExtensionInstallStep(contentStr) + + if err := os.WriteFile(setupStepsPath, []byte(updatedContent), 0644); err != nil { + return fmt.Errorf("failed to update copilot-setup-steps.yml: %w", err) + } + copilotSetupLog.Printf("Updated file with extension install step: %s", setupStepsPath) + if verbose { - fmt.Fprintf(os.Stderr, "Skipping %s (already exists)\n", setupStepsPath) + fmt.Fprintf(os.Stderr, "Updated %s with gh-aw extension install step\n", setupStepsPath) } return nil } @@ -108,3 +114,109 @@ func ensureCopilotSetupSteps(verbose bool) error { return nil } + +// injectExtensionInstallStep injects the gh-aw extension install step into an existing workflow +func injectExtensionInstallStep(content string) string { + // Define the extension install step to inject + extensionStep := ` - name: Install gh-aw extension + run: gh extension install githubnext/gh-aw + env: + GH_TOKEN: ${{ github.token }}` + + // Try to inject after "Set up Go" step + lines := strings.Split(content, "\n") + var result []string + injected := false + + for i := 0; i < len(lines); i++ { + line := lines[i] + result = append(result, line) + + // If we find "Set up Go" and haven't injected yet + if !injected && strings.Contains(line, "- name: Set up Go") { + // Find the end of this step (next "- name:" at the same or less indentation) + stepIndent := len(line) - len(strings.TrimLeft(line, " ")) + + j := i + 1 + for j < len(lines) { + nextLine := lines[j] + if strings.TrimSpace(nextLine) == "" { + result = append(result, nextLine) + j++ + continue + } + + nextIndent := len(nextLine) - len(strings.TrimLeft(nextLine, " ")) + if strings.HasPrefix(strings.TrimSpace(nextLine), "- name:") && nextIndent <= stepIndent { + // Found the next step at same level, inject before it + result = append(result, "") + result = append(result, extensionStep) + injected = true + i = j - 1 // Will be incremented in the loop + break + } + result = append(result, nextLine) + j++ + } + + // If we reached the end without finding another step + if j >= len(lines) && !injected { + result = append(result, "") + result = append(result, extensionStep) + injected = true + break + } + } + } + + if !injected { + // Fallback: try to inject after checkout step + result = []string{} + for i := 0; i < len(lines); i++ { + line := lines[i] + result = append(result, line) + + if strings.Contains(line, "- name: Checkout code") || strings.Contains(line, "actions/checkout@") { + // Find the end of checkout step + stepIndent := len(line) - len(strings.TrimLeft(line, " ")) + + j := i + 1 + for j < len(lines) { + nextLine := lines[j] + if strings.TrimSpace(nextLine) == "" { + result = append(result, nextLine) + j++ + continue + } + + nextIndent := len(nextLine) - len(strings.TrimLeft(nextLine, " ")) + if strings.HasPrefix(strings.TrimSpace(nextLine), "- name:") && nextIndent <= stepIndent { + // Found the next step, inject before it + result = append(result, "") + result = append(result, extensionStep) + injected = true + i = j - 1 + break + } + result = append(result, nextLine) + j++ + } + + if j >= len(lines) && !injected { + result = append(result, "") + result = append(result, extensionStep) + injected = true + } + break + } + } + } + + // If still not injected, append at the end + if !injected { + result = append(result, "") + result = append(result, extensionStep) + } + + return strings.Join(result, "\n") +} diff --git a/pkg/cli/init_mcp_test.go b/pkg/cli/init_mcp_test.go index c612f6105c7..24d15b5d59b 100644 --- a/pkg/cli/init_mcp_test.go +++ b/pkg/cli/init_mcp_test.go @@ -217,7 +217,7 @@ func TestEnsureMCPConfig_UpdatesExisting(t *testing.T) { } } -func TestEnsureCopilotSetupSteps_SkipsExisting(t *testing.T) { +func TestEnsureCopilotSetupSteps_InjectsStep(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() @@ -240,10 +240,26 @@ func TestEnsureCopilotSetupSteps_SkipsExisting(t *testing.T) { t.Fatalf("Failed to create workflows directory: %v", err) } - // Create custom copilot-setup-steps.yml + // Create custom copilot-setup-steps.yml without extension install step setupStepsPath := filepath.Join(workflowsDir, "copilot-setup-steps.yml") - customContent := []byte("# Custom setup steps\nname: Custom\n") - if err := os.WriteFile(setupStepsPath, customContent, 0644); err != nil { + customContent := `name: "Copilot Setup Steps" + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build code + run: make build +` + if err := os.WriteFile(setupStepsPath, []byte(customContent), 0644); err != nil { t.Fatalf("Failed to write custom setup steps: %v", err) } @@ -252,13 +268,90 @@ func TestEnsureCopilotSetupSteps_SkipsExisting(t *testing.T) { t.Fatalf("ensureCopilotSetupSteps() returned error: %v", err) } - // Verify the file was not overwritten + // Verify the extension install step was injected content, err := os.ReadFile(setupStepsPath) if err != nil { t.Fatalf("Failed to read setup steps file: %v", err) } - if string(content) != string(customContent) { - t.Errorf("Expected custom content to be preserved, but it was overwritten") + contentStr := string(content) + if !strings.Contains(contentStr, "Install gh-aw extension") { + t.Errorf("Expected extension install step to be injected") + } + if !strings.Contains(contentStr, "gh extension install githubnext/gh-aw") { + t.Errorf("Expected extension install command to be present") + } + + // Verify it was injected after Set up Go step + goIndex := strings.Index(contentStr, "Set up Go") + extensionIndex := strings.Index(contentStr, "Install gh-aw extension") + buildIndex := strings.Index(contentStr, "Build code") + + if goIndex == -1 || extensionIndex == -1 || buildIndex == -1 { + t.Fatalf("Could not find expected steps in file") + } + + if !(goIndex < extensionIndex && extensionIndex < buildIndex) { + t.Errorf("Extension install step not in correct position (should be after Go setup, before Build)") + } +} + +func TestEnsureCopilotSetupSteps_SkipsWhenStepExists(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Change to temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(oldWd) + }() + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + // Create .github/workflows directory + workflowsDir := filepath.Join(".github", "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create workflows directory: %v", err) + } + + // Create copilot-setup-steps.yml that already has the extension install step + setupStepsPath := filepath.Join(workflowsDir, "copilot-setup-steps.yml") + customContent := `name: "Copilot Setup Steps" + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install gh-aw extension + run: gh extension install githubnext/gh-aw + + - name: Build code + run: make build +` + if err := os.WriteFile(setupStepsPath, []byte(customContent), 0644); err != nil { + t.Fatalf("Failed to write custom setup steps: %v", err) + } + + // Call ensureCopilotSetupSteps + if err := ensureCopilotSetupSteps(false); err != nil { + t.Fatalf("ensureCopilotSetupSteps() returned error: %v", err) + } + + // Verify the file was not modified (content should be the same) + content, err := os.ReadFile(setupStepsPath) + if err != nil { + t.Fatalf("Failed to read setup steps file: %v", err) + } + + if string(content) != customContent { + t.Errorf("Expected file to remain unchanged when extension step already exists") } } From f14b8212336ebaf9deedb11400198868a03d79ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:28:28 +0000 Subject: [PATCH 6/7] Refactor step injection to use YAML unmarshalling/marshalling - Replace string manipulation with proper YAML parsing - Add WorkflowStep, WorkflowJob, and Workflow structs - Use goccy/go-yaml for unmarshalling and marshalling - Improves reliability and maintainability of step injection Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/copilot_setup.go | 174 ++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 95 deletions(-) diff --git a/pkg/cli/copilot_setup.go b/pkg/cli/copilot_setup.go index 1cb8b252dfc..81b075e079e 100644 --- a/pkg/cli/copilot_setup.go +++ b/pkg/cli/copilot_setup.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/githubnext/gh-aw/pkg/logger" + "github.com/goccy/go-yaml" ) var copilotSetupLog = logger.New("cli:copilot_setup") @@ -56,6 +57,29 @@ jobs: gh aw version || ./gh-aw version ` +// WorkflowStep represents a GitHub Actions workflow step +type WorkflowStep struct { + Name string `yaml:"name,omitempty"` + Uses string `yaml:"uses,omitempty"` + Run string `yaml:"run,omitempty"` + With map[string]any `yaml:"with,omitempty"` + Env map[string]any `yaml:"env,omitempty"` +} + +// WorkflowJob represents a GitHub Actions workflow job +type WorkflowJob struct { + RunsOn any `yaml:"runs-on,omitempty"` + Permissions map[string]any `yaml:"permissions,omitempty"` + Steps []WorkflowStep `yaml:"steps,omitempty"` +} + +// Workflow represents a GitHub Actions workflow file +type Workflow struct { + Name string `yaml:"name,omitempty"` + On any `yaml:"on,omitempty"` + Jobs map[string]WorkflowJob `yaml:"jobs,omitempty"` +} + // ensureCopilotSetupSteps creates or updates .github/workflows/copilot-setup-steps.yml func ensureCopilotSetupSteps(verbose bool) error { copilotSetupLog.Print("Creating copilot-setup-steps.yml") @@ -80,9 +104,8 @@ func ensureCopilotSetupSteps(verbose bool) error { return fmt.Errorf("failed to read existing copilot-setup-steps.yml: %w", err) } + // Check if the extension install step is already present (quick check) contentStr := string(content) - - // Check if the extension install step is already present if strings.Contains(contentStr, "gh extension install githubnext/gh-aw") || strings.Contains(contentStr, "Install gh-aw extension") { copilotSetupLog.Print("Extension install step already exists, skipping update") @@ -92,11 +115,25 @@ func ensureCopilotSetupSteps(verbose bool) error { return nil } + // Parse existing workflow + var workflow Workflow + if err := yaml.Unmarshal(content, &workflow); err != nil { + return fmt.Errorf("failed to parse existing copilot-setup-steps.yml: %w", err) + } + // Inject the extension install step copilotSetupLog.Print("Injecting extension install step into existing file") - updatedContent := injectExtensionInstallStep(contentStr) + if err := injectExtensionInstallStep(&workflow); err != nil { + return fmt.Errorf("failed to inject extension install step: %w", err) + } + + // Marshal back to YAML + updatedContent, err := yaml.Marshal(&workflow) + if err != nil { + return fmt.Errorf("failed to marshal updated workflow: %w", err) + } - if err := os.WriteFile(setupStepsPath, []byte(updatedContent), 0644); err != nil { + if err := os.WriteFile(setupStepsPath, updatedContent, 0644); err != nil { return fmt.Errorf("failed to update copilot-setup-steps.yml: %w", err) } copilotSetupLog.Printf("Updated file with extension install step: %s", setupStepsPath) @@ -116,107 +153,54 @@ func ensureCopilotSetupSteps(verbose bool) error { } // injectExtensionInstallStep injects the gh-aw extension install step into an existing workflow -func injectExtensionInstallStep(content string) string { +func injectExtensionInstallStep(workflow *Workflow) error { // Define the extension install step to inject - extensionStep := ` - name: Install gh-aw extension - run: gh extension install githubnext/gh-aw - env: - GH_TOKEN: ${{ github.token }}` - - // Try to inject after "Set up Go" step - lines := strings.Split(content, "\n") - var result []string - injected := false - - for i := 0; i < len(lines); i++ { - line := lines[i] - result = append(result, line) - - // If we find "Set up Go" and haven't injected yet - if !injected && strings.Contains(line, "- name: Set up Go") { - // Find the end of this step (next "- name:" at the same or less indentation) - stepIndent := len(line) - len(strings.TrimLeft(line, " ")) - - j := i + 1 - for j < len(lines) { - nextLine := lines[j] - if strings.TrimSpace(nextLine) == "" { - result = append(result, nextLine) - j++ - continue - } - - nextIndent := len(nextLine) - len(strings.TrimLeft(nextLine, " ")) - if strings.HasPrefix(strings.TrimSpace(nextLine), "- name:") && nextIndent <= stepIndent { - // Found the next step at same level, inject before it - result = append(result, "") - result = append(result, extensionStep) - injected = true - i = j - 1 // Will be incremented in the loop - break - } - result = append(result, nextLine) - j++ - } + extensionStep := WorkflowStep{ + Name: "Install gh-aw extension", + Run: "gh extension install githubnext/gh-aw", + Env: map[string]any{ + "GH_TOKEN": "${{ github.token }}", + }, + } - // If we reached the end without finding another step - if j >= len(lines) && !injected { - result = append(result, "") - result = append(result, extensionStep) - injected = true - break - } + // Find the copilot-setup-steps job + job, exists := workflow.Jobs["copilot-setup-steps"] + if !exists { + return fmt.Errorf("copilot-setup-steps job not found in workflow") + } + + // Find the position to insert the step (after "Set up Go" or after "Checkout code") + insertPosition := -1 + for i, step := range job.Steps { + if strings.Contains(step.Name, "Set up Go") { + insertPosition = i + 1 + break } } - if !injected { - // Fallback: try to inject after checkout step - result = []string{} - for i := 0; i < len(lines); i++ { - line := lines[i] - result = append(result, line) - - if strings.Contains(line, "- name: Checkout code") || strings.Contains(line, "actions/checkout@") { - // Find the end of checkout step - stepIndent := len(line) - len(strings.TrimLeft(line, " ")) - - j := i + 1 - for j < len(lines) { - nextLine := lines[j] - if strings.TrimSpace(nextLine) == "" { - result = append(result, nextLine) - j++ - continue - } - - nextIndent := len(nextLine) - len(strings.TrimLeft(nextLine, " ")) - if strings.HasPrefix(strings.TrimSpace(nextLine), "- name:") && nextIndent <= stepIndent { - // Found the next step, inject before it - result = append(result, "") - result = append(result, extensionStep) - injected = true - i = j - 1 - break - } - result = append(result, nextLine) - j++ - } - - if j >= len(lines) && !injected { - result = append(result, "") - result = append(result, extensionStep) - injected = true - } + // If Set up Go not found, try after Checkout + if insertPosition == -1 { + for i, step := range job.Steps { + if strings.Contains(step.Name, "Checkout") || strings.Contains(step.Uses, "checkout@") { + insertPosition = i + 1 break } } } - // If still not injected, append at the end - if !injected { - result = append(result, "") - result = append(result, extensionStep) + // If still not found, append at the end + if insertPosition == -1 { + insertPosition = len(job.Steps) } - return strings.Join(result, "\n") + // Insert the step at the determined position + newSteps := make([]WorkflowStep, 0, len(job.Steps)+1) + newSteps = append(newSteps, job.Steps[:insertPosition]...) + newSteps = append(newSteps, extensionStep) + newSteps = append(newSteps, job.Steps[insertPosition:]...) + + job.Steps = newSteps + workflow.Jobs["copilot-setup-steps"] = job + + return nil } From 11e92bcbb59bf68316a06405bdc19f541b73708d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Oct 2025 16:49:57 +0000 Subject: [PATCH 7/7] Add changeset for --mcp flag feature [skip-ci] --- .changeset/patch-add-mcp-flag.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-add-mcp-flag.md diff --git a/.changeset/patch-add-mcp-flag.md b/.changeset/patch-add-mcp-flag.md new file mode 100644 index 00000000000..de78d684cfc --- /dev/null +++ b/.changeset/patch-add-mcp-flag.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add --mcp flag to init command for Copilot Agent MCP configuration