From bf81529fe52f8395792fe4b0b74af31be2566dad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:59:47 +0000 Subject: [PATCH 1/6] Initial plan From 1cc328753072b9812137552c291ef90959609810 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:14:09 +0000 Subject: [PATCH 2/6] Add interactive mode to init command with engine selection and secret configuration Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/init.go | 393 ++++++++++++++++++++++++++++++++++++++++ pkg/cli/init_command.go | 35 +++- 2 files changed, 421 insertions(+), 7 deletions(-) diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 7be5b55abb9..f574ab3d72a 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -6,15 +6,408 @@ import ( "path/filepath" "strings" + "github.com/charmbracelet/huh" "github.com/githubnext/gh-aw/pkg/campaign" "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/constants" "github.com/githubnext/gh-aw/pkg/logger" + "github.com/githubnext/gh-aw/pkg/parser" "github.com/githubnext/gh-aw/pkg/workflow" ) var initLog = logger.New("cli:init") +// InitRepositoryInteractive runs an interactive setup for the repository +func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { + initLog.Print("Starting interactive repository initialization") + + // Assert this function is not running in automated unit tests + if os.Getenv("GO_TEST_MODE") == "true" || os.Getenv("CI") != "" { + return fmt.Errorf("interactive init cannot be used in automated tests or CI environments") + } + + // Ensure we're in a git repository + if !isGitRepo() { + initLog.Print("Not in a git repository, initialization failed") + return fmt.Errorf("not in a git repository") + } + initLog.Print("Verified git repository") + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Welcome to GitHub Agentic Workflows setup!")) + fmt.Fprintln(os.Stderr, "") + + // Prompt for engine selection + var selectedEngine string + engineOptions := []struct { + value string + label string + description string + }{ + {"copilot", "GitHub Copilot", "GitHub Copilot CLI with agent support"}, + {"claude", "Claude", "Anthropic Claude Code coding agent"}, + {"codex", "Codex", "OpenAI Codex/GPT engine"}, + } + + // Create engine selection options for huh + var huhEngineOptions []string + for _, opt := range engineOptions { + huhEngineOptions = append(huhEngineOptions, opt.value) + } + + // Use interactive prompt to select engine + form := createEngineSelectionForm(&selectedEngine, engineOptions) + if err := form.Run(); err != nil { + return fmt.Errorf("engine selection failed: %w", err) + } + + initLog.Printf("User selected engine: %s", selectedEngine) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Configuring repository for %s engine...", selectedEngine))) + fmt.Fprintln(os.Stderr, "") + + // Initialize repository with basic settings + if err := initializeBasicRepository(verbose); err != nil { + return err + } + + // Configure engine-specific settings + mcp := false + if selectedEngine == "copilot" { + mcp = true + initLog.Print("Copilot engine selected, enabling MCP configuration") + } + + // Configure MCP if copilot is selected + 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")) + } + } + + // Configure VSCode settings + initLog.Print("Configuring VSCode settings") + if err := ensureVSCodeSettings(verbose); err != nil { + initLog.Printf("Failed to update VSCode settings: %v", err) + return fmt.Errorf("failed to update VSCode settings: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Updated .vscode/settings.json")) + } + + // Check and setup secrets for the selected engine + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking required secrets for the selected engine...")) + fmt.Fprintln(os.Stderr, "") + + if err := setupEngineSecrets(selectedEngine, verbose); err != nil { + // Secret setup is non-fatal, just warn the user + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Secret setup encountered an issue: %v", err))) + } + + // Display success message + initLog.Print("Interactive repository initialization completed successfully") + 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("To create a workflow, launch Copilot CLI: npx @github/copilot")) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Then type /agent and select agentic-workflows")) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Or add workflows from the catalog: "+string(constants.CLIExtensionPrefix)+" add ")) + fmt.Fprintln(os.Stderr, "") + + return nil +} + +// createEngineSelectionForm creates an interactive form for engine selection +func createEngineSelectionForm(selectedEngine *string, engineOptions []struct { + value string + label string + description string +}) *huh.Form { + // Build options for huh.Select + var options []huh.Option[string] + for _, opt := range engineOptions { + options = append(options, huh.NewOption(fmt.Sprintf("%s - %s", opt.label, opt.description), opt.value)) + } + + return huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Which AI engine would you like to use?"). + Description("Select the AI engine that will power your agentic workflows"). + Options(options...). + Value(selectedEngine), + ), + ).WithAccessible(console.IsAccessibleMode()) +} + +// initializeBasicRepository sets up the basic repository structure +func initializeBasicRepository(verbose bool) error { + // Configure .gitattributes + initLog.Print("Configuring .gitattributes") + if err := ensureGitAttributes(); err != nil { + initLog.Printf("Failed to configure .gitattributes: %v", err) + return fmt.Errorf("failed to configure .gitattributes: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Configured .gitattributes")) + } + + // Ensure .github/aw/logs/.gitignore exists + initLog.Print("Ensuring .github/aw/logs/.gitignore exists") + if err := ensureLogsGitignore(); err != nil { + initLog.Printf("Failed to ensure logs .gitignore: %v", err) + return fmt.Errorf("failed to ensure logs .gitignore: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Configured .github/aw/logs/.gitignore")) + } + + // Write copilot instructions + initLog.Print("Writing GitHub Copilot instructions") + if err := ensureCopilotInstructions(verbose, false); err != nil { + initLog.Printf("Failed to write copilot instructions: %v", err) + return fmt.Errorf("failed to write copilot instructions: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created GitHub Copilot instructions")) + } + + // Write dispatcher agent + initLog.Print("Writing agentic workflows dispatcher agent") + if err := ensureAgenticWorkflowsDispatcher(verbose, false); err != nil { + initLog.Printf("Failed to write dispatcher agent: %v", err) + return fmt.Errorf("failed to write dispatcher agent: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created dispatcher agent")) + } + + // Write create workflow prompt + initLog.Print("Writing create workflow prompt") + if err := ensureCreateWorkflowPrompt(verbose, false); err != nil { + initLog.Printf("Failed to write create workflow prompt: %v", err) + return fmt.Errorf("failed to write create workflow prompt: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created create workflow prompt")) + } + + // Write update workflow prompt + initLog.Print("Writing update workflow prompt") + if err := ensureUpdateWorkflowPrompt(verbose, false); err != nil { + initLog.Printf("Failed to write update workflow prompt: %v", err) + return fmt.Errorf("failed to write update workflow prompt: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created update workflow prompt")) + } + + // Write create shared agentic workflow prompt + initLog.Print("Writing create shared agentic workflow prompt") + if err := ensureCreateSharedAgenticWorkflowPrompt(verbose, false); err != nil { + initLog.Printf("Failed to write create shared workflow prompt: %v", err) + return fmt.Errorf("failed to write create shared workflow prompt: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created shared workflow creation prompt")) + } + + // Delete existing setup agentic workflows agent if it exists + initLog.Print("Cleaning up setup agentic workflows agent") + if err := deleteSetupAgenticWorkflowsAgent(verbose); err != nil { + initLog.Printf("Failed to delete setup agentic workflows agent: %v", err) + return fmt.Errorf("failed to delete setup agentic workflows agent: %w", err) + } + + // Write debug workflow prompt + initLog.Print("Writing debug workflow prompt") + if err := ensureDebugWorkflowPrompt(verbose, false); err != nil { + initLog.Printf("Failed to write debug workflow prompt: %v", err) + return fmt.Errorf("failed to write debug workflow prompt: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created debug workflow prompt")) + } + + // Write upgrade agentic workflows prompt + initLog.Print("Writing upgrade agentic workflows prompt") + if err := ensureUpgradeAgenticWorkflowsPrompt(verbose, false); err != nil { + initLog.Printf("Failed to write upgrade workflows prompt: %v", err) + return fmt.Errorf("failed to write upgrade workflows prompt: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created upgrade workflows prompt")) + } + + return nil +} + +// setupEngineSecrets checks for engine-specific secrets and attempts to configure them +func setupEngineSecrets(engine string, verbose bool) error { + initLog.Printf("Setting up secrets for engine: %s", engine) + + // Get current repository + repoSlug, err := GetCurrentRepoSlug() + if err != nil { + initLog.Printf("Failed to get current repository: %v", err) + return fmt.Errorf("failed to get current repository: %w", err) + } + + // Get required secrets for the engine + tokensToCheck := getRecommendedTokensForEngine(engine) + + // Check environment for secrets + var availableSecrets []string + var missingSecrets []tokenSpec + + for _, spec := range tokensToCheck { + // Check if secret is available in environment + secretValue := os.Getenv(spec.Name) + + // Try alternative environment variable names + if secretValue == "" { + switch spec.Name { + case "ANTHROPIC_API_KEY": + secretValue = os.Getenv("ANTHROPIC_KEY") + case "OPENAI_API_KEY": + secretValue = os.Getenv("OPENAI_KEY") + case "COPILOT_GITHUB_TOKEN": + // Use the proper GitHub token helper + secretValue, _ = parser.GetGitHubToken() + } + } + + if secretValue != "" { + availableSecrets = append(availableSecrets, spec.Name) + } else { + missingSecrets = append(missingSecrets, spec) + } + } + + // Display found secrets + if len(availableSecrets) > 0 { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found the following secrets in your environment:")) + for _, secretName := range availableSecrets { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✓ %s", secretName))) + } + fmt.Fprintln(os.Stderr, "") + + // Attempt to configure them as repository secrets + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Attempting to configure these secrets for repository Actions...")) + fmt.Fprintln(os.Stderr, "") + + successCount := 0 + for _, secretName := range availableSecrets { + if err := attemptSetSecret(secretName, repoSlug, verbose); err != nil { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to set %s: %v", secretName, err))) + } + } else { + successCount++ + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf(" ✓ Configured %s", secretName))) + } + } + + if successCount > 0 { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully configured %d secret(s) for repository Actions", successCount))) + } + } + + // Display missing secrets + if len(missingSecrets) > 0 { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("The following required secrets are not available in your environment:")) + + parts := splitRepoSlug(repoSlug) + cmdOwner := parts[0] + cmdRepo := parts[1] + + for _, spec := range missingSecrets { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" ✗ %s", spec.Name))) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" When needed: %s", spec.When))) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" Description: %s", spec.Description))) + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" gh aw secrets set %s --owner %s --repo %s", spec.Name, cmdOwner, cmdRepo))) + } + fmt.Fprintln(os.Stderr, "") + } + + return nil +} + +// attemptSetSecret attempts to set a secret for the repository +func attemptSetSecret(secretName, repoSlug string, verbose bool) error { + initLog.Printf("Attempting to set secret: %s for repo: %s", secretName, repoSlug) + + // Check if secret already exists + exists, err := checkSecretExistsInRepo(secretName, repoSlug) + if err != nil { + // If we can't check, try to set anyway + if verbose { + initLog.Printf("Could not check if secret exists: %v", err) + } + } else if exists { + // Secret already exists, skip + if verbose { + initLog.Printf("Secret %s already exists, skipping", secretName) + } + return nil + } + + // Get secret value from environment + secretValue := os.Getenv(secretName) + if secretValue == "" { + // Try alternative names + switch secretName { + case "ANTHROPIC_API_KEY": + secretValue = os.Getenv("ANTHROPIC_KEY") + case "OPENAI_API_KEY": + secretValue = os.Getenv("OPENAI_KEY") + case "COPILOT_GITHUB_TOKEN": + secretValue, err = parser.GetGitHubToken() + if err != nil { + return fmt.Errorf("failed to get GitHub token: %w", err) + } + } + } + + if secretValue == "" { + return fmt.Errorf("secret value not found in environment") + } + + // Set the secret using gh CLI + cmd := workflow.ExecGH("secret", "set", secretName, "--repo", repoSlug, "--body", secretValue) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to set secret: %w (output: %s)", err, string(output)) + } + + initLog.Printf("Successfully set secret: %s", secretName) + return nil +} + // InitRepository initializes the repository for agentic workflows func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine string, codespaceRepos []string, codespaceEnabled bool, completions bool, rootCmd CommandProvider) error { initLog.Print("Starting repository initialization for agentic workflows") diff --git a/pkg/cli/init_command.go b/pkg/cli/init_command.go index bac6df02093..d532f841ee0 100644 --- a/pkg/cli/init_command.go +++ b/pkg/cli/init_command.go @@ -17,6 +17,15 @@ func NewInitCommand() *cobra.Command { Short: "Initialize repository for agentic workflows", Long: `Initialize the repository for agentic workflows by configuring .gitattributes and creating GitHub Copilot instruction files. +Interactive Mode (default): + gh aw init + + When invoked without flags, init enters interactive mode and prompts you to: + - Select which AI engine to use (Copilot, Claude, or Codex) + - Automatically configure engine-specific settings (e.g., MCP for Copilot) + - Detect and configure secrets from your environment + - Set up repository Actions secrets automatically + This command: - Configures .gitattributes to mark .lock.yml files as generated - Creates .github/aw/logs/.gitignore to ignore downloaded workflow logs @@ -69,13 +78,13 @@ After running this command, you can: To create, update or debug automated agentic actions using github, playwright, and other tools, load the .github/agents/agentic-workflows.agent.md (applies to .github/workflows/*.md) Examples: - ` + string(constants.CLIExtensionPrefix) + ` init - ` + string(constants.CLIExtensionPrefix) + ` init -v - ` + string(constants.CLIExtensionPrefix) + ` init --no-mcp - ` + string(constants.CLIExtensionPrefix) + ` init --tokens --engine copilot - ` + string(constants.CLIExtensionPrefix) + ` init --codespaces - ` + string(constants.CLIExtensionPrefix) + ` init --codespaces repo1,repo2 - ` + string(constants.CLIExtensionPrefix) + ` init --completions`, + ` + string(constants.CLIExtensionPrefix) + ` init # Interactive mode + ` + string(constants.CLIExtensionPrefix) + ` init -v # Interactive with verbose output + ` + string(constants.CLIExtensionPrefix) + ` init --no-mcp # Skip MCP configuration + ` + string(constants.CLIExtensionPrefix) + ` init --tokens --engine copilot # Check Copilot tokens + ` + string(constants.CLIExtensionPrefix) + ` init --codespaces # Configure Codespaces + ` + string(constants.CLIExtensionPrefix) + ` init --codespaces repo1,repo2 # Codespaces with additional repos + ` + string(constants.CLIExtensionPrefix) + ` init --completions # Install shell completions`, RunE: func(cmd *cobra.Command, args []string) error { verbose, _ := cmd.Flags().GetBool("verbose") mcpFlag, _ := cmd.Flags().GetBool("mcp") @@ -108,6 +117,18 @@ Examples: } } + // Check if we should enter interactive mode + // Interactive mode: no flags provided at all (only verbose is allowed) + if !cmd.Flags().Changed("mcp") && !cmd.Flags().Changed("no-mcp") && + !cmd.Flags().Changed("campaign") && !cmd.Flags().Changed("tokens") && + !cmd.Flags().Changed("engine") && !cmd.Flags().Changed("codespaces") && + !cmd.Flags().Changed("completions") { + + // Enter interactive mode + initCommandLog.Print("Entering interactive mode") + return InitRepositoryInteractive(verbose, cmd.Root()) + } + initCommandLog.Printf("Executing init command: verbose=%v, mcp=%v, campaign=%v, tokens=%v, engine=%v, codespaces=%v, codespaceEnabled=%v, completions=%v", verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions) if err := InitRepository(verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions, cmd.Root()); err != nil { initCommandLog.Printf("Init command failed: %v", err) From 366ddb33f699864e50ccc34d92cb637afb81bf71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:16:43 +0000 Subject: [PATCH 3/6] Add unit tests for interactive mode detection and preconditions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/init_command_test.go | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pkg/cli/init_command_test.go b/pkg/cli/init_command_test.go index ca5bd5d2534..43c6285e53e 100644 --- a/pkg/cli/init_command_test.go +++ b/pkg/cli/init_command_test.go @@ -105,6 +105,54 @@ func TestInitCommandHelp(t *testing.T) { if !strings.Contains(helpText, "Copilot") { t.Error("Expected help text to mention Copilot") } + + if !strings.Contains(helpText, "Interactive Mode") { + t.Error("Expected help text to mention Interactive Mode") + } +} + +func TestInitCommandInteractiveModeDetection(t *testing.T) { + t.Parallel() + + // Test that interactive mode is triggered when no flags are set + // We can't test the actual interactive prompts in unit tests, but we can + // verify that the command structure supports the detection logic + + cmd := NewInitCommand() + + // Verify that all the flags exist that are checked for interactive mode detection + requiredFlags := []string{"mcp", "no-mcp", "campaign", "tokens", "engine", "codespaces", "completions"} + for _, flagName := range requiredFlags { + flag := cmd.Flags().Lookup(flagName) + if flag == nil { + t.Errorf("Expected flag %q to exist for interactive mode detection", flagName) + } + } +} + +func TestInitRepositoryInteractivePreconditions(t *testing.T) { + // Test that InitRepositoryInteractive properly checks preconditions + + // Test 1: Fails in CI environment + t.Setenv("CI", "true") + err := InitRepositoryInteractive(false, nil) + if err == nil { + t.Error("Expected InitRepositoryInteractive to fail in CI environment") + } + if !strings.Contains(err.Error(), "automated tests or CI") { + t.Errorf("Expected error about CI environment, got: %v", err) + } + + // Test 2: Fails in test mode + t.Setenv("CI", "") + t.Setenv("GO_TEST_MODE", "true") + err = InitRepositoryInteractive(false, nil) + if err == nil { + t.Error("Expected InitRepositoryInteractive to fail in test mode") + } + if !strings.Contains(err.Error(), "automated tests or CI") { + t.Errorf("Expected error about test mode, got: %v", err) + } } func TestInitRepositoryBasic(t *testing.T) { From c78da5f5216863cb5658e4ae70f1a89596844fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:24:39 +0000 Subject: [PATCH 4/6] Fix linting error: remove unused huhEngineOptions variable Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/init.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/cli/init.go b/pkg/cli/init.go index f574ab3d72a..ae0d1bf61dc 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -49,12 +49,6 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { {"codex", "Codex", "OpenAI Codex/GPT engine"}, } - // Create engine selection options for huh - var huhEngineOptions []string - for _, opt := range engineOptions { - huhEngineOptions = append(huhEngineOptions, opt.value) - } - // Use interactive prompt to select engine form := createEngineSelectionForm(&selectedEngine, engineOptions) if err := form.Run(); err != nil { From f254409d3808a413c47eec9343840bef8fb1a856 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:44:09 +0000 Subject: [PATCH 5/6] Add confirmation prompt before configuring secrets and improve permission error handling - Add interactive confirmation prompt before setting repository secrets - Gracefully handle permission errors (403, Forbidden) with clear messaging - Display specific error messages for permission issues vs other failures - Skip setting secrets if user declines confirmation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/init.go | 75 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/pkg/cli/init.go b/pkg/cli/init.go index ae0d1bf61dc..facd87723b6 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -308,25 +308,56 @@ func setupEngineSecrets(engine string, verbose bool) error { } fmt.Fprintln(os.Stderr, "") - // Attempt to configure them as repository secrets - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Attempting to configure these secrets for repository Actions...")) - fmt.Fprintln(os.Stderr, "") + // Ask for confirmation before configuring secrets + var confirmSetSecrets bool + confirmForm := huh.NewForm( + huh.NewGroup( + huh.NewConfirm(). + Title("Would you like to configure these secrets as repository Actions secrets?"). + Description("This will use the gh CLI to set the secrets in your repository"). + Affirmative("Yes, configure secrets"). + Negative("No, skip"). + Value(&confirmSetSecrets), + ), + ).WithAccessible(console.IsAccessibleMode()) + + if err := confirmForm.Run(); err != nil { + return fmt.Errorf("confirmation failed: %w", err) + } - successCount := 0 - for _, secretName := range availableSecrets { - if err := attemptSetSecret(secretName, repoSlug, verbose); err != nil { - if verbose { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to set %s: %v", secretName, err))) + if !confirmSetSecrets { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Skipped configuring secrets")) + fmt.Fprintln(os.Stderr, "") + } else { + // Attempt to configure them as repository secrets + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Configuring secrets for repository Actions...")) + fmt.Fprintln(os.Stderr, "") + + successCount := 0 + for _, secretName := range availableSecrets { + if err := attemptSetSecret(secretName, repoSlug, verbose); err != nil { + // Handle different types of errors gracefully + errMsg := err.Error() + if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "Forbidden") || + strings.Contains(errMsg, "permissions") || strings.Contains(errMsg, "Resource not accessible") { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf(" ✗ Insufficient permissions to set %s", secretName))) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(" You may need to grant additional permissions to your GitHub token")) + } else { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf(" ✗ Failed to set %s: %v", secretName, err))) + } + } else { + successCount++ + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf(" ✓ Configured %s", secretName))) } - } else { - successCount++ - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf(" ✓ Configured %s", secretName))) } - } - if successCount > 0 { - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully configured %d secret(s) for repository Actions", successCount))) + if successCount > 0 { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully configured %d secret(s) for repository Actions", successCount))) + } else { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatWarningMessage("No secrets were configured. You may need to set them manually.")) + } } } @@ -359,7 +390,11 @@ func attemptSetSecret(secretName, repoSlug string, verbose bool) error { // Check if secret already exists exists, err := checkSecretExistsInRepo(secretName, repoSlug) if err != nil { - // If we can't check, try to set anyway + // If we get a permission error, return it immediately + if strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "Forbidden") { + return fmt.Errorf("insufficient permissions to access repository secrets: %w", err) + } + // For other errors, log but try to set anyway if verbose { initLog.Printf("Could not check if secret exists: %v", err) } @@ -395,7 +430,13 @@ func attemptSetSecret(secretName, repoSlug string, verbose bool) error { // Set the secret using gh CLI cmd := workflow.ExecGH("secret", "set", secretName, "--repo", repoSlug, "--body", secretValue) if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to set secret: %w (output: %s)", err, string(output)) + outputStr := string(output) + // Check for permission-related errors + if strings.Contains(outputStr, "403") || strings.Contains(outputStr, "Forbidden") || + strings.Contains(outputStr, "Resource not accessible") || strings.Contains(err.Error(), "403") { + return fmt.Errorf("insufficient permissions to set secrets in repository: %w", err) + } + return fmt.Errorf("failed to set secret: %w (output: %s)", err, outputStr) } initLog.Printf("Successfully set secret: %s", secretName) From fd1ffd0c49ea683f46b668260a3ca221e47e850a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:55:56 +0000 Subject: [PATCH 6/6] Use EngineName constants from constants.go and rename mcp to copilotMcp - Use constants.CopilotEngine, constants.ClaudeEngine, constants.CodexEngine instead of string literals - Rename variable mcp to copilotMcp for clarity Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/init.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cli/init.go b/pkg/cli/init.go index facd87723b6..d64c6399d9c 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -44,9 +44,9 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { label string description string }{ - {"copilot", "GitHub Copilot", "GitHub Copilot CLI with agent support"}, - {"claude", "Claude", "Anthropic Claude Code coding agent"}, - {"codex", "Codex", "OpenAI Codex/GPT engine"}, + {string(constants.CopilotEngine), "GitHub Copilot", "GitHub Copilot CLI with agent support"}, + {string(constants.ClaudeEngine), "Claude", "Anthropic Claude Code coding agent"}, + {string(constants.CodexEngine), "Codex", "OpenAI Codex/GPT engine"}, } // Use interactive prompt to select engine @@ -66,14 +66,14 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { } // Configure engine-specific settings - mcp := false - if selectedEngine == "copilot" { - mcp = true + copilotMcp := false + if selectedEngine == string(constants.CopilotEngine) { + copilotMcp = true initLog.Print("Copilot engine selected, enabling MCP configuration") } // Configure MCP if copilot is selected - if mcp { + if copilotMcp { initLog.Print("Configuring GitHub Copilot Agent MCP integration") // Create copilot-setup-steps.yml @@ -120,7 +120,7 @@ func InitRepositoryInteractive(verbose bool, rootCmd CommandProvider) error { fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Repository initialized for agentic workflows!")) fmt.Fprintln(os.Stderr, "") - if mcp { + if copilotMcp { fmt.Fprintln(os.Stderr, console.FormatInfoMessage("GitHub Copilot Agent MCP integration configured")) fmt.Fprintln(os.Stderr, "") }