From fb7855f26f3180cd0da29d0076b5a6bb34a680ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:42:27 +0000 Subject: [PATCH 1/3] Initial plan From b55a8f4c6080b83e031c66d7f60ef5694ffca1d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:55:32 +0000 Subject: [PATCH 2/3] Add --push flag to upgrade command - Add Push field to UpgradeConfig struct - Add --push flag to upgrade command CLI - Check for clean working directory before upgrade when --push is enabled - Implement commit/pull/push logic after successful compilation - Handle cases where no remote is configured (test environments) - Add tests for --push flag functionality - Update help text with --push example Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/upgrade_command.go | 127 ++++++++++++++++++++++++++++++-- pkg/cli/upgrade_command_test.go | 119 ++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 6 deletions(-) diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go index 9e289b861f..51bcf51554 100644 --- a/pkg/cli/upgrade_command.go +++ b/pkg/cli/upgrade_command.go @@ -3,6 +3,8 @@ package cli import ( "fmt" "os" + "os/exec" + "strings" "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/constants" @@ -17,11 +19,12 @@ type UpgradeConfig struct { Verbose bool WorkflowDir string NoFix bool + Push bool } // RunUpgrade runs the upgrade command with the given configuration func RunUpgrade(config UpgradeConfig) error { - return runUpgradeCommand(config.Verbose, config.WorkflowDir, config.NoFix, false) + return runUpgradeCommand(config.Verbose, config.WorkflowDir, config.NoFix, false, config.Push) } // NewUpgradeCommand creates the upgrade command @@ -49,19 +52,22 @@ This command always upgrades all Markdown files in .github/workflows. Examples: ` + string(constants.CLIExtensionPrefix) + ` upgrade # Upgrade all workflows ` + string(constants.CLIExtensionPrefix) + ` upgrade --no-fix # Update agent files only (skip codemods and compilation) + ` + string(constants.CLIExtensionPrefix) + ` upgrade --push # Upgrade and automatically commit/push changes ` + string(constants.CLIExtensionPrefix) + ` upgrade --dir custom/workflows # Upgrade workflows in custom directory`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { verbose, _ := cmd.Flags().GetBool("verbose") dir, _ := cmd.Flags().GetString("dir") noFix, _ := cmd.Flags().GetBool("no-fix") + push, _ := cmd.Flags().GetBool("push") - return runUpgradeCommand(verbose, dir, noFix, false) + return runUpgradeCommand(verbose, dir, noFix, false, push) }, } cmd.Flags().StringP("dir", "d", "", "Workflow directory (default: .github/workflows)") cmd.Flags().Bool("no-fix", false, "Skip applying codemods and compiling workflows (only update agent files)") + cmd.Flags().Bool("push", false, "Automatically commit and push changes after successful upgrade") // Register completions RegisterDirFlagCompletion(cmd, "dir") @@ -70,11 +76,24 @@ Examples: } // runUpgradeCommand executes the upgrade process -func runUpgradeCommand(verbose bool, workflowDir string, noFix bool, noCompile bool) error { - upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v", - verbose, workflowDir, noFix, noCompile) +func runUpgradeCommand(verbose bool, workflowDir string, noFix bool, noCompile bool, push bool) error { + upgradeLog.Printf("Running upgrade command: verbose=%v, workflowDir=%s, noFix=%v, noCompile=%v, push=%v", + verbose, workflowDir, noFix, noCompile, push) + + // Step 0a: If --push is enabled, ensure git status is clean before starting + if push { + upgradeLog.Print("Checking for clean working directory (--push enabled)") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking git status...")) + if err := checkCleanWorkingDirectory(verbose); err != nil { + upgradeLog.Printf("Git status check failed: %v", err) + return fmt.Errorf("--push requires a clean working directory: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Working directory is clean")) + } + } - // Step 0: Ensure gh-aw extension is on the latest version + // Step 0b: Ensure gh-aw extension is on the latest version fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking gh-aw extension version...")) if err := ensureLatestExtensionVersion(verbose); err != nil { upgradeLog.Printf("Extension version check failed: %v", err) @@ -161,6 +180,102 @@ func runUpgradeCommand(verbose bool, workflowDir string, noFix bool, noCompile b fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Upgrade complete")) + // Step 4: If --push is enabled, commit and push changes + if push { + upgradeLog.Print("Push enabled - preparing to commit and push changes") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Preparing to commit and push changes...")) + + // Check if there are any changes to commit + upgradeLog.Print("Checking for modified files") + cmd := exec.Command("git", "status", "--porcelain") + output, err := cmd.Output() + if err != nil { + upgradeLog.Printf("Failed to check git status: %v", err) + return fmt.Errorf("failed to check git status: %w", err) + } + + if len(strings.TrimSpace(string(output))) == 0 { + upgradeLog.Print("No changes to commit") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No changes to commit")) + return nil + } + + // Pull latest changes from remote before committing (if remote exists) + upgradeLog.Print("Checking for remote repository") + checkRemoteCmd := exec.Command("git", "remote", "get-url", "origin") + if err := checkRemoteCmd.Run(); err == nil { + // Remote exists, pull changes + upgradeLog.Print("Pulling latest changes from remote") + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Pulling latest changes from remote...")) + } + pullCmd := exec.Command("git", "pull", "--rebase") + if output, err := pullCmd.CombinedOutput(); err != nil { + upgradeLog.Printf("Failed to pull changes: %v, output: %s", err, string(output)) + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to pull changes: %v", err))) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("You may need to manually resolve conflicts and push")) + return fmt.Errorf("failed to pull changes: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Pulled latest changes")) + } + } else { + upgradeLog.Print("No remote repository configured, skipping pull") + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No remote repository configured, skipping pull")) + } + } + + // Stage all modified files + upgradeLog.Print("Staging all changes") + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Staging changes...")) + } + addCmd := exec.Command("git", "add", "-A") + if output, err := addCmd.CombinedOutput(); err != nil { + upgradeLog.Printf("Failed to stage changes: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to stage changes: %w", err) + } + + // Commit the changes + upgradeLog.Print("Committing changes") + commitMessage := "chore: upgrade agentic workflows" + if err := commitChanges(commitMessage, verbose); err != nil { + upgradeLog.Printf("Failed to commit changes: %v", err) + return fmt.Errorf("failed to commit changes: %w", err) + } + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes committed")) + } + + // Push the changes (only if remote exists) + upgradeLog.Print("Checking if remote repository exists for push") + checkRemoteCmd = exec.Command("git", "remote", "get-url", "origin") + if err := checkRemoteCmd.Run(); err == nil { + // Remote exists, push changes + upgradeLog.Print("Pushing changes to remote") + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Pushing to remote...")) + } + pushCmd := exec.Command("git", "push") + if output, err := pushCmd.CombinedOutput(); err != nil { + upgradeLog.Printf("Failed to push changes: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to push changes: %w\nOutput: %s", err, string(output)) + } + + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes pushed to remote")) + } else { + upgradeLog.Print("No remote repository configured, skipping push") + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No remote repository configured, changes committed locally")) + } + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes committed locally (no remote configured)")) + } + } + return nil } diff --git a/pkg/cli/upgrade_command_test.go b/pkg/cli/upgrade_command_test.go index 64be8ef692..abb961397c 100644 --- a/pkg/cli/upgrade_command_test.go +++ b/pkg/cli/upgrade_command_test.go @@ -50,6 +50,7 @@ This is a test workflow. Verbose: false, NoFix: true, // Skip codemods for this test WorkflowDir: "", + Push: false, } err = RunUpgrade(config) @@ -125,6 +126,7 @@ This workflow also has deprecated timeout_minutes field. Verbose: false, NoFix: false, // Apply codemods WorkflowDir: "", + Push: false, } err = RunUpgrade(config) @@ -185,6 +187,7 @@ This workflow should not be modified when --no-fix is used. Verbose: false, NoFix: true, // Skip codemods WorkflowDir: "", + Push: false, } err = RunUpgrade(config) @@ -215,6 +218,7 @@ func TestUpgradeCommand_NonGitRepo(t *testing.T) { Verbose: false, NoFix: true, WorkflowDir: "", + Push: false, } err := RunUpgrade(config) @@ -262,6 +266,7 @@ This is a test workflow that should be compiled during upgrade. Verbose: false, NoFix: false, // Apply codemods and compile WorkflowDir: "", + Push: false, } err = RunUpgrade(config) @@ -317,6 +322,7 @@ This workflow should not be compiled with --no-fix. Verbose: false, NoFix: true, // Skip codemods and compilation WorkflowDir: "", + Push: false, } err = RunUpgrade(config) @@ -326,3 +332,116 @@ This workflow should not be compiled with --no-fix. lockFile := filepath.Join(workflowsDir, "test-workflow.lock.yml") assert.NoFileExists(t, lockFile, "Lock file should not be created with --no-fix") } + +func TestUpgradeCommand_PushRequiresCleanWorkingDirectory(t *testing.T) { + // Create a temporary directory for test files + tmpDir := t.TempDir() + originalDir, _ := os.Getwd() + defer os.Chdir(originalDir) + + // Initialize git repository + os.Chdir(tmpDir) + exec.Command("git", "init").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create .github/workflows directory + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + require.NoError(t, err, "Failed to create workflows directory") + + // Create a workflow file + workflowFile := filepath.Join(workflowsDir, "test-workflow.md") + content := `--- +on: + workflow_dispatch: + +permissions: + contents: read +--- + +# Test Workflow + +This is a test workflow. +` + err = os.WriteFile(workflowFile, []byte(content), 0644) + require.NoError(t, err, "Failed to create test workflow file") + + // Commit the workflow file first + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "Initial commit").Run() + + // Create an uncommitted change + unstagedFile := filepath.Join(tmpDir, "uncommitted.txt") + err = os.WriteFile(unstagedFile, []byte("uncommitted content"), 0644) + require.NoError(t, err, "Failed to create uncommitted file") + + // Run upgrade command with --push (should fail due to uncommitted changes) + config := UpgradeConfig{ + Verbose: false, + NoFix: true, + WorkflowDir: "", + Push: true, + } + + err = RunUpgrade(config) + require.Error(t, err, "Upgrade with --push should fail when there are uncommitted changes") + assert.Contains(t, strings.ToLower(err.Error()), "clean", "Error message should mention clean working directory") +} + +func TestUpgradeCommand_PushWithNoChanges(t *testing.T) { + // Create a temporary directory for test files + tmpDir := t.TempDir() + originalDir, _ := os.Getwd() + defer os.Chdir(originalDir) + + // Initialize git repository + os.Chdir(tmpDir) + exec.Command("git", "init").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + exec.Command("git", "config", "user.name", "Test User").Run() + + // Create .github/workflows directory + workflowsDir := filepath.Join(tmpDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0755) + require.NoError(t, err, "Failed to create workflows directory") + + // Create a simple workflow + workflowFile := filepath.Join(workflowsDir, "test-workflow.md") + content := `--- +on: + workflow_dispatch: + +permissions: + contents: read +--- + +# Test Workflow + +This workflow is already up to date. +` + err = os.WriteFile(workflowFile, []byte(content), 0644) + require.NoError(t, err, "Failed to create test workflow file") + + // Commit everything first + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "Initial commit").Run() + + // Create all the agent files to ensure no changes will be made + if err := ensureCopilotInstructions(false, false); err == nil { + exec.Command("git", "add", ".").Run() + exec.Command("git", "commit", "-m", "Add agent files").Run() + } + + // Run upgrade command with --push (should succeed but not create a new commit) + config := UpgradeConfig{ + Verbose: false, + NoFix: true, // Skip codemods to avoid changes + WorkflowDir: "", + Push: true, + } + + err = RunUpgrade(config) + // Should succeed even if no changes to commit + require.NoError(t, err, "Upgrade with --push should succeed when working directory is clean") +} From 32b7e5e56925c0369653333b3f38522b55d42e42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:42:53 +0000 Subject: [PATCH 3/3] Refactor --push feature into git helpers and add to init command - Extract git operations into reusable helper functions in git.go: - hasChangesToCommit() - check if there are changes to commit - hasRemote() - check if remote repository exists - pullFromRemote() - pull latest changes with rebase - stageAllChanges() - stage all changes with git add -A - pushToRemote() - push changes to remote - commitAndPushChanges() - orchestrate full commit/push workflow - Refactor upgrade_command.go to use new git helpers - Add --push flag to init command - Update all test files to include new push parameter - Update help text for both commands with --push examples Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/git.go | 113 +++++++++++++++++++++++++++++++++++ pkg/cli/init.go | 39 +++++++++++- pkg/cli/init_command.go | 11 ++-- pkg/cli/init_command_test.go | 58 +++++++++--------- pkg/cli/init_mcp_test.go | 12 ++-- pkg/cli/init_test.go | 20 +++---- pkg/cli/interfaces_test.go | 4 +- pkg/cli/upgrade_command.go | 96 ++++------------------------- 8 files changed, 218 insertions(+), 135 deletions(-) diff --git a/pkg/cli/git.go b/pkg/cli/git.go index 1feeca0639..9a9ce64860 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -533,3 +533,116 @@ func checkWorkflowFileStatus(workflowPath string) (*WorkflowFileStatus, error) { return status, nil } + +// hasChangesToCommit checks if there are any changes in the working directory +func hasChangesToCommit() (bool, error) { + gitLog.Print("Checking for modified files") + cmd := exec.Command("git", "status", "--porcelain") + output, err := cmd.Output() + if err != nil { + gitLog.Printf("Failed to check git status: %v", err) + return false, fmt.Errorf("failed to check git status: %w", err) + } + + hasChanges := len(strings.TrimSpace(string(output))) > 0 + gitLog.Printf("Has changes to commit: %v", hasChanges) + return hasChanges, nil +} + +// hasRemote checks if a remote repository named 'origin' is configured +func hasRemote() bool { + gitLog.Print("Checking for remote repository") + cmd := exec.Command("git", "remote", "get-url", "origin") + err := cmd.Run() + hasRemoteRepo := err == nil + gitLog.Printf("Has remote repository: %v", hasRemoteRepo) + return hasRemoteRepo +} + +// pullFromRemote pulls the latest changes from the remote repository using rebase +func pullFromRemote(verbose bool) error { + gitLog.Print("Pulling latest changes from remote") + pullCmd := exec.Command("git", "pull", "--rebase") + if output, err := pullCmd.CombinedOutput(); err != nil { + gitLog.Printf("Failed to pull changes: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to pull changes: %w", err) + } + gitLog.Print("Successfully pulled latest changes") + return nil +} + +// stageAllChanges stages all modified files using git add -A +func stageAllChanges(verbose bool) error { + gitLog.Print("Staging all changes") + addCmd := exec.Command("git", "add", "-A") + if output, err := addCmd.CombinedOutput(); err != nil { + gitLog.Printf("Failed to stage changes: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to stage changes: %w", err) + } + gitLog.Print("Successfully staged all changes") + return nil +} + +// pushToRemote pushes the current branch to the remote repository +func pushToRemote(verbose bool) error { + gitLog.Print("Pushing changes to remote") + pushCmd := exec.Command("git", "push") + if output, err := pushCmd.CombinedOutput(); err != nil { + gitLog.Printf("Failed to push changes: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to push changes: %w\nOutput: %s", err, string(output)) + } + gitLog.Print("Successfully pushed changes to remote") + return nil +} + +// commitAndPushChanges is a helper that orchestrates the full commit and push workflow +// It checks for changes, pulls from remote (if exists), stages all changes, commits, and pushes (if remote exists) +func commitAndPushChanges(commitMessage string, verbose bool) error { + gitLog.Printf("Starting commit and push workflow with message: %s", commitMessage) + + // Check if there are any changes to commit + hasChanges, err := hasChangesToCommit() + if err != nil { + return err + } + + if !hasChanges { + gitLog.Print("No changes to commit") + return nil + } + + // Pull latest changes from remote before committing (if remote exists) + if hasRemote() { + gitLog.Print("Remote repository exists, pulling latest changes") + if err := pullFromRemote(verbose); err != nil { + return err + } + } else { + gitLog.Print("No remote repository configured, skipping pull") + } + + // Stage all modified files + if err := stageAllChanges(verbose); err != nil { + return err + } + + // Commit the changes + gitLog.Printf("Committing changes with message: %s", commitMessage) + if err := commitChanges(commitMessage, verbose); err != nil { + gitLog.Printf("Failed to commit changes: %v", err) + return fmt.Errorf("failed to commit changes: %w", err) + } + + // Push the changes (only if remote exists) + if hasRemote() { + gitLog.Print("Remote repository exists, pushing changes") + if err := pushToRemote(verbose); err != nil { + return err + } + } else { + gitLog.Print("No remote repository configured, skipping push") + } + + gitLog.Print("Commit and push workflow completed successfully") + return nil +} diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 72ffd0554c..2f39b0edff 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -444,9 +444,18 @@ func attemptSetSecret(secretName, repoSlug string, verbose bool) error { } // 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 { +func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine string, codespaceRepos []string, codespaceEnabled bool, completions bool, push bool, rootCmd CommandProvider) error { initLog.Print("Starting repository initialization for agentic workflows") + // If --push is enabled, ensure git status is clean before starting + if push { + initLog.Print("Checking for clean working directory (--push enabled)") + if err := checkCleanWorkingDirectory(verbose); err != nil { + initLog.Printf("Git status check failed: %v", err) + return fmt.Errorf("--push requires a clean working directory: %w", err) + } + } + // Ensure we're in a git repository if !isGitRepo() { initLog.Print("Not in a git repository, initialization failed") @@ -680,6 +689,34 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s initLog.Print("Repository initialization completed successfully") + // If --push is enabled, commit and push changes + if push { + initLog.Print("Push enabled - preparing to commit and push changes") + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Preparing to commit and push changes...")) + + // Use the helper function to orchestrate the full workflow + commitMessage := "chore: initialize agentic workflows" + if err := commitAndPushChanges(commitMessage, verbose); err != nil { + // Check if it's the "no changes" case + hasChanges, checkErr := hasChangesToCommit() + if checkErr == nil && !hasChanges { + initLog.Print("No changes to commit") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No changes to commit")) + } else { + return err + } + } else { + // Print success messages based on whether remote exists + fmt.Fprintln(os.Stderr, "") + if hasRemote() { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes pushed to remote")) + } else { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes committed locally (no remote configured)")) + } + } + } + // Display success message with next steps fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Repository initialized for agentic workflows!")) diff --git a/pkg/cli/init_command.go b/pkg/cli/init_command.go index d532f841ee..ffc5272b57 100644 --- a/pkg/cli/init_command.go +++ b/pkg/cli/init_command.go @@ -84,7 +84,8 @@ Examples: ` + 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`, + ` + string(constants.CLIExtensionPrefix) + ` init --completions # Install shell completions + ` + string(constants.CLIExtensionPrefix) + ` init --push # Initialize and automatically commit/push`, RunE: func(cmd *cobra.Command, args []string) error { verbose, _ := cmd.Flags().GetBool("verbose") mcpFlag, _ := cmd.Flags().GetBool("mcp") @@ -95,6 +96,7 @@ Examples: codespaceReposStr, _ := cmd.Flags().GetString("codespaces") codespaceEnabled := cmd.Flags().Changed("codespaces") completions, _ := cmd.Flags().GetBool("completions") + push, _ := cmd.Flags().GetBool("push") // Determine MCP state: default true, unless --no-mcp is specified // --mcp flag is kept for backward compatibility (hidden from help) @@ -122,15 +124,15 @@ Examples: 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") { + !cmd.Flags().Changed("completions") && !cmd.Flags().Changed("push") { // 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("Executing init command: verbose=%v, mcp=%v, campaign=%v, tokens=%v, engine=%v, codespaces=%v, codespaceEnabled=%v, completions=%v, push=%v", verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions, push) + if err := InitRepository(verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions, push, cmd.Root()); err != nil { initCommandLog.Printf("Init command failed: %v", err) return err } @@ -148,6 +150,7 @@ Examples: // NoOptDefVal allows using --codespaces without a value (returns empty string when no value provided) cmd.Flags().Lookup("codespaces").NoOptDefVal = " " cmd.Flags().Bool("completions", false, "Install shell completion for the detected shell (bash, zsh, fish, or PowerShell)") + cmd.Flags().Bool("push", false, "Automatically commit and push changes after successful initialization") // Hide the deprecated --mcp flag from help (kept for backward compatibility) _ = cmd.Flags().MarkHidden("mcp") diff --git a/pkg/cli/init_command_test.go b/pkg/cli/init_command_test.go index 43c6285e53..ba89d58fa1 100644 --- a/pkg/cli/init_command_test.go +++ b/pkg/cli/init_command_test.go @@ -180,9 +180,9 @@ func TestInitRepositoryBasic(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Test basic init with MCP enabled by default (mcp=true, noMcp=false behavior) - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) failed: %v", err) } // Verify .gitattributes was created/updated @@ -245,9 +245,9 @@ func TestInitRepositoryWithMCP(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Test init with MCP explicitly enabled (same as default) - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with MCP failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with MCP failed: %v", err) } // Verify .vscode/mcp.json was created @@ -288,9 +288,9 @@ func TestInitRepositoryWithNoMCP(t *testing.T) { 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, false, nil) + err = InitRepository(false, false, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with --no-mcp failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with --no-mcp failed: %v", err) } // Verify .vscode/mcp.json was NOT created @@ -336,9 +336,9 @@ func TestInitRepositoryWithMCPBackwardCompatibility(t *testing.T) { 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, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with deprecated --mcp flag failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with deprecated --mcp flag failed: %v", err) } // Verify .vscode/mcp.json was created @@ -379,9 +379,9 @@ func TestInitRepositoryVerbose(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Test verbose mode with MCP enabled by default (should not error, just produce more output) - err = InitRepository(true, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(true, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) in verbose mode failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) in verbose mode failed: %v", err) } // Verify basic files were still created @@ -406,12 +406,12 @@ func TestInitRepositoryNotInGitRepo(t *testing.T) { } // Don't initialize git repo - should fail for some operations - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) // The function should handle this gracefully or return an error // Based on the implementation, ensureGitAttributes requires git if err == nil { - t.Log("InitRepository(, false, nil) succeeded despite not being in a git repo") + t.Log("InitRepository(, false, false, nil) succeeded despite not being in a git repo") } } @@ -440,15 +440,15 @@ func TestInitRepositoryIdempotent(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Run init twice with MCP enabled by default - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("First InitRepository(, false, nil) failed: %v", err) + t.Fatalf("First InitRepository(, false, false, nil) failed: %v", err) } // Second run should be idempotent - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("Second InitRepository(, false, nil) failed: %v", err) + t.Fatalf("Second InitRepository(, false, false, nil) failed: %v", err) } // Verify .gitattributes still correct @@ -491,14 +491,14 @@ func TestInitRepositoryWithMCPIdempotent(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Run init with MCP twice - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("First InitRepository(, false, nil) with MCP failed: %v", err) + t.Fatalf("First InitRepository(, false, false, nil) with MCP failed: %v", err) } - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("Second InitRepository(, false, nil) with MCP failed: %v", err) + t.Fatalf("Second InitRepository(, false, false, nil) with MCP failed: %v", err) } // Verify files still exist and are correct @@ -538,9 +538,9 @@ func TestInitRepositoryCreatesDirectories(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Run init with MCP - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) failed: %v", err) } // Verify directory structure @@ -606,7 +606,7 @@ func TestInitRepositoryErrorHandling(t *testing.T) { } // Test init without git repo (with MCP enabled by default) - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) // Should handle error gracefully or return error // The actual behavior depends on implementation @@ -649,9 +649,9 @@ func TestInitRepositoryWithExistingFiles(t *testing.T) { } // Run init with MCP enabled by default - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) failed: %v", err) } // Verify existing content is preserved and new entry is added @@ -699,9 +699,9 @@ func TestInitRepositoryWithCodespace(t *testing.T) { // Test init with --codespaces flag (with MCP enabled by default and additional repos) additionalRepos := []string{"org/repo1", "owner/repo2"} - err = InitRepository(false, true, false, false, "", additionalRepos, true, false, nil) + err = InitRepository(false, true, false, false, "", additionalRepos, true, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with codespaces failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with codespaces failed: %v", err) } // Verify .devcontainer/devcontainer.json was created at default location @@ -764,9 +764,9 @@ func TestInitCommandWithCodespacesNoArgs(t *testing.T) { exec.Command("git", "config", "user.email", "test@example.com").Run() // Test init with --codespaces flag (no additional repos, MCP enabled by default) - err = InitRepository(false, true, false, false, "", []string{}, true, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, true, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with codespaces (no args) failed: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with codespaces (no args) failed: %v", err) } // Verify .devcontainer/devcontainer.json was created at default location diff --git a/pkg/cli/init_mcp_test.go b/pkg/cli/init_mcp_test.go index 9ab92a75c8..f5e32bda3e 100644 --- a/pkg/cli/init_mcp_test.go +++ b/pkg/cli/init_mcp_test.go @@ -40,9 +40,9 @@ func TestInitRepository_WithMCP(t *testing.T) { } // Call the function with MCP flag (no campaign agent) - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with MCP returned error: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with MCP returned error: %v", err) } // Verify standard files were created @@ -133,15 +133,15 @@ func TestInitRepository_MCP_Idempotent(t *testing.T) { } // Call the function first time with MCP - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with MCP returned error on first call: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with MCP returned error on first call: %v", err) } // Call the function second time with MCP - err = InitRepository(false, true, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) with MCP returned error on second call: %v", err) + t.Fatalf("InitRepository(, false, false, nil) with MCP returned error on second call: %v", err) } // Verify files still exist diff --git a/pkg/cli/init_test.go b/pkg/cli/init_test.go index 21ed1253a3..5a00cb3c0e 100644 --- a/pkg/cli/init_test.go +++ b/pkg/cli/init_test.go @@ -54,18 +54,18 @@ func TestInitRepository(t *testing.T) { } // Call the function (no MCP or campaign) - err = InitRepository(false, false, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, false, false, false, "", []string{}, false, false, false, nil) // Check error expectation if tt.wantError { if err == nil { - t.Errorf("InitRepository(, false, nil) expected error, got nil") + t.Errorf("InitRepository(, false, false, nil) expected error, got nil") } return } if err != nil { - t.Fatalf("InitRepository(, false, nil) returned unexpected error: %v", err) + t.Fatalf("InitRepository(, false, false, nil) returned unexpected error: %v", err) } // Verify .gitattributes was created @@ -161,15 +161,15 @@ func TestInitRepository_Idempotent(t *testing.T) { } // Call the function first time - err = InitRepository(false, false, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, false, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) returned error on first call: %v", err) + t.Fatalf("InitRepository(, false, false, nil) returned error on first call: %v", err) } // Call the function second time - err = InitRepository(false, false, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, false, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) returned error on second call: %v", err) + t.Fatalf("InitRepository(, false, false, nil) returned error on second call: %v", err) } // Verify files still exist and are correct @@ -233,9 +233,9 @@ func TestInitRepository_Verbose(t *testing.T) { } // Call the function with verbose=true (should not error) - err = InitRepository(true, false, false, false, "", []string{}, false, false, nil) + err = InitRepository(true, false, false, false, "", []string{}, false, false, false, nil) if err != nil { - t.Fatalf("InitRepository(, false, nil) returned error with verbose=true: %v", err) + t.Fatalf("InitRepository(, false, false, nil) returned error with verbose=true: %v", err) } // Verify files were created @@ -268,7 +268,7 @@ func TestInitRepository_Campaign(t *testing.T) { } // Call the function with campaign flag enabled - err = InitRepository(false, true, true, false, "", []string{}, false, false, nil) + err = InitRepository(false, true, true, false, "", []string{}, false, false, false, nil) if err != nil { t.Fatalf("InitRepository with campaign flag returned error: %v", err) } diff --git a/pkg/cli/interfaces_test.go b/pkg/cli/interfaces_test.go index 74566bd465..cc8b835f08 100644 --- a/pkg/cli/interfaces_test.go +++ b/pkg/cli/interfaces_test.go @@ -65,7 +65,7 @@ func TestInitRepository_WithNilRootCmd(t *testing.T) { require.NoError(t, err, "Failed to init git repo") // InitRepository with nil rootCmd and completions disabled should succeed - err = InitRepository(false, false, false, false, "", []string{}, false, false, nil) + err = InitRepository(false, false, false, false, "", []string{}, false, false, false, nil) require.NoError(t, err, "InitRepository with nil rootCmd should succeed when completions are disabled") } @@ -94,7 +94,7 @@ func TestInitRepository_WithRootCmd(t *testing.T) { } // InitRepository with real rootCmd should succeed - err = InitRepository(false, false, false, false, "", []string{}, false, false, rootCmd) + err = InitRepository(false, false, false, false, "", []string{}, false, false, false, rootCmd) require.NoError(t, err, "InitRepository with rootCmd should succeed") } diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go index 51bcf51554..3105ac471d 100644 --- a/pkg/cli/upgrade_command.go +++ b/pkg/cli/upgrade_command.go @@ -3,8 +3,6 @@ package cli import ( "fmt" "os" - "os/exec" - "strings" "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/constants" @@ -186,92 +184,24 @@ func runUpgradeCommand(verbose bool, workflowDir string, noFix bool, noCompile b fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Preparing to commit and push changes...")) - // Check if there are any changes to commit - upgradeLog.Print("Checking for modified files") - cmd := exec.Command("git", "status", "--porcelain") - output, err := cmd.Output() - if err != nil { - upgradeLog.Printf("Failed to check git status: %v", err) - return fmt.Errorf("failed to check git status: %w", err) - } - - if len(strings.TrimSpace(string(output))) == 0 { - upgradeLog.Print("No changes to commit") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No changes to commit")) - return nil - } - - // Pull latest changes from remote before committing (if remote exists) - upgradeLog.Print("Checking for remote repository") - checkRemoteCmd := exec.Command("git", "remote", "get-url", "origin") - if err := checkRemoteCmd.Run(); err == nil { - // Remote exists, pull changes - upgradeLog.Print("Pulling latest changes from remote") - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Pulling latest changes from remote...")) - } - pullCmd := exec.Command("git", "pull", "--rebase") - if output, err := pullCmd.CombinedOutput(); err != nil { - upgradeLog.Printf("Failed to pull changes: %v, output: %s", err, string(output)) - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to pull changes: %v", err))) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("You may need to manually resolve conflicts and push")) - return fmt.Errorf("failed to pull changes: %w", err) - } - if verbose { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Pulled latest changes")) - } - } else { - upgradeLog.Print("No remote repository configured, skipping pull") - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No remote repository configured, skipping pull")) - } - } - - // Stage all modified files - upgradeLog.Print("Staging all changes") - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Staging changes...")) - } - addCmd := exec.Command("git", "add", "-A") - if output, err := addCmd.CombinedOutput(); err != nil { - upgradeLog.Printf("Failed to stage changes: %v, output: %s", err, string(output)) - return fmt.Errorf("failed to stage changes: %w", err) - } - - // Commit the changes - upgradeLog.Print("Committing changes") + // Use the helper function to orchestrate the full workflow commitMessage := "chore: upgrade agentic workflows" - if err := commitChanges(commitMessage, verbose); err != nil { - upgradeLog.Printf("Failed to commit changes: %v", err) - return fmt.Errorf("failed to commit changes: %w", err) - } - if verbose { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes committed")) - } - - // Push the changes (only if remote exists) - upgradeLog.Print("Checking if remote repository exists for push") - checkRemoteCmd = exec.Command("git", "remote", "get-url", "origin") - if err := checkRemoteCmd.Run(); err == nil { - // Remote exists, push changes - upgradeLog.Print("Pushing changes to remote") - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Pushing to remote...")) - } - pushCmd := exec.Command("git", "push") - if output, err := pushCmd.CombinedOutput(); err != nil { - upgradeLog.Printf("Failed to push changes: %v, output: %s", err, string(output)) - return fmt.Errorf("failed to push changes: %w\nOutput: %s", err, string(output)) + if err := commitAndPushChanges(commitMessage, verbose); err != nil { + // Check if it's the "no changes" case + hasChanges, checkErr := hasChangesToCommit() + if checkErr == nil && !hasChanges { + upgradeLog.Print("No changes to commit") + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No changes to commit")) + return nil } + return err + } - fmt.Fprintln(os.Stderr, "") + // Print success messages based on whether remote exists + fmt.Fprintln(os.Stderr, "") + if hasRemote() { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes pushed to remote")) } else { - upgradeLog.Print("No remote repository configured, skipping push") - if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No remote repository configured, changes committed locally")) - } - fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes committed locally (no remote configured)")) } }