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
12 changes: 8 additions & 4 deletions pkg/cli/update_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {

// Track updates
var updatedActions []string
var failedActions []string
var failedActions []actionUpdateFailure
var skippedActions []string

// Snapshot entries before iteration to avoid mutating the map mid-loop.
Expand All @@ -82,7 +82,7 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check %s: %v", entry.Repo, err)))
}
failedActions = append(failedActions, entry.Repo)
failedActions = append(failedActions, actionUpdateFailure{name: entry.Repo, err: err.Error()})
continue
}

Expand Down Expand Up @@ -139,8 +139,8 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {

if len(failedActions) > 0 {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check %d action(s):", len(failedActions))))
for _, action := range failedActions {
fmt.Fprintf(os.Stderr, " %s\n", action)
for _, f := range failedActions {
fmt.Fprintf(os.Stderr, " %s: %s\n", f.name, f.err)
}
fmt.Fprintln(os.Stderr, "")
}
Expand Down Expand Up @@ -182,6 +182,10 @@ func getLatestActionRelease(repo, currentVersion string, allowMajor, verbose boo
}
return latestRelease, latestSHA, nil
}
// Include the gh output in the error for better diagnostics
if trimmed := strings.TrimSpace(outputStr); trimmed != "" {
return "", "", fmt.Errorf("failed to fetch releases: %w: %s", err, trimmed)
}
return "", "", fmt.Errorf("failed to fetch releases: %w", err)
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/update_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ type updateFailure struct {
Name string
Error string
}

// actionUpdateFailure represents a failed GitHub Action update check
type actionUpdateFailure struct {
name string
err string
}
3 changes: 2 additions & 1 deletion pkg/cli/update_workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ func updateWorkflow(wf *workflowWithSource, allowMajor, force, verbose bool, eng
updateLog.Printf("Updating workflow: name=%s, source=%s, force=%v, noMerge=%v", wf.Name, wf.SourceSpec, force, noMerge)

if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("\nUpdating workflow: "+wf.Name))
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updating workflow: "+wf.Name))
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Source: "+wf.SourceSpec))
}

Expand Down
31 changes: 29 additions & 2 deletions pkg/workflow/github_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ package workflow

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
Expand Down Expand Up @@ -73,6 +76,26 @@ func ExecGHContext(ctx context.Context, args ...string) *exec.Cmd {
return setupGHCommand(ctx, args...)
}

// enrichGHError enriches an error returned from a gh CLI command with the
// stderr output captured in *exec.ExitError. When cmd.Output() (stdout-only
// capture) fails, Go populates ExitError.Stderr with the command's stderr,
// which typically contains the human-readable error message from gh.
// This function appends that message to the error so callers see useful
// diagnostics instead of a bare "exit status 1".
func enrichGHError(err error) error {
if err == nil {
return nil
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && len(exitErr.Stderr) > 0 {
stderr := strings.TrimSpace(string(exitErr.Stderr))
if stderr != "" {
return fmt.Errorf("%w: %s", err, stderr)
}
}
return err
}

// runGHWithSpinnerContext executes a gh CLI command with context support, a spinner,
// and returns the output. This is the core implementation for RunGHContext.
func runGHWithSpinnerContext(ctx context.Context, spinnerMessage string, combined bool, args ...string) ([]byte, error) {
Expand All @@ -88,6 +111,7 @@ func runGHWithSpinnerContext(ctx context.Context, spinnerMessage string, combine
output, err = cmd.CombinedOutput()
} else {
output, err = cmd.Output()
err = enrichGHError(err)
}
spinner.Stop()
return output, err
Expand All @@ -96,7 +120,8 @@ func runGHWithSpinnerContext(ctx context.Context, spinnerMessage string, combine
if combined {
return cmd.CombinedOutput()
}
return cmd.Output()
output, err := cmd.Output()
return output, enrichGHError(err)
}

// runGHWithSpinner executes a gh CLI command with a spinner and returns the output.
Expand All @@ -114,6 +139,7 @@ func runGHWithSpinner(spinnerMessage string, combined bool, args ...string) ([]b
output, err = cmd.CombinedOutput()
} else {
output, err = cmd.Output()
err = enrichGHError(err)
}
spinner.Stop()
return output, err
Expand All @@ -122,7 +148,8 @@ func runGHWithSpinner(spinnerMessage string, combined bool, args ...string) ([]b
if combined {
return cmd.CombinedOutput()
}
return cmd.Output()
output, err := cmd.Output()
return output, enrichGHError(err)
}

// RunGH executes a gh CLI command with a spinner and returns the stdout output.
Expand Down
33 changes: 33 additions & 0 deletions pkg/workflow/github_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package workflow

import (
"context"
"errors"
"os"
"os/exec"
"slices"
Expand Down Expand Up @@ -384,3 +385,35 @@ func TestRunGHWithSpinnerHelperExists(t *testing.T) {
})
}
}

// TestEnrichGHError tests that enrichGHError appends stderr from *exec.ExitError
func TestEnrichGHError(t *testing.T) {
t.Run("nil error unchanged", func(t *testing.T) {
assert.NoError(t, enrichGHError(nil), "nil error should remain nil")
})

t.Run("non-ExitError unchanged", func(t *testing.T) {
err := errors.New("plain error")
assert.Equal(t, err, enrichGHError(err), "non-ExitError should be returned unchanged")
})

t.Run("ExitError with no stderr unchanged", func(t *testing.T) {
// Run a command that exits non-zero without producing stderr
cmd := exec.Command("sh", "-c", "exit 1")
_, cmdErr := cmd.Output()
require.Error(t, cmdErr, "command should fail")
enriched := enrichGHError(cmdErr)
// With no stderr, the error should be equivalent to the original
assert.Equal(t, cmdErr.Error(), enriched.Error(), "ExitError with empty stderr should match original error message")
})

t.Run("ExitError with stderr gets stderr appended", func(t *testing.T) {
// Run a command that exits non-zero and writes to stderr
cmd := exec.Command("sh", "-c", "echo 'not found' >&2; exit 1")
_, cmdErr := cmd.Output()
require.Error(t, cmdErr, "command should fail")
enriched := enrichGHError(cmdErr)
assert.Contains(t, enriched.Error(), "not found", "enriched error should contain stderr output")
assert.Contains(t, enriched.Error(), "exit status 1", "enriched error should still contain original error")
})
}
Loading