diff --git a/pkg/cli/audit.go b/pkg/cli/audit.go index 9d3d5f19472..8e685b31b55 100644 --- a/pkg/cli/audit.go +++ b/pkg/cli/audit.go @@ -580,14 +580,10 @@ func auditJobRun(runID int64, jobID int64, stepNumber int, owner, repo, hostname return fmt.Errorf("failed to create output directory: %w", err) } - // Fetch job logs using gh CLI + // Fetch job logs using gh CLI. + // Use GH_HOST env var instead of --hostname (which is only valid for gh api, not gh run view). args := []string{"run", "view"} - // Add hostname flag if specified (for GitHub Enterprise) - if hostname != "" && hostname != "github.com" { - args = append(args, "--hostname", hostname) - } - // Add repository flag if specified if owner != "" && repo != "" { args = append(args, "-R", fmt.Sprintf("%s/%s", owner, repo)) @@ -600,7 +596,9 @@ func auditJobRun(runID int64, jobID int64, stepNumber int, owner, repo, hostname fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Executing: gh "+strings.Join(args, " "))) } - output, err := workflow.RunGHCombined("Fetching job logs...", args...) + cmd := workflow.ExecGH(args...) + workflow.SetGHHostEnv(cmd, hostname) + output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to fetch job logs: %w\nOutput: %s", err, string(output)) } diff --git a/pkg/cli/pr_command.go b/pkg/cli/pr_command.go index de7bdb4d5a5..e6f1c24fc98 100644 --- a/pkg/cli/pr_command.go +++ b/pkg/cli/pr_command.go @@ -771,14 +771,9 @@ func createPR(branchName, title, body string, verbose bool) (int, string, error) // repositories are targeted correctly instead of defaulting to github.com. remoteHost := getHostFromOriginRemote() - // Build gh repo view args, adding --hostname for GHES instances. - repoViewArgs := []string{"repo", "view", "--json", "owner,name"} - if remoteHost != "github.com" { - repoViewArgs = append(repoViewArgs, "--hostname", remoteHost) - } - - // Get the current repository info to ensure PR is created in the correct repo - repoOutput, err := workflow.RunGH("Fetching repository info...", repoViewArgs...) + // Get the current repository info to ensure PR is created in the correct repo. + // Use GH_HOST env var instead of --hostname (which is only valid for gh api, not gh repo view). + repoOutput, err := workflow.RunGHWithHost("Fetching repository info...", remoteHost, "repo", "view", "--json", "owner,name") if err != nil { return 0, "", fmt.Errorf("failed to get current repository info: %w", err) } @@ -797,14 +792,10 @@ func createPR(branchName, title, body string, verbose bool) (int, string, error) repoSpec := fmt.Sprintf("%s/%s", repoInfo.Owner.Login, repoInfo.Name) // Build gh pr create args. Explicitly specifying --repo ensures the PR is created in the - // current repo (not an upstream fork). For GHES instances, --hostname routes the request - // to the correct GitHub Enterprise host instead of defaulting to github.com. + // current repo (not an upstream fork). Use GH_HOST env var instead of --hostname + // (which is only valid for gh api, not gh pr create). prCreateArgs := []string{"pr", "create", "--repo", repoSpec, "--title", title, "--body", body, "--head", branchName} - if remoteHost != "github.com" { - prCreateArgs = append(prCreateArgs, "--hostname", remoteHost) - } - - output, err := workflow.RunGH("Creating pull request...", prCreateArgs...) + output, err := workflow.RunGHWithHost("Creating pull request...", remoteHost, prCreateArgs...) if err != nil { // Try to get stderr for better error reporting var exitError *exec.ExitError diff --git a/pkg/workflow/github_cli.go b/pkg/workflow/github_cli.go index 51c315cc5af..88c629ba91d 100644 --- a/pkg/workflow/github_cli.go +++ b/pkg/workflow/github_cli.go @@ -184,3 +184,43 @@ func RunGHContext(ctx context.Context, spinnerMessage string, args ...string) ([ func RunGHCombined(spinnerMessage string, args ...string) ([]byte, error) { return runGHWithSpinner(spinnerMessage, true, args...) } + +// RunGHWithHost executes a gh CLI command with a spinner, targeting a specific GitHub host. +// For non-github.com hosts (GHES, Proxima/data residency), the GH_HOST environment variable +// is set on the command. This is necessary because most gh subcommands (repo, pr, run, etc.) +// do not accept a --hostname flag — only `gh api` does. +// +// Usage: +// +// output, err := RunGHWithHost("Fetching repo info...", "myorg.ghe.com", "repo", "view", "--json", "owner,name") +func RunGHWithHost(spinnerMessage string, host string, args ...string) ([]byte, error) { + cmd := ExecGH(args...) + SetGHHostEnv(cmd, host) + + if tty.IsStderrTerminal() { + spinner := console.NewSpinner(spinnerMessage) + spinner.Start() + output, err := cmd.Output() + err = enrichGHError(err) + spinner.Stop() + return output, err + } + + output, err := cmd.Output() + return output, enrichGHError(err) +} + +// SetGHHostEnv sets the GH_HOST environment variable on the command for non-github.com hosts. +// This is needed for GitHub Enterprise Server (GHES) and Proxima (data residency) instances +// because commands like `gh repo view`, `gh pr create`, and `gh run view` do not accept a +// --hostname flag (unlike `gh api` which does). +func SetGHHostEnv(cmd *exec.Cmd, host string) { + if host == "" || host == "github.com" { + return + } + if cmd.Env == nil { + cmd.Env = append(os.Environ(), "GH_HOST="+host) + } else { + cmd.Env = append(cmd.Env, "GH_HOST="+host) + } +} diff --git a/pkg/workflow/github_cli_test.go b/pkg/workflow/github_cli_test.go index 9e47015ee98..89574c3637a 100644 --- a/pkg/workflow/github_cli_test.go +++ b/pkg/workflow/github_cli_test.go @@ -417,3 +417,63 @@ func TestEnrichGHError(t *testing.T) { assert.Contains(t, enriched.Error(), "exit status 1", "enriched error should still contain original error") }) } + +func TestSetGHHostEnv(t *testing.T) { + tests := []struct { + name string + host string + expectSet bool + initialEnv []string + }{ + { + name: "github.com is a no-op", + host: "github.com", + expectSet: false, + }, + { + name: "empty host is a no-op", + host: "", + expectSet: false, + }, + { + name: "GHES host sets GH_HOST", + host: "myorg.ghe.com", + expectSet: true, + }, + { + name: "Proxima host sets GH_HOST", + host: "verizon.ghe.com", + expectSet: true, + }, + { + name: "appends to existing env", + host: "myorg.ghe.com", + expectSet: true, + initialEnv: []string{"FOO=bar"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("echo", "test") + if tt.initialEnv != nil { + cmd.Env = tt.initialEnv + } + + SetGHHostEnv(cmd, tt.host) + + if !tt.expectSet { + if tt.initialEnv == nil { + assert.Nil(t, cmd.Env, "Env should remain nil for %s", tt.host) + } + return + } + + require.NotNil(t, cmd.Env, "Env should be set for host %s", tt.host) + found := slices.ContainsFunc(cmd.Env, func(e string) bool { + return e == "GH_HOST="+tt.host + }) + assert.True(t, found, "GH_HOST=%s should be in cmd.Env", tt.host) + }) + } +}