From 6f146e2a152209641b09ba5d0a72c066e0c0f4a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 01:49:13 +0000 Subject: [PATCH 1/3] Initial plan From 712215977fc5841b969101ffec209b69e08aba36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:12:26 +0000 Subject: [PATCH 2/3] fix: detect GHES host from git remote for PR creation (#issue) When adding a workflow with --create-pull-request in a repo whose origin points to GitHub Enterprise Server, detect the host from the git remote URL and pass --hostname to gh pr create, so the PR is created on the correct GHES instance instead of always defaulting to github.com. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .changeset/patch-fix-ghes-pr-creation.md | 5 + pkg/cli/git.go | 54 ++++++++++ pkg/cli/git_test.go | 127 +++++++++++++++++++++++ pkg/cli/pr_command.go | 23 +++- 4 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 .changeset/patch-fix-ghes-pr-creation.md diff --git a/.changeset/patch-fix-ghes-pr-creation.md b/.changeset/patch-fix-ghes-pr-creation.md new file mode 100644 index 00000000000..76d787a7916 --- /dev/null +++ b/.changeset/patch-fix-ghes-pr-creation.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fix `add-wizard` and `add --create-pull-request` failing to create pull requests in GitHub Enterprise Server repositories. The PR creation commands now detect the GitHub host from the git `origin` remote URL and pass `--hostname` to `gh pr create`, ensuring GHES repositories are targeted correctly instead of always defaulting to github.com. diff --git a/pkg/cli/git.go b/pkg/cli/git.go index 725c4d2aff8..a5a55af750c 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -87,6 +87,60 @@ func parseGitHubRepoSlugFromURL(url string) string { return "" } +// extractHostFromRemoteURL extracts the hostname from a git remote URL. +// Supports HTTPS (https://host/path), HTTP (http://host/path), and SSH (git@host:path or ssh://git@host/path) formats. +// Returns "github.com" as the default if the URL cannot be parsed. +func extractHostFromRemoteURL(remoteURL string) string { + // HTTPS / HTTP format: https://host/path or http://host/path + for _, scheme := range []string{"https://", "http://"} { + if after, ok := strings.CutPrefix(remoteURL, scheme); ok { + if host, _, found := strings.Cut(after, "/"); found { + return host + } + return after + } + } + + // SSH scp-like format: git@host:path + if after, ok := strings.CutPrefix(remoteURL, "git@"); ok { + if host, _, found := strings.Cut(after, ":"); found { + return host + } + } + + // SSH URL format: ssh://git@host/path or ssh://host/path + if after, ok := strings.CutPrefix(remoteURL, "ssh://"); ok { + // Strip optional user info (e.g. "git@") + if _, userStripped, hasAt := strings.Cut(after, "@"); hasAt { + after = userStripped + } + if host, _, found := strings.Cut(after, "/"); found { + return host + } + return after + } + + return "github.com" +} + +// getHostFromOriginRemote returns the hostname of the git origin remote. +// For example, a remote URL of "https://ghes.example.com/org/repo.git" returns "ghes.example.com", +// and "git@github.com:owner/repo.git" returns "github.com". +// Returns "github.com" as the default if the remote URL cannot be determined. +func getHostFromOriginRemote() string { + cmd := exec.Command("git", "config", "--get", "remote.origin.url") + output, err := cmd.Output() + if err != nil { + gitLog.Printf("Failed to get remote origin URL: %v", err) + return "github.com" + } + + remoteURL := strings.TrimSpace(string(output)) + host := extractHostFromRemoteURL(remoteURL) + gitLog.Printf("Detected GitHub host from remote origin: %s", host) + return host +} + // getRepositorySlugFromRemote extracts the repository slug (owner/repo) from git remote URL func getRepositorySlugFromRemote() string { gitLog.Print("Getting repository slug from git remote") diff --git a/pkg/cli/git_test.go b/pkg/cli/git_test.go index 1158e8524e0..4a05cc9b728 100644 --- a/pkg/cli/git_test.go +++ b/pkg/cli/git_test.go @@ -453,3 +453,130 @@ func TestCheckWorkflowFileStatusNotInRepo(t *testing.T) { t.Error("Expected empty status when not in git repository") } } + +func TestExtractHostFromRemoteURL(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + { + name: "public GitHub HTTPS", + url: "https://github.com/owner/repo.git", + expected: "github.com", + }, + { + name: "public GitHub SSH scp-like", + url: "git@github.com:owner/repo.git", + expected: "github.com", + }, + { + name: "GHES HTTPS", + url: "https://ghes.example.com/org/repo.git", + expected: "ghes.example.com", + }, + { + name: "GHES SSH scp-like", + url: "git@ghes.example.com:org/repo.git", + expected: "ghes.example.com", + }, + { + name: "GHES HTTPS without .git suffix", + url: "https://ghes.example.com/org/repo", + expected: "ghes.example.com", + }, + { + name: "SSH URL format with user", + url: "ssh://git@ghes.example.com/org/repo.git", + expected: "ghes.example.com", + }, + { + name: "SSH URL format without user", + url: "ssh://ghes.example.com/org/repo.git", + expected: "ghes.example.com", + }, + { + name: "HTTP URL", + url: "http://ghes.example.com/org/repo.git", + expected: "ghes.example.com", + }, + { + name: "empty URL defaults to github.com", + url: "", + expected: "github.com", + }, + { + name: "unrecognized URL defaults to github.com", + url: "not-a-url", + expected: "github.com", + }, + { + name: "GHES with port", + url: "https://ghes.example.com:8443/org/repo.git", + expected: "ghes.example.com:8443", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractHostFromRemoteURL(tt.url) + if got != tt.expected { + t.Errorf("extractHostFromRemoteURL(%q) = %q, want %q", tt.url, got, tt.expected) + } + }) + } +} + +func TestGetHostFromOriginRemote(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-get-host-*") + + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Logf("Warning: failed to restore directory: %v", err) + } + }() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Initialize a git repo + if err := exec.Command("git", "init").Run(); err != nil { + t.Fatalf("Failed to init git repo: %v", err) + } + + t.Run("no remote defaults to github.com", func(t *testing.T) { + got := getHostFromOriginRemote() + if got != "github.com" { + t.Errorf("getHostFromOriginRemote() without remote = %q, want %q", got, "github.com") + } + }) + + t.Run("public GitHub remote", func(t *testing.T) { + if err := exec.Command("git", "remote", "add", "origin", "https://github.com/owner/repo.git").Run(); err != nil { + t.Fatalf("Failed to add remote: %v", err) + } + defer func() { _ = exec.Command("git", "remote", "remove", "origin").Run() }() + + got := getHostFromOriginRemote() + if got != "github.com" { + t.Errorf("getHostFromOriginRemote() = %q, want %q", got, "github.com") + } + }) + + t.Run("GHES remote", func(t *testing.T) { + if err := exec.Command("git", "remote", "add", "origin", "https://ghes.example.com/org/repo.git").Run(); err != nil { + t.Fatalf("Failed to add remote: %v", err) + } + defer func() { _ = exec.Command("git", "remote", "remove", "origin").Run() }() + + got := getHostFromOriginRemote() + if got != "ghes.example.com" { + t.Errorf("getHostFromOriginRemote() = %q, want %q", got, "ghes.example.com") + } + }) +} diff --git a/pkg/cli/pr_command.go b/pkg/cli/pr_command.go index df2e33fb834..0488d617cfc 100644 --- a/pkg/cli/pr_command.go +++ b/pkg/cli/pr_command.go @@ -767,8 +767,18 @@ func createPR(branchName, title, body string, verbose bool) (int, string, error) fmt.Fprintln(os.Stderr, console.FormatProgressMessage("Creating PR: "+title)) } + // Detect the GitHub host from the git remote so that GitHub Enterprise Server + // 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...", "repo", "view", "--json", "owner,name") + repoOutput, err := workflow.RunGH("Fetching repository info...", repoViewArgs...) if err != nil { return 0, "", fmt.Errorf("failed to get current repository info: %w", err) } @@ -786,8 +796,15 @@ func createPR(branchName, title, body string, verbose bool) (int, string, error) repoSpec := fmt.Sprintf("%s/%s", repoInfo.Owner.Login, repoInfo.Name) - // Explicitly specify the repository to ensure PR is created in the current repo (not upstream) - output, err := workflow.RunGH("Creating pull request...", "pr", "create", "--repo", repoSpec, "--title", title, "--body", body, "--head", branchName) + // 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. + 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...) if err != nil { // Try to get stderr for better error reporting var exitError *exec.ExitError From 374217f76d16000bae1e8402f7c88b616e89ec67 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Fri, 13 Mar 2026 19:47:09 -0700 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkg/cli/git.go | 6 +++--- pkg/cli/git_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cli/git.go b/pkg/cli/git.go index a5a55af750c..0ca4c85b406 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -87,9 +87,9 @@ func parseGitHubRepoSlugFromURL(url string) string { return "" } -// extractHostFromRemoteURL extracts the hostname from a git remote URL. -// Supports HTTPS (https://host/path), HTTP (http://host/path), and SSH (git@host:path or ssh://git@host/path) formats. -// Returns "github.com" as the default if the URL cannot be parsed. +// extractHostFromRemoteURL extracts the host (optionally including port) from a git remote URL. +// Supports HTTPS (https://host[:port]/path), HTTP (http://host[:port]/path), and SSH (git@host[:port]:path or ssh://git@host[:port]/path) formats. +// Returns the host portion as "host[:port]" when parsed, or "github.com" as the default if the URL cannot be parsed. func extractHostFromRemoteURL(remoteURL string) string { // HTTPS / HTTP format: https://host/path or http://host/path for _, scheme := range []string{"https://", "http://"} { diff --git a/pkg/cli/git_test.go b/pkg/cli/git_test.go index 4a05cc9b728..73b42c4845a 100644 --- a/pkg/cli/git_test.go +++ b/pkg/cli/git_test.go @@ -546,7 +546,7 @@ func TestGetHostFromOriginRemote(t *testing.T) { // Initialize a git repo if err := exec.Command("git", "init").Run(); err != nil { - t.Fatalf("Failed to init git repo: %v", err) + t.Skip("Git not available") } t.Run("no remote defaults to github.com", func(t *testing.T) {