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: 5 additions & 7 deletions pkg/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
}
Expand Down
21 changes: 6 additions & 15 deletions pkg/cli/pr_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions pkg/workflow/github_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
60 changes: 60 additions & 0 deletions pkg/workflow/github_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading