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
83 changes: 63 additions & 20 deletions pkg/cli/add_interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,7 @@ func (c *AddInteractiveConfig) showWorkflowDescriptions() {
func (c *AddInteractiveConfig) checkGHAuthStatus() error {
addInteractiveLog.Print("Checking GitHub CLI authentication status")

cmd := exec.Command("gh", "auth", "status")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Checking GitHub authentication...", "auth", "status")

if err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage("You are not logged in to GitHub CLI."))
Expand Down Expand Up @@ -258,9 +257,7 @@ func (c *AddInteractiveConfig) checkRepoVisibility() bool {
addInteractiveLog.Print("Checking repository visibility")

// Use gh api to check repository visibility
args := []string{"api", fmt.Sprintf("/repos/%s", c.RepoOverride), "--jq", ".visibility"}
cmd := workflow.ExecGH(args...)
output, err := cmd.Output()
output, err := workflow.RunGH("Checking repository visibility...", "api", fmt.Sprintf("/repos/%s", c.RepoOverride), "--jq", ".visibility")
if err != nil {
addInteractiveLog.Printf("Could not check repository visibility: %v", err)
// Default to public if we can't determine
Expand All @@ -278,9 +275,7 @@ func (c *AddInteractiveConfig) checkActionsEnabled() error {
addInteractiveLog.Print("Checking if GitHub Actions is enabled")

// Use gh api to check Actions permissions
args := []string{"api", fmt.Sprintf("/repos/%s/actions/permissions", c.RepoOverride), "--jq", ".enabled"}
cmd := workflow.ExecGH(args...)
output, err := cmd.Output()
output, err := workflow.RunGH("Checking GitHub Actions status...", "api", fmt.Sprintf("/repos/%s/actions/permissions", c.RepoOverride), "--jq", ".enabled")
if err != nil {
addInteractiveLog.Printf("Failed to check Actions status: %v", err)
// If we can't check, warn but continue - actual operations will fail if Actions is disabled
Expand Down Expand Up @@ -349,9 +344,7 @@ func (c *AddInteractiveConfig) checkExistingSecrets() error {
c.existingSecrets = make(map[string]bool)

// Use gh api to list repository secrets
args := []string{"api", fmt.Sprintf("/repos/%s/actions/secrets", c.RepoOverride), "--jq", ".secrets[].name"}
cmd := workflow.ExecGH(args...)
output, err := cmd.Output()
output, err := workflow.RunGH("Checking repository secrets...", "api", fmt.Sprintf("/repos/%s/actions/secrets", c.RepoOverride), "--jq", ".secrets[].name")
if err != nil {
addInteractiveLog.Printf("Could not fetch existing secrets: %v", err)
// Continue without error - we'll just assume no secrets exist
Expand Down Expand Up @@ -709,7 +702,6 @@ func (c *AddInteractiveConfig) applyChanges(ctx context.Context, workflowFiles,
addInteractiveLog.Print("Applying changes")

fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Creating pull request..."))

// Add the workflow using existing implementation with --create-pull-request
// Pass the resolved workflows to avoid re-fetching them
Expand All @@ -722,8 +714,6 @@ func (c *AddInteractiveConfig) applyChanges(ctx context.Context, workflowFiles,
c.addResult = result

// Step 8b: Auto-merge the PR
fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Merging pull request..."))

if result.PRNumber == 0 {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Could not determine PR number"))
fmt.Fprintln(os.Stderr, "Please merge the PR manually from the GitHub web interface.")
Expand Down Expand Up @@ -764,22 +754,68 @@ func (c *AddInteractiveConfig) applyChanges(ctx context.Context, workflowFiles,
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Secret '%s' added", secretName)))
}

// Step 8d: Update local branch with merged changes from GitHub
if err := c.updateLocalBranch(); err != nil {
// Non-fatal - warn but continue, workflow can still run on GitHub
addInteractiveLog.Printf("Failed to update local branch: %v", err)
if c.Verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Could not update local branch: %v", err)))
}
}

return nil
}

// updateLocalBranch fetches and pulls the latest changes from GitHub after PR merge
func (c *AddInteractiveConfig) updateLocalBranch() error {
addInteractiveLog.Print("Updating local branch with merged changes")

// Get the default branch name using gh
output, err := workflow.RunGHCombined("Getting default branch...", "repo", "view", "--repo", c.RepoOverride, "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name")
defaultBranch := "main"
if err == nil {
defaultBranch = strings.TrimSpace(string(output))
}
addInteractiveLog.Printf("Default branch: %s", defaultBranch)

// Fetch the latest changes from origin
if c.Verbose {
fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Fetching latest changes from GitHub..."))
}

// Use git fetch followed by git pull
fetchCmd := exec.Command("git", "fetch", "origin", defaultBranch)
fetchOutput, err := fetchCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git fetch failed: %w (output: %s)", err, string(fetchOutput))
}

pullCmd := exec.Command("git", "pull", "origin", defaultBranch)
pullOutput, err := pullCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("git pull failed: %w (output: %s)", err, string(pullOutput))
}

if c.Verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Local branch updated with merged changes"))
}

return nil
}

// mergePullRequest merges the specified PR
func (c *AddInteractiveConfig) mergePullRequest(prNumber int) error {
cmd := workflow.ExecGH("pr", "merge", fmt.Sprintf("%d", prNumber), "--repo", c.RepoOverride, "--merge")
if output, err := cmd.CombinedOutput(); err != nil {
output, err := workflow.RunGHCombined("Merging pull request...", "pr", "merge", fmt.Sprintf("%d", prNumber), "--repo", c.RepoOverride, "--merge")
if err != nil {
return fmt.Errorf("merge failed: %w (output: %s)", err, string(output))
}
return nil
}

// addRepositorySecret adds a secret to the repository
func (c *AddInteractiveConfig) addRepositorySecret(name, value string) error {
cmd := workflow.ExecGH("secret", "set", name, "--repo", c.RepoOverride, "--body", value)
if output, err := cmd.CombinedOutput(); err != nil {
output, err := workflow.RunGHCombined("Adding repository secret...", "secret", "set", name, "--repo", c.RepoOverride, "--body", value)
if err != nil {
return fmt.Errorf("failed to set secret: %w (output: %s)", err, string(output))
}
return nil
Expand Down Expand Up @@ -934,8 +970,7 @@ func getWorkflowStatuses(pattern, repoOverride string, verbose bool) ([]Workflow
fmt.Fprintf(os.Stderr, "Running: gh %s\n", strings.Join(args, " "))
}

cmd := workflow.ExecGH(args...)
output, err := cmd.Output()
output, err := workflow.RunGH("Checking workflow status...", args...)
if err != nil {
if verbose {
fmt.Fprintf(os.Stderr, "gh workflow list failed: %v\n", err)
Expand Down Expand Up @@ -972,6 +1007,14 @@ func (c *AddInteractiveConfig) showFinalInstructions() {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("🎉 Addition complete!"))
fmt.Fprintln(os.Stderr, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Fprintln(os.Stderr, "")

// Show summary with workflow name(s)
if c.resolvedWorkflows != nil && len(c.resolvedWorkflows.Workflows) > 0 {
wf := c.resolvedWorkflows.Workflows[0]
fmt.Fprintf(os.Stderr, "The workflow '%s' has been added to the repository and will now run automatically.\n", wf.Spec.WorkflowName)
c.showWorkflowDescriptions()
}

fmt.Fprintln(os.Stderr, "Useful commands:")
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s status # Check workflow status", string(constants.CLIExtensionPrefix))))
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(fmt.Sprintf(" %s run <workflow> # Trigger a workflow", string(constants.CLIExtensionPrefix))))
Expand Down
6 changes: 2 additions & 4 deletions pkg/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,7 @@ func auditJobRun(runID int64, jobID int64, stepNumber int, owner, repo, hostname
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Executing: gh %s", strings.Join(args, " "))))
}

cmd := workflow.ExecGH(args...)
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching job logs...", args...)
if err != nil {
return fmt.Errorf("failed to fetch job logs: %w\nOutput: %s", err, string(output))
}
Expand Down Expand Up @@ -622,8 +621,7 @@ func fetchWorkflowRunMetadata(runID int64, owner, repo, hostname string, verbose
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Executing: gh %s", strings.Join(args, " "))))
}

cmd := workflow.ExecGH(args...)
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching run metadata...", args...)
if err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(string(output)))
Expand Down
18 changes: 7 additions & 11 deletions pkg/cli/download_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@ func isBranchRefViaGit(repo, ref string) (bool, error) {
//nolint:unused // Reserved for future use
func isBranchRef(repo, ref string) (bool, error) {
// Use gh CLI to list branches
cmd := workflow.ExecGH("api", fmt.Sprintf("/repos/%s/branches", repo), "--jq", ".[].name")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching branches...", "api", fmt.Sprintf("/repos/%s/branches", repo), "--jq", ".[].name")
if err != nil {
// Check if this is an authentication error
outputStr := string(output)
Expand Down Expand Up @@ -205,8 +204,7 @@ func resolveBranchHead(repo, branch string, verbose bool) (string, error) {
}

// Use gh CLI to get branch info
cmd := workflow.ExecGH("api", fmt.Sprintf("/repos/%s/branches/%s", repo, branch), "--jq", ".commit.sha")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching branch info...", "api", fmt.Sprintf("/repos/%s/branches/%s", repo, branch), "--jq", ".commit.sha")
if err != nil {
// Check if this is an authentication error
outputStr := string(output)
Expand Down Expand Up @@ -295,8 +293,7 @@ func resolveDefaultBranchHead(repo string, verbose bool) (string, error) {
}

// First get the default branch name
cmd := workflow.ExecGH("api", fmt.Sprintf("/repos/%s", repo), "--jq", ".default_branch")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching repository info...", "api", fmt.Sprintf("/repos/%s", repo), "--jq", ".default_branch")
if err != nil {
// Check if this is an authentication error
outputStr := string(output)
Expand Down Expand Up @@ -458,8 +455,7 @@ func downloadWorkflowContent(repo, path, ref string, verbose bool) ([]byte, erro
}

// Use gh CLI to download the file
cmd := workflow.ExecGH("api", fmt.Sprintf("/repos/%s/contents/%s?ref=%s", repo, path, ref), "--jq", ".content")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Downloading workflow...", "api", fmt.Sprintf("/repos/%s/contents/%s?ref=%s", repo, path, ref), "--jq", ".content")
if err != nil {
// Check if this is an authentication error
outputStr := string(output)
Expand All @@ -477,9 +473,9 @@ func downloadWorkflowContent(repo, path, ref string, verbose bool) ([]byte, erro

// The content is base64 encoded, decode it
contentBase64 := strings.TrimSpace(string(output))
cmd = exec.Command("base64", "-d")
cmd.Stdin = strings.NewReader(contentBase64)
content, err := cmd.Output()
base64Cmd := exec.Command("base64", "-d")
base64Cmd.Stdin = strings.NewReader(contentBase64)
content, err := base64Cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to decode file content: %w", err)
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,7 @@ 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 {
if output, err := workflow.RunGHCombined("Setting secret...", "secret", "set", secretName, "--repo", repoSlug, "--body", secretValue); err != nil {
outputStr := string(output)
// Check for permission-related errors
if strings.Contains(outputStr, "403") || strings.Contains(outputStr, "Forbidden") ||
Expand Down
3 changes: 1 addition & 2 deletions pkg/cli/logs_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,7 @@ func downloadWorkflowRunLogs(runID int64, outputDir string, verbose bool) error

// Use gh api to download the logs zip file
// The endpoint returns a 302 redirect to the actual zip file
cmd := workflow.ExecGH("api", "repos/{owner}/{repo}/actions/runs/"+strconv.FormatInt(runID, 10)+"/logs")
output, err := cmd.Output()
output, err := workflow.RunGH("Downloading workflow logs...", "api", "repos/{owner}/{repo}/actions/runs/"+strconv.FormatInt(runID, 10)+"/logs")
if err != nil {
// Check for authentication errors
if strings.Contains(err.Error(), "exit status 4") {
Expand Down
6 changes: 2 additions & 4 deletions pkg/cli/logs_github_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ func fetchJobStatuses(runID int64, verbose bool) (int, error) {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching job statuses for run %d", runID)))
}

cmd := workflow.ExecGH("api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: .conclusion}")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching job statuses...", "api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: .conclusion}")
if err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Failed to fetch job statuses for run %d: %v", runID, err)))
Expand Down Expand Up @@ -79,8 +78,7 @@ func fetchJobDetails(runID int64, verbose bool) ([]JobInfoWithDuration, error) {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching job details for run %d", runID)))
}

cmd := workflow.ExecGH("api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: .conclusion, started_at: .started_at, completed_at: .completed_at}")
output, err := cmd.CombinedOutput()
output, err := workflow.RunGHCombined("Fetching job details...", "api", fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), "--jq", ".jobs[] | {name: .name, status: .status, conclusion: .conclusion, started_at: .started_at, completed_at: .completed_at}")
if err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Failed to fetch job details for run %d: %v", runID, err)))
Expand Down
12 changes: 4 additions & 8 deletions pkg/cli/pr_automerge.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ func AutoMergePullRequestsCreatedAfter(repoSlug string, createdAfter time.Time,
}

// List open PRs with creation time information
listCmd := workflow.ExecGH("pr", "list", "--repo", repoSlug, "--json", "number,title,isDraft,mergeable,createdAt,updatedAt")
output, err := listCmd.Output()
output, err := workflow.RunGH("Listing pull requests...", "pr", "list", "--repo", repoSlug, "--json", "number,title,isDraft,mergeable,createdAt,updatedAt")
if err != nil {
prAutomergeLog.Printf("Failed to list pull requests: %v", err)
return fmt.Errorf("failed to list pull requests: %w", err)
Expand Down Expand Up @@ -83,8 +82,7 @@ func AutoMergePullRequestsCreatedAfter(repoSlug string, createdAfter time.Time,
// Convert from draft to non-draft if necessary
if pr.IsDraft {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Converting PR #%d from draft to ready for review", pr.Number)))
readyCmd := workflow.ExecGH("pr", "ready", fmt.Sprintf("%d", pr.Number), "--repo", repoSlug)
if output, err := readyCmd.CombinedOutput(); err != nil {
if output, err := workflow.RunGHCombined("Converting draft to ready...", "pr", "ready", fmt.Sprintf("%d", pr.Number), "--repo", repoSlug); err != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to convert PR #%d from draft: %v (output: %s)", pr.Number, err, string(output))))
continue
}
Expand All @@ -98,8 +96,7 @@ func AutoMergePullRequestsCreatedAfter(repoSlug string, createdAfter time.Time,

// Auto-merge the PR
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Auto-merging PR #%d", pr.Number)))
mergeCmd := workflow.ExecGH("pr", "merge", fmt.Sprintf("%d", pr.Number), "--repo", repoSlug, "--auto", "--squash")
if output, err := mergeCmd.CombinedOutput(); err != nil {
if output, err := workflow.RunGHCombined("Auto-merging pull request...", "pr", "merge", fmt.Sprintf("%d", pr.Number), "--repo", repoSlug, "--auto", "--squash"); err != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to auto-merge PR #%d: %v (output: %s)", pr.Number, err, string(output))))
continue
}
Expand Down Expand Up @@ -127,8 +124,7 @@ func WaitForWorkflowCompletion(repoSlug, runID string, timeoutMinutes int, verbo
Timeout: timeout,
PollFunc: func() (PollResult, error) {
// Check workflow status
cmd := workflow.ExecGH("run", "view", runID, "--repo", repoSlug, "--json", "status,conclusion")
output, err := cmd.Output()
output, err := workflow.RunGH("Checking workflow status...", "run", "view", runID, "--repo", repoSlug, "--json", "status,conclusion")

if err != nil {
return PollFailure, fmt.Errorf("failed to check workflow status: %w", err)
Expand Down
Loading
Loading