Skip to content
Closed
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
92 changes: 82 additions & 10 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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 adding workflows.
The --force flag overwrites existing workflow files.

Note: To create a new workflow from scratch, use the 'new' command instead.`,
Expand All @@ -59,6 +60,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 @@ -71,12 +73,18 @@ Note: To create a new workflow from scratch, use the 'new' command instead.`,
return err
}

// Handle normal mode
// Validate that --push and --create-pull-request are mutually exclusive
if prFlag && pushFlag {
return fmt.Errorf("cannot use both --create-pull-request and --push flags together")
}

// Handle PR mode
if prFlag {
return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, true, 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, true, false, noGitattributes, workflowDir, noStopAfter, stopAfter)
}

// Handle push mode or normal mode
return AddWorkflows(workflows, numberFlag, verbose, engineOverride, nameFlag, forceFlag, appendText, false, pushFlag, noGitattributes, workflowDir, noStopAfter, stopAfter)
},
}

Expand All @@ -97,6 +105,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 adding workflows")

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

Expand All @@ -123,9 +134,9 @@ 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)
// with optional repository installation, PR creation, or push
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 Down Expand Up @@ -161,6 +172,19 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s
}
}

// If push is enabled, check prerequisites
if push {
// Check if we're in a git repository
if !isGitRepo() {
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("--push requires a clean working directory: %w", err)
}
}

// Parse workflow specifications and group by repository
repoVersions := make(map[string]string) // repo -> version
processedWorkflows := []*WorkflowSpec{} // List of processed workflow specs
Expand Down Expand Up @@ -243,7 +267,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, noGitattributes, hasWildcard, workflowDir, noStopAfter, stopAfter, push)
}

// handleRepoOnlySpec handles the case when user provides only owner/repo without workflow name
Expand Down Expand Up @@ -398,7 +422,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, noGitattributes bool, fromWildcard bool, workflowDir string, noStopAfter bool, stopAfter string, push bool) error {
// Create file tracker for all operations
tracker, err := NewFileTracker()
if err != nil {
Expand Down Expand Up @@ -448,6 +472,54 @@ 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..."))

// Generate commit message based on the workflows added
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))
}

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 +553,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, noGitattributes, fromWildcard, workflowDir, noStopAfter, stopAfter, false); 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
6 changes: 3 additions & 3 deletions pkg/cli/add_gitattributes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ This is a test workflow.`
os.Remove(".gitattributes")

// Call addWorkflowsNormal with noGitattributes=false
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", false, false, "", false, "")
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 +112,7 @@ This is a test workflow.`
os.Remove(".gitattributes")

// Call addWorkflowsNormal with noGitattributes=true
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "", false, "")
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "", 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 +134,7 @@ This is a test workflow.`
}

// Call addWorkflowsNormal with noGitattributes=true
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "", false, "")
err := addWorkflowsNormal([]*WorkflowSpec{spec}, 1, false, "", "", false, "", true, false, "", 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
75 changes: 74 additions & 1 deletion 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,7 +448,7 @@ 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, createPullRequest bool, rootCmd CommandProvider) error {
initLog.Print("Starting repository initialization for agentic workflows")

// If --push is enabled, ensure git status is clean before starting
Expand All @@ -459,6 +460,24 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s
}
}

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

// Check if we're in a git repository
if !isGitRepo() {
return fmt.Errorf("not in a git repository - PR creation 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)
}
}

// Ensure we're in a git repository
if !isGitRepo() {
initLog.Print("Not in a git repository, initialization failed")
Expand Down Expand Up @@ -679,6 +698,60 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s

initLog.Print("Repository initialization completed successfully")

// If --create-pull-request is enabled, create a PR with changes
if createPullRequest {
initLog.Print("PR creation enabled - preparing to create pull request")
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)
}

// Ensure we switch back to the original branch on any error
defer func() {
if err := switchBranch(currentBranch, verbose); err != nil {
initLog.Printf("Warning: failed to switch back to branch %s: %v", currentBranch, err)
}
}()

// Commit changes
commitMessage := "chore: initialize agentic workflows"
if err := commitChanges(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 - skipping PR creation"))
return nil
}
return fmt.Errorf("failed to commit changes: %w", err)
}

// Push branch
if err := pushBranch(branchName, verbose); err != nil {
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."
if err := createPR(branchName, prTitle, prBody, verbose); err != nil {
return fmt.Errorf("failed to create pull request: %w", err)
}

fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Pull request created successfully"))
fmt.Fprintln(os.Stderr, "")
}

// If --push is enabled, commit and push changes
if push {
initLog.Print("Push enabled - preparing to commit and push changes")
Expand Down
Loading
Loading