Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 77 additions & 16 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Examples:
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/workflows/ci-doctor.md@main
` + string(constants.CLIExtensionPrefix) + ` add https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor.md
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor --create-pull-request --force
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor --push # Add and push changes
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/*
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/*@v1.0.0
` + string(constants.CLIExtensionPrefix) + ` add githubnext/agentics/ci-doctor --dir shared # Add to .github/workflows/shared/
Expand All @@ -47,6 +48,7 @@ Workflow specifications:
The -n flag allows you to specify a custom name for the workflow file (only applies to the first workflow when adding multiple).
The --dir flag allows you to specify a subdirectory under .github/workflows/ where the workflow will be added.
The --create-pull-request flag (or --pr) automatically creates a pull request with the workflow changes.
The --push flag automatically commits and pushes changes after successful workflow addition.
The --force flag overwrites existing workflow files.

Note: To create a new workflow from scratch, use the 'new' command instead.`,
Expand All @@ -59,6 +61,7 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,
createPRFlag, _ := cmd.Flags().GetBool("create-pull-request")
prFlagAlias, _ := cmd.Flags().GetBool("pr")
prFlag := createPRFlag || prFlagAlias // Support both --create-pull-request and --pr
pushFlag, _ := cmd.Flags().GetBool("push")
forceFlag, _ := cmd.Flags().GetBool("force")
appendText, _ := cmd.Flags().GetString("append")
verbose, _ := cmd.Flags().GetBool("verbose")
Expand All @@ -73,9 +76,9 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,

// Handle normal mode
if prFlag {
return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, noGitattributes, workflowDir, noStopAfter, stopAfter)
return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter)
} else {
return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, noGitattributes, workflowDir, noStopAfter, stopAfter)
return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter)
}
},
}
Expand All @@ -97,6 +100,9 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,
cmd.Flags().Bool("pr", false, "Alias for --create-pull-request")
_ = cmd.Flags().MarkHidden("pr") // Hide the short alias from help output

// Add push flag to add command
cmd.Flags().Bool("push", false, "Automatically commit and push changes after successful workflow addition")

// Add force flag to add command
cmd.Flags().BoolP("force", "f", false, "Overwrite existing workflow files without confirmation")

Expand Down Expand Up @@ -124,8 +130,8 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,

// AddWorkflows adds one or more workflows from components to .github/workflows
// with optional repository installation and PR creation
func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) error {
addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, noGitattributes=%v, workflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflows), engineOverride, createPR, noGitattributes, workflowDir, noStopAfter, stopAfter)
func AddWorkflows(workflows []string, number int, verbose bool, engineOverride string, name string, force bool, appendText string, createPR bool, push bool, noGitattributes bool, workflowDir string, noStopAfter bool, stopAfter string) error {
addLog.Printf("Adding workflows: count=%d, engineOverride=%s, createPR=%v, push=%v, noGitattributes=%v, workflowDir=%s, noStopAfter=%v, stopAfter=%s", len(workflows), engineOverride, createPR, push, noGitattributes, workflowDir, noStopAfter, stopAfter)

if len(workflows) == 0 {
return fmt.Errorf("at least one workflow name is required")
Expand All @@ -143,21 +149,27 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s
return handleRepoOnlySpec(workflows[0], verbose)
}

// If creating a PR, check prerequisites
if createPR {
// Check if GitHub CLI is available
if !isGHCLIAvailable() {
return fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available")
}

// If creating a PR or pushing, check prerequisites
if createPR || push {
// Check if we're in a git repository
if !isGitRepo() {
return fmt.Errorf("not in a git repository - PR creation requires a git repository")
if createPR {
return fmt.Errorf("not in a git repository - PR creation requires a git repository")
}
return fmt.Errorf("not in a git repository - push requires a git repository")
}

// Check no other changes are present
if err := checkCleanWorkingDirectory(verbose); err != nil {
return fmt.Errorf("working directory is not clean: %w", err)
if createPR {
return fmt.Errorf("working directory is not clean: %w", err)
}
return fmt.Errorf("--push requires a clean working directory: %w", err)
}

// Check if GitHub CLI is available (only for PR)
if createPR && !isGHCLIAvailable() {
return fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available")
}
}

Expand Down Expand Up @@ -243,7 +255,7 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s

// Handle normal workflow addition
addLog.Print("Adding workflows normally without PR")
return addWorkflowsNormal(processedWorkflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter)
return addWorkflowsNormal(processedWorkflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter)
}

// handleRepoOnlySpec handles the case when user provides only owner/repo without workflow name
Expand Down Expand Up @@ -398,7 +410,7 @@ func displayAvailableWorkflows(repoSlug, version string, verbose bool) error {
}

// addWorkflowsNormal handles normal workflow addition without PR creation
func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, engineOverride string, name string, force bool, appendText string, push bool, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string) error {
// Create file tracker for all operations
tracker, err := NewFileTracker()
if err != nil {
Expand Down Expand Up @@ -448,6 +460,55 @@ func addWorkflowsNormal(workflows []*WorkflowSpec, number int, verbose bool, eng
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully added all %d workflows", len(workflows))))
}

// If --push is enabled, commit and push changes
if push {
addLog.Print("Push enabled - preparing to commit and push changes")
fmt.Fprintln(os.Stderr, "")

// Check if we're on the default branch
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking current branch..."))
if err := checkOnDefaultBranch(verbose); err != nil {
addLog.Printf("Default branch check failed: %v", err)
return fmt.Errorf("cannot push: %w", err)
}

// Confirm with user (skip in CI)
if err := confirmPushOperation(verbose); err != nil {
addLog.Printf("Push operation not confirmed: %v", err)
return fmt.Errorf("push operation cancelled: %w", err)
}

fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Preparing to commit and push changes..."))

// Create commit message
var commitMessage string
if len(workflows) == 1 {
commitMessage = fmt.Sprintf("chore: add workflow %s", workflows[0].WorkflowName)
} else {
commitMessage = fmt.Sprintf("chore: add %d workflows", len(workflows))
}

// Use the helper function to orchestrate the full workflow
if err := commitAndPushChanges(commitMessage, verbose); err != nil {
// Check if it's the "no changes" case
hasChanges, checkErr := hasChangesToCommit()
if checkErr == nil && !hasChanges {
addLog.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)"))
}
}
}

return nil
}

Expand Down Expand Up @@ -481,7 +542,7 @@ func addWorkflowsWithPR(workflows []*WorkflowSpec, number int, verbose bool, eng
}()

// Add workflows using the normal function logic
if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil {
if err := addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, false, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter); err != nil {
// Rollback on error
if rollbackErr := tracker.RollbackAllFiles(verbose); rollbackErr != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to rollback files: %v", rollbackErr)))
Expand Down
6 changes: 5 additions & 1 deletion pkg/cli/add_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ func TestNewAddCommand(t *testing.T) {
prFlag := flags.Lookup("pr")
assert.NotNil(t, prFlag, "Should have 'pr' flag (alias)")

// Check push flag
pushFlag := flags.Lookup("push")
assert.NotNil(t, pushFlag, "Should have 'push' flag")

// Check force flag
forceFlag := flags.Lookup("force")
assert.NotNil(t, forceFlag, "Should have 'force' flag")
Expand Down Expand Up @@ -78,7 +82,7 @@ func TestNewAddCommand(t *testing.T) {
}

func TestAddWorkflows_EmptyWorkflows(t *testing.T) {
err := AddWorkflows([]string{}, 1, false, "", "", false, "", false, false, "", false, "")
err := AddWorkflows([]string{}, 1, false, "", "", false, "", false, false, false, "", false, "")
require.Error(t, err, "Should error when no workflows are provided")
assert.Contains(t, err.Error(), "at least one workflow", "Error should mention missing workflow")
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/add_current_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestAddWorkflowsFromCurrentRepository(t *testing.T) {
// Clear cache before each test
ClearCurrentRepoSlugCache()

err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, "", false, "")
err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "")

if tt.expectError {
if err == nil {
Expand Down Expand Up @@ -179,7 +179,7 @@ func TestAddWorkflowsFromCurrentRepositoryMultiple(t *testing.T) {
// Clear cache before each test
ClearCurrentRepoSlugCache()

err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, "", false, "")
err := AddWorkflows(tt.workflowSpecs, 1, false, "", "", false, "", false, false, false, "", false, "")

if tt.expectError {
if err == nil {
Expand Down Expand Up @@ -220,7 +220,7 @@ func TestAddWorkflowsFromCurrentRepositoryNotInGitRepo(t *testing.T) {

// When not in a git repo, the check should be skipped (can't determine current repo)
// The function should proceed and fail for other reasons (e.g., workflow not found)
err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, "", false, "")
err = AddWorkflows([]string{"some-owner/some-repo/workflow"}, 1, false, "", "", false, "", false, false, false, "", false, "")

// Should NOT get the "cannot add workflows from the current repository" error
if err != nil && strings.Contains(err.Error(), "cannot add workflows from the current repository") {
Expand Down
9 changes: 6 additions & 3 deletions pkg/cli/add_gitattributes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ This is a test workflow.`
os.Remove(".gitattributes")

// Call addWorkflowsNormal with noGitattributes=false
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false, "", false, "")
// Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter)
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false, false, "", false, "")
if err != nil {
// We expect this to fail because we don't have a full workflow setup,
// but gitattributes should still be updated before the error
Expand Down Expand Up @@ -112,7 +113,8 @@ This is a test workflow.`
os.Remove(".gitattributes")

// Call addWorkflowsNormal with noGitattributes=true
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "", false, "")
// Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter)
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, true, false, "", false, "")
if err != nil {
// We expect this to fail because we don't have a full workflow setup
t.Logf("Expected error during workflow addition: %v", err)
Expand All @@ -134,7 +136,8 @@ This is a test workflow.`
}

// Call addWorkflowsNormal with noGitattributes=true
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "", false, "")
// Signature: addWorkflowsNormal(workflows, number, verbose, engineOverride, name, force, appendText, push, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter)
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, true, false, "", false, "")
if err != nil {
// We expect this to fail because we don't have a full workflow setup
t.Logf("Expected error during workflow addition: %v", err)
Expand Down
77 changes: 71 additions & 6 deletions pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -447,18 +448,32 @@ 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, push bool, rootCmd CommandProvider) error {
func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine string, codespaceRepos []string, codespaceEnabled bool, completions bool, push bool, shouldCreatePR 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 --push or --create-pull-request is enabled, ensure git status is clean before starting
if push || shouldCreatePR {
if shouldCreatePR {
initLog.Print("Checking for clean working directory (--create-pull-request enabled)")
} else {
initLog.Print("Checking for clean working directory (--push enabled)")
}
if err := checkCleanWorkingDirectory(verbose); err != nil {
initLog.Printf("Git status check failed: %v", err)
if shouldCreatePR {
return fmt.Errorf("--create-pull-request requires a clean working directory: %w", err)
}
return fmt.Errorf("--push requires a clean working directory: %w", err)
}
}

// If creating a PR, check GitHub CLI is available
if shouldCreatePR {
if !isGHCLIAvailable() {
return fmt.Errorf("GitHub CLI (gh) is required for PR creation but not available")
}
}

// Ensure we're in a git repository
if !isGitRepo() {
initLog.Print("Not in a git repository, initialization failed")
Expand Down Expand Up @@ -679,8 +694,58 @@ 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 {
// If --create-pull-request is enabled, create branch, commit, push, and create PR
if shouldCreatePR {
initLog.Print("Create PR enabled - preparing to create branch, commit, push, and create PR")
fmt.Fprintln(os.Stderr, "")

// Get current branch for restoration later
currentBranch, err := getCurrentBranch()
if err != nil {
return fmt.Errorf("failed to get current branch: %w", err)
}

// Create temporary branch
branchName := fmt.Sprintf("init-agentic-workflows-%d", rand.Intn(9000)+1000)
if err := createAndSwitchBranch(branchName, verbose); err != nil {
return fmt.Errorf("failed to create branch %s: %w", branchName, err)
}

// Commit changes
commitMessage := "chore: initialize agentic workflows"
if err := commitChanges(commitMessage, verbose); err != nil {
// Switch back to original branch before returning error
_ = switchBranch(currentBranch, verbose)
return fmt.Errorf("failed to commit changes: %w", err)
}

// Push branch
if err := pushBranch(branchName, verbose); err != nil {
// Switch back to original branch before returning error
_ = switchBranch(currentBranch, verbose)
return fmt.Errorf("failed to push branch %s: %w", branchName, err)
}

// Create PR
prTitle := "Initialize agentic workflows"
prBody := "This PR initializes the repository for agentic workflows by:\n" +
"- Configuring .gitattributes\n" +
"- Creating GitHub Copilot custom instructions\n" +
"- Setting up workflow prompts and agents"
if err := createPR(branchName, prTitle, prBody, verbose); err != nil {
// Switch back to original branch before returning error
_ = switchBranch(currentBranch, verbose)
return fmt.Errorf("failed to create PR: %w", err)
}

// Switch back to original branch
if err := switchBranch(currentBranch, verbose); err != nil {
return fmt.Errorf("failed to switch back to branch %s: %w", currentBranch, err)
}

fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Created PR for initialization"))
} else if push {
// If --push is enabled, commit and push changes
initLog.Print("Push enabled - preparing to commit and push changes")
fmt.Fprintln(os.Stderr, "")

Expand Down
Loading
Loading