diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 081b924844..aa093e00f9 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,57 +1,40 @@ -name: "Copilot Setup Steps" - -# Automatically run the setup steps when they are changed to allow for easy validation, and -# allow manual testing through the repository's "Actions" tab -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml +name: Copilot Setup Steps +"on": pull_request: paths: - - .github/workflows/copilot-setup-steps.yml - + - .github/workflows/copilot-setup-steps.yml + push: + paths: + - .github/workflows/copilot-setup-steps.yml + workflow_dispatch: null jobs: - # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: runs-on: ubuntu-latest - - # Set the permissions to the lowest permissions possible needed for your steps. - # Copilot will be given its own token for its operations. permissions: - # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. contents: read - - # You can define any steps you want, and they will run before the agent starts. - # If you do not check out your code, Copilot will do this for you. steps: - - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - - - name: Set up Node.js - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 - with: - node-version: "24" - cache: npm - cache-dependency-path: pkg/workflow/js/package-lock.json - - - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 - with: - go-version-file: go.mod - cache: true - - - name: Install JavaScript dependencies - run: npm ci - working-directory: ./pkg/workflow/js - - - name: Install development dependencies - run: make deps-dev - - - name: Build code - run: make build - continue-on-error: true - - - name: Recompile workflows - run: make recompile - continue-on-error: true + - name: Install gh-aw extension + run: curl -fsSL https://raw.githubusercontent.com/githubnext/gh-aw/refs/heads/main/install-gh-aw.sh | bash + - name: Verify gh-aw installation + run: gh aw version + - name: Checkout code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + - name: Set up Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f + with: + cache: npm + cache-dependency-path: pkg/workflow/js/package-lock.json + node-version: "24" + - name: Set up Go + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 + with: + cache: true + go-version-file: go.mod + - name: Install JavaScript dependencies + run: npm ci + - name: Install development dependencies + run: make deps-dev + - name: Build code + run: make build + - name: Recompile workflows + run: make recompile diff --git a/.vscode/mcp.json b/.vscode/mcp.json index db62c82151..6699af5648 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,8 +1,11 @@ { "servers": { "github-agentic-workflows": { - "command": "./gh-aw", - "args": ["mcp-server", "--cmd", "./gh-aw"], + "command": "gh", + "args": [ + "aw", + "mcp-server" + ], "cwd": "${workspaceFolder}" } } diff --git a/pkg/cli/init_command.go b/pkg/cli/init_command.go index be4ee83c82..8c93c4cd68 100644 --- a/pkg/cli/init_command.go +++ b/pkg/cli/init_command.go @@ -25,15 +25,18 @@ This command: - Creates the debug agentic workflow agent at .github/agents/debug-agentic-workflow.agent.md - Removes old prompt files from .github/prompts/ if they exist +By default (without --no-mcp): +- Creates .github/workflows/copilot-setup-steps.yml with gh-aw installation steps +- Creates .vscode/mcp.json with gh-aw MCP server configuration + +With --no-mcp flag: +- Skips creating GitHub Copilot Agent MCP server configuration files + With --tokens flag: - Validates which required and optional secrets are configured - Provides commands to set up missing secrets for the specified engine - Use with --engine flag to check engine-specific tokens (copilot, claude, codex) -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 - With --codespaces flag: - Creates .devcontainer/gh-aw/devcontainer.json with universal image (in subfolder to avoid conflicts) - Configures permissions for current repo: actions:write, contents:write, discussions:read, issues:read, pull-requests:write, workflows:write @@ -54,19 +57,28 @@ After running this command, you can: Examples: ` + constants.CLIExtensionPrefix + ` init ` + constants.CLIExtensionPrefix + ` init -v - ` + constants.CLIExtensionPrefix + ` init --mcp + ` + constants.CLIExtensionPrefix + ` init --no-mcp ` + constants.CLIExtensionPrefix + ` init --tokens --engine copilot ` + constants.CLIExtensionPrefix + ` init --codespaces ` + constants.CLIExtensionPrefix + ` init --codespaces repo1,repo2`, RunE: func(cmd *cobra.Command, args []string) error { verbose, _ := cmd.Flags().GetBool("verbose") - mcp, _ := cmd.Flags().GetBool("mcp") + mcpFlag, _ := cmd.Flags().GetBool("mcp") + noMcp, _ := cmd.Flags().GetBool("no-mcp") campaign, _ := cmd.Flags().GetBool("campaign") tokens, _ := cmd.Flags().GetBool("tokens") engine, _ := cmd.Flags().GetString("engine") codespaceReposStr, _ := cmd.Flags().GetString("codespaces") codespaceEnabled := cmd.Flags().Changed("codespaces") + // Determine MCP state: default true, unless --no-mcp is specified + // --mcp flag is kept for backward compatibility (hidden from help) + mcp := !noMcp + if cmd.Flags().Changed("mcp") { + // If --mcp is explicitly set, use it (backward compatibility) + mcp = mcpFlag + } + // Trim the codespace repos string (NoOptDefVal uses a space) codespaceReposStr = strings.TrimSpace(codespaceReposStr) @@ -90,7 +102,8 @@ Examples: }, } - cmd.Flags().Bool("mcp", false, "Configure GitHub Copilot Agent MCP server integration") + cmd.Flags().Bool("no-mcp", false, "Skip configuring GitHub Copilot Agent MCP server integration") + cmd.Flags().Bool("mcp", false, "Configure GitHub Copilot Agent MCP server integration (deprecated, MCP is enabled by default)") cmd.Flags().Bool("campaign", false, "Install the Campaign Designer agent for gh-aw campaigns in this repository") cmd.Flags().Bool("tokens", false, "Validate required secrets for agentic workflows") cmd.Flags().String("engine", "", "AI engine to check tokens for (copilot, claude, codex) - requires --tokens flag") @@ -98,5 +111,8 @@ Examples: // NoOptDefVal allows using --codespaces without a value (returns empty string when no value provided) cmd.Flags().Lookup("codespaces").NoOptDefVal = " " + // Hide the deprecated --mcp flag from help (kept for backward compatibility) + _ = cmd.Flags().MarkHidden("mcp") + return cmd } diff --git a/pkg/cli/init_command_test.go b/pkg/cli/init_command_test.go index 5cc7f926ef..ff67500288 100644 --- a/pkg/cli/init_command_test.go +++ b/pkg/cli/init_command_test.go @@ -34,12 +34,24 @@ func TestNewInitCommand(t *testing.T) { } // Verify flags + noMcpFlag := cmd.Flags().Lookup("no-mcp") + if noMcpFlag == nil { + t.Error("Expected 'no-mcp' flag to be defined") + return + } + + // Verify hidden --mcp flag still exists for backward compatibility mcpFlag := cmd.Flags().Lookup("mcp") if mcpFlag == nil { - t.Error("Expected 'mcp' flag to be defined") + t.Error("Expected 'mcp' flag to be defined (for backward compatibility)") return } + // Verify --mcp flag is hidden + if !mcpFlag.Hidden { + t.Error("Expected 'mcp' flag to be hidden") + } + campaignFlag := cmd.Flags().Lookup("campaign") if campaignFlag == nil { t.Error("Expected 'campaign' flag to be defined") @@ -50,6 +62,10 @@ func TestNewInitCommand(t *testing.T) { t.Errorf("Expected campaign flag default to be 'false', got %q", campaignFlag.DefValue) } + if noMcpFlag.DefValue != "false" { + t.Errorf("Expected no-mcp flag default to be 'false', got %q", noMcpFlag.DefValue) + } + if mcpFlag.DefValue != "false" { t.Errorf("Expected mcp flag default to be 'false', got %q", mcpFlag.DefValue) } @@ -115,8 +131,8 @@ func TestInitRepositoryBasic(t *testing.T) { exec.Command("git", "config", "user.name", "Test User").Run() exec.Command("git", "config", "user.email", "test@example.com").Run() - // Test basic init without MCP or campaign agent - err = InitRepository(false, false, false, false, "", []string{}, false) + // Test basic init with MCP enabled by default (mcp=true, noMcp=false behavior) + err = InitRepository(false, true, false, false, "", []string{}, false) if err != nil { t.Fatalf("InitRepository() failed: %v", err) } @@ -143,6 +159,17 @@ func TestInitRepositoryBasic(t *testing.T) { if _, err := os.Stat(logsGitignorePath); os.IsNotExist(err) { t.Error("Expected .github/aw/logs/.gitignore to be created") } + + // Verify MCP files were created by default + mcpConfigPath := filepath.Join(".vscode", "mcp.json") + if _, err := os.Stat(mcpConfigPath); os.IsNotExist(err) { + t.Error("Expected .vscode/mcp.json to be created by default") + } + + setupStepsPath := filepath.Join(".github", "workflows", "copilot-setup-steps.yml") + if _, err := os.Stat(setupStepsPath); os.IsNotExist(err) { + t.Error("Expected .github/workflows/copilot-setup-steps.yml to be created by default") + } } func TestInitRepositoryWithMCP(t *testing.T) { @@ -169,7 +196,7 @@ func TestInitRepositoryWithMCP(t *testing.T) { exec.Command("git", "config", "user.name", "Test User").Run() exec.Command("git", "config", "user.email", "test@example.com").Run() - // Test init with MCP flag + // Test init with MCP explicitly enabled (same as default) err = InitRepository(false, true, false, false, "", []string{}, false) if err != nil { t.Fatalf("InitRepository() with MCP failed: %v", err) @@ -188,6 +215,97 @@ func TestInitRepositoryWithMCP(t *testing.T) { } } +func TestInitRepositoryWithNoMCP(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(originalDir) + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Initialize git repo + if err := exec.Command("git", "init").Run(); err != nil { + t.Skip("Git not available") + } + + // Configure git + exec.Command("git", "config", "user.name", "Test User").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + + // Test init with --no-mcp flag (mcp=false) + err = InitRepository(false, false, false, false, "", []string{}, false) + if err != nil { + t.Fatalf("InitRepository() with --no-mcp failed: %v", err) + } + + // Verify .vscode/mcp.json was NOT created + mcpConfigPath := filepath.Join(".vscode", "mcp.json") + if _, err := os.Stat(mcpConfigPath); err == nil { + t.Error("Expected .vscode/mcp.json to NOT be created with --no-mcp flag") + } + + // Verify copilot-setup-steps.yml was NOT created + setupStepsPath := filepath.Join(".github", "workflows", "copilot-setup-steps.yml") + if _, err := os.Stat(setupStepsPath); err == nil { + t.Error("Expected .github/workflows/copilot-setup-steps.yml to NOT be created with --no-mcp flag") + } + + // Verify basic files were still created + if _, err := os.Stat(".gitattributes"); os.IsNotExist(err) { + t.Error("Expected .gitattributes to be created even with --no-mcp flag") + } +} + +func TestInitRepositoryWithMCPBackwardCompatibility(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + _ = os.Chdir(originalDir) + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Initialize git repo + if err := exec.Command("git", "init").Run(); err != nil { + t.Skip("Git not available") + } + + // Configure git + exec.Command("git", "config", "user.name", "Test User").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + + // Test init with deprecated --mcp flag for backward compatibility (mcp=true) + err = InitRepository(false, true, false, false, "", []string{}, false) + if err != nil { + t.Fatalf("InitRepository() with deprecated --mcp flag failed: %v", err) + } + + // Verify .vscode/mcp.json was created + mcpConfigPath := filepath.Join(".vscode", "mcp.json") + if _, err := os.Stat(mcpConfigPath); os.IsNotExist(err) { + t.Error("Expected .vscode/mcp.json to be created with --mcp flag (backward compatibility)") + } + + // Verify copilot-setup-steps.yml was created + setupStepsPath := filepath.Join(".github", "workflows", "copilot-setup-steps.yml") + if _, err := os.Stat(setupStepsPath); os.IsNotExist(err) { + t.Error("Expected .github/workflows/copilot-setup-steps.yml to be created with --mcp flag (backward compatibility)") + } +} + func TestInitRepositoryVerbose(t *testing.T) { tmpDir := testutil.TempDir(t, "test-*") @@ -212,8 +330,8 @@ func TestInitRepositoryVerbose(t *testing.T) { exec.Command("git", "config", "user.name", "Test User").Run() exec.Command("git", "config", "user.email", "test@example.com").Run() - // Test verbose mode (should not error, just produce more output) - err = InitRepository(true, false, false, false, "", []string{}, false) + // Test verbose mode with MCP enabled by default (should not error, just produce more output) + err = InitRepository(true, true, false, false, "", []string{}, false) if err != nil { t.Fatalf("InitRepository() in verbose mode failed: %v", err) } @@ -240,7 +358,7 @@ func TestInitRepositoryNotInGitRepo(t *testing.T) { } // Don't initialize git repo - should fail for some operations - err = InitRepository(false, false, false, false, "", []string{}, false) + err = InitRepository(false, true, false, false, "", []string{}, false) // The function should handle this gracefully or return an error // Based on the implementation, ensureGitAttributes requires git @@ -273,14 +391,14 @@ func TestInitRepositoryIdempotent(t *testing.T) { exec.Command("git", "config", "user.name", "Test User").Run() exec.Command("git", "config", "user.email", "test@example.com").Run() - // Run init twice - err = InitRepository(false, false, false, false, "", []string{}, false) + // Run init twice with MCP enabled by default + err = InitRepository(false, true, false, false, "", []string{}, false) if err != nil { t.Fatalf("First InitRepository() failed: %v", err) } // Second run should be idempotent - err = InitRepository(false, false, false, false, "", []string{}, false) + err = InitRepository(false, true, false, false, "", []string{}, false) if err != nil { t.Fatalf("Second InitRepository() failed: %v", err) } @@ -400,14 +518,14 @@ func TestInitCommandFlagValidation(t *testing.T) { cmd := NewInitCommand() - // Test that mcp flag is a boolean - mcpFlag := cmd.Flags().Lookup("mcp") - if mcpFlag == nil { - t.Fatal("Expected 'mcp' flag to exist") + // Test that no-mcp flag is a boolean + noMcpFlag := cmd.Flags().Lookup("no-mcp") + if noMcpFlag == nil { + t.Fatal("Expected 'no-mcp' flag to exist") } - if mcpFlag.Value.Type() != "bool" { - t.Errorf("Expected mcp flag to be bool, got %s", mcpFlag.Value.Type()) + if noMcpFlag.Value.Type() != "bool" { + t.Errorf("Expected no-mcp flag to be bool, got %s", noMcpFlag.Value.Type()) } // Test that campaign flag is a boolean @@ -439,8 +557,8 @@ func TestInitRepositoryErrorHandling(t *testing.T) { t.Fatalf("Failed to change to temp directory: %v", err) } - // Test init without git repo - err = InitRepository(false, false, false, false, "", []string{}, false) + // Test init without git repo (with MCP enabled by default) + err = InitRepository(false, true, false, false, "", []string{}, false) // Should handle error gracefully or return error // The actual behavior depends on implementation @@ -482,8 +600,8 @@ func TestInitRepositoryWithExistingFiles(t *testing.T) { t.Fatalf("Failed to create existing .gitattributes: %v", err) } - // Run init - err = InitRepository(false, false, false, false, "", []string{}, false) + // Run init with MCP enabled by default + err = InitRepository(false, true, false, false, "", []string{}, false) if err != nil { t.Fatalf("InitRepository() failed: %v", err) } @@ -531,9 +649,9 @@ func TestInitRepositoryWithCodespace(t *testing.T) { exec.Command("git", "config", "user.name", "Test User").Run() exec.Command("git", "config", "user.email", "test@example.com").Run() - // Test init with --codespaces flag (with additional repos) + // Test init with --codespaces flag (with MCP enabled by default and additional repos) additionalRepos := []string{"org/repo1", "owner/repo2"} - err = InitRepository(false, false, false, false, "", additionalRepos, true) + err = InitRepository(false, true, false, false, "", additionalRepos, true) if err != nil { t.Fatalf("InitRepository() with codespaces failed: %v", err) } @@ -597,8 +715,8 @@ func TestInitCommandWithCodespacesNoArgs(t *testing.T) { exec.Command("git", "config", "user.name", "Test User").Run() exec.Command("git", "config", "user.email", "test@example.com").Run() - // Test init with --codespaces flag (no additional repos) - err = InitRepository(false, false, false, false, "", []string{}, true) + // Test init with --codespaces flag (no additional repos, MCP enabled by default) + err = InitRepository(false, true, false, false, "", []string{}, true) if err != nil { t.Fatalf("InitRepository() with codespaces (no args) failed: %v", err) }