diff --git a/.changeset/patch-ghes-auto-detect-helpers.md b/.changeset/patch-ghes-auto-detect-helpers.md new file mode 100644 index 00000000000..f6d81e417ab --- /dev/null +++ b/.changeset/patch-ghes-auto-detect-helpers.md @@ -0,0 +1,16 @@ +--- +"gh-aw": patch +--- + +Add GHES auto-detection helper functions for wizard configuration. This lays the foundation for auto-configuring GHES-specific settings in `gh aw add-wizard`: + +- Add `isGHESInstance()` to detect GHES instances vs. public GitHub +- Add `getGHESAPIURL()` to get the GHES API URL for engine.api-target configuration +- Add `getGHESAllowedDomains()` to get GHES domains for network firewall configuration + +These functions detect GHES instances by parsing the git origin remote URL and can be used by the wizard to: +1. Auto-populate `engine.api-target` for Copilot on GHES +2. Auto-add GHES domains to `network.allowed` for firewall configuration +3. Enable proper gh CLI host configuration via `GH_HOST` environment variable + +Related to github/gh-aw#20875 and the GHES wizard auto-configuration requirements. diff --git a/pkg/cli/git.go b/pkg/cli/git.go index 0ca4c85b406..8b4f9fbf195 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -141,6 +141,48 @@ func getHostFromOriginRemote() string { return host } +// isGHESInstance returns true if the git remote origin points to a GitHub Enterprise Server instance +// (not github.com). This is used to enable GHES-specific behavior like auto-configuring api-target +// and network firewall domains. +func isGHESInstance() bool { + host := getHostFromOriginRemote() + isGHES := host != "github.com" + gitLog.Printf("GHES instance check: host=%s, isGHES=%v", host, isGHES) + return isGHES +} + +// getGHESAPIURL returns the API URL for a GHES instance (e.g., "https://ghes.example.com/api/v3") +// or an empty string if not a GHES instance. This is used to auto-populate engine.api-target. +func getGHESAPIURL() string { + host := getHostFromOriginRemote() + if host == "github.com" { + return "" + } + + // GHES API URL format: https:///api/v3 + apiURL := fmt.Sprintf("https://%s/api/v3", host) + gitLog.Printf("GHES API URL: %s", apiURL) + return apiURL +} + +// getGHESAllowedDomains returns a list of domains that should be added to the firewall +// allowed list for GHES instances. This includes the GHES hostname and api.. +func getGHESAllowedDomains() []string { + host := getHostFromOriginRemote() + if host == "github.com" { + return nil + } + + // For GHES, allow both the main host and api subdomain + domains := []string{ + host, + "api." + host, + } + + gitLog.Printf("GHES allowed domains: %v", domains) + return domains +} + // 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_ghes_test.go b/pkg/cli/git_ghes_test.go new file mode 100644 index 00000000000..04795ab76b8 --- /dev/null +++ b/pkg/cli/git_ghes_test.go @@ -0,0 +1,166 @@ +//go:build !integration + +package cli + +import ( + "os" + "os/exec" + "testing" + + "github.com/github/gh-aw/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsGHESInstance(t *testing.T) { + tests := []struct { + name string + remoteURL string + wantIsGHES bool + }{ + { + name: "public GitHub", + remoteURL: "https://github.com/org/repo.git", + wantIsGHES: false, + }, + { + name: "GHES instance", + remoteURL: "https://ghes.example.com/org/repo.git", + wantIsGHES: true, + }, + { + name: "GHES SSH format", + remoteURL: "git@ghes.example.com:org/repo.git", + wantIsGHES: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + originalDir, err := os.Getwd() + require.NoError(t, err, "Failed to get current directory") + defer func() { + _ = os.Chdir(originalDir) + }() + + require.NoError(t, os.Chdir(tmpDir), "Failed to change to temp directory") + + // Initialize git repo + require.NoError(t, exec.Command("git", "init").Run(), "Failed to init git repo") + exec.Command("git", "config", "user.name", "Test User").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + + // Set remote URL + require.NoError(t, exec.Command("git", "remote", "add", "origin", tt.remoteURL).Run(), "Failed to add remote") + defer func() { _ = exec.Command("git", "remote", "remove", "origin").Run() }() + + got := isGHESInstance() + assert.Equal(t, tt.wantIsGHES, got, "isGHESInstance() returned unexpected result") + }) + } +} + +func TestGetGHESAPIURL(t *testing.T) { + tests := []struct { + name string + remoteURL string + wantAPIURL string + }{ + { + name: "public GitHub returns empty", + remoteURL: "https://github.com/org/repo.git", + wantAPIURL: "", + }, + { + name: "GHES instance returns API URL", + remoteURL: "https://ghes.example.com/org/repo.git", + wantAPIURL: "https://ghes.example.com/api/v3", + }, + { + name: "GHES SSH format returns API URL", + remoteURL: "git@contoso.ghe.com:org/repo.git", + wantAPIURL: "https://contoso.ghe.com/api/v3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + originalDir, err := os.Getwd() + require.NoError(t, err, "Failed to get current directory") + defer func() { + _ = os.Chdir(originalDir) + }() + + require.NoError(t, os.Chdir(tmpDir), "Failed to change to temp directory") + + // Initialize git repo + require.NoError(t, exec.Command("git", "init").Run(), "Failed to init git repo") + exec.Command("git", "config", "user.name", "Test User").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + + // Set remote URL + require.NoError(t, exec.Command("git", "remote", "add", "origin", tt.remoteURL).Run(), "Failed to add remote") + defer func() { _ = exec.Command("git", "remote", "remove", "origin").Run() }() + + got := getGHESAPIURL() + assert.Equal(t, tt.wantAPIURL, got, "getGHESAPIURL() returned unexpected result") + }) + } +} + +func TestGetGHESAllowedDomains(t *testing.T) { + tests := []struct { + name string + remoteURL string + wantDomains []string + }{ + { + name: "public GitHub returns nil", + remoteURL: "https://github.com/org/repo.git", + wantDomains: nil, + }, + { + name: "GHES instance returns host and api subdomain", + remoteURL: "https://ghes.example.com/org/repo.git", + wantDomains: []string{ + "ghes.example.com", + "api.ghes.example.com", + }, + }, + { + name: "GHES SSH format returns domains", + remoteURL: "git@contoso-aw.ghe.com:org/repo.git", + wantDomains: []string{ + "contoso-aw.ghe.com", + "api.contoso-aw.ghe.com", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "test-*") + originalDir, err := os.Getwd() + require.NoError(t, err, "Failed to get current directory") + defer func() { + _ = os.Chdir(originalDir) + }() + + require.NoError(t, os.Chdir(tmpDir), "Failed to change to temp directory") + + // Initialize git repo + require.NoError(t, exec.Command("git", "init").Run(), "Failed to init git repo") + exec.Command("git", "config", "user.name", "Test User").Run() + exec.Command("git", "config", "user.email", "test@example.com").Run() + + // Set remote URL + require.NoError(t, exec.Command("git", "remote", "add", "origin", tt.remoteURL).Run(), "Failed to add remote") + defer func() { _ = exec.Command("git", "remote", "remove", "origin").Run() }() + + got := getGHESAllowedDomains() + assert.Equal(t, tt.wantDomains, got, "getGHESAllowedDomains() returned unexpected result") + }) + } +}