From 286af7d76cb4ed3ff462b2a4bf4b45792fdbdf93 Mon Sep 17 00:00:00 2001 From: Mara Nikola Kiefer Date: Fri, 17 Jul 2026 06:40:12 +0200 Subject: [PATCH 1/5] Enhance GitHub host detection for PAT creation and setup commands --- docs/src/content/docs/setup/cli.md | 2 + pkg/cli/cli_consistency_help_test.go | 1 + pkg/cli/doctor_command.go | 6 ++- pkg/cli/doctor_command_test.go | 77 ++++++++++++++++++++++++++++ pkg/cli/engine_secrets.go | 24 ++++++++- pkg/cli/engine_secrets_test.go | 70 ++++++++++++++++++++----- pkg/cli/setup_repository.go | 17 ++++++ 7 files changed, 182 insertions(+), 15 deletions(-) diff --git a/docs/src/content/docs/setup/cli.md b/docs/src/content/docs/setup/cli.md index a6c1bebe0d2..437720f05a5 100644 --- a/docs/src/content/docs/setup/cli.md +++ b/docs/src/content/docs/setup/cli.md @@ -271,6 +271,8 @@ See [Authentication](/gh-aw/reference/auth/) for details. Run diagnostics to verify CLI authentication and repository setup. +When running inside a GitHub Enterprise checkout and `GH_HOST` is unset, `doctor` auto-detects the host from the git remote. Outside a checkout, run `gh auth login --hostname ` or set `GH_HOST` explicitly. + ```bash wrap gh aw doctor gh aw doctor --json diff --git a/pkg/cli/cli_consistency_help_test.go b/pkg/cli/cli_consistency_help_test.go index 95fb3d6d4f2..13c5f7dd9dd 100644 --- a/pkg/cli/cli_consistency_help_test.go +++ b/pkg/cli/cli_consistency_help_test.go @@ -86,6 +86,7 @@ func TestCLIDocsReflectStatusAuditAndExperimentsCommands(t *testing.T) { assert.Contains(t, text, "**Options:** `--repo/-r`, `--dir/-d`, `--require-owner-type`, `--json/-j`", "doctor docs should include the --dir shorthand") assert.Contains(t, text, "`--require-owner-type` accepts `any`, `user`, or `org` and defaults to `any`", "doctor docs should document the full owner type set and default") assert.Contains(t, text, "`--dir` and `--require-owner-type` require `--repo`", "doctor docs should document the repo requirement for repository-only flags") + assert.Contains(t, text, "Outside a checkout, run `gh auth login --hostname ` or set `GH_HOST` explicitly.", "doctor docs should explain how to use enterprise hosts outside a checkout") } func TestSubcommandListingsUseHyphenBullets(t *testing.T) { diff --git a/pkg/cli/doctor_command.go b/pkg/cli/doctor_command.go index 24e0a52b114..f1dbed351f5 100644 --- a/pkg/cli/doctor_command.go +++ b/pkg/cli/doctor_command.go @@ -16,7 +16,11 @@ func NewDoctorCommand() *cobra.Command { Long: `Run diagnostics to verify CLI authentication and repository setup. Checks GitHub CLI authentication. When --repo is provided, also verifies the -repository exists, resolves the owner type, and inspects checkout state.`, +repository exists, resolves the owner type, and inspects checkout state. + +When running inside a GitHub Enterprise checkout and GH_HOST is unset, doctor +auto-detects the host from the git remote. Outside a checkout, authenticate with +gh auth login --hostname or set GH_HOST explicitly.`, Example: ` gh aw doctor gh aw doctor --json gh aw doctor --repo github/gh-aw diff --git a/pkg/cli/doctor_command_test.go b/pkg/cli/doctor_command_test.go index 3a4a8c1c040..af5d22ee9ca 100644 --- a/pkg/cli/doctor_command_test.go +++ b/pkg/cli/doctor_command_test.go @@ -3,9 +3,14 @@ package cli import ( + "context" "errors" + "os" + "path/filepath" "testing" + "github.com/github/gh-aw/pkg/testutil" + "github.com/github/gh-aw/pkg/workflow" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,6 +43,12 @@ func TestDoctorCommandAdvertisesJSONExample(t *testing.T) { assert.Contains(t, cmd.Example, "gh aw doctor --repo github/gh-aw --json") } +func TestDoctorCommandLongMentionsEnterpriseHostFallback(t *testing.T) { + cmd := NewDoctorCommand() + assert.Contains(t, cmd.Long, "auto-detects the host from the git remote") + assert.Contains(t, cmd.Long, "gh auth login --hostname ") +} + func TestDoctorCommandExampleHasNoTabs(t *testing.T) { cmd := NewDoctorCommand() assert.NotContains(t, cmd.Example, "\t") @@ -210,3 +221,69 @@ func TestDoctorCommandAllowsVerboseWithoutRepo(t *testing.T) { assert.True(t, authCalled) assert.True(t, verbose) } + +func TestRunSetupAuthAutoDetectsDefaultGHHost(t *testing.T) { + workflow.SetDefaultGHHost("") + t.Cleanup(func() { workflow.SetDefaultGHHost("") }) + if prev, ok := os.LookupEnv("GH_HOST"); ok { + t.Cleanup(func() { _ = os.Setenv("GH_HOST", prev) }) + } else { + t.Cleanup(func() { _ = os.Unsetenv("GH_HOST") }) + } + require.NoError(t, os.Unsetenv("GH_HOST")) + + tempDir := testutil.TempDir(t, "doctor-gh-host-*") + require.NoError(t, initTestGitRepo(tempDir)) + require.NoError(t, addOriginRemoteToTestRepo(tempDir, "https://ghes.example.com/owner/repo.git")) + t.Chdir(tempDir) + + runtime := setupRepositoryRuntime{ + checkAuth: func(context.Context) error { + assert.Equal(t, "ghes.example.com", getGHHostFromCommandEnv(workflow.ExecGH("auth", "status"))) + return nil + }, + } + + require.NoError(t, runSetupAuthWithRuntime(SetupAuthOptions{}, runtime)) +} + +func TestRunSetupRepositoryCheckAutoDetectsDefaultGHHost(t *testing.T) { + workflow.SetDefaultGHHost("") + t.Cleanup(func() { workflow.SetDefaultGHHost("") }) + if prev, ok := os.LookupEnv("GH_HOST"); ok { + t.Cleanup(func() { _ = os.Setenv("GH_HOST", prev) }) + } else { + t.Cleanup(func() { _ = os.Unsetenv("GH_HOST") }) + } + require.NoError(t, os.Unsetenv("GH_HOST")) + + tempDir := testutil.TempDir(t, "doctor-repo-gh-host-*") + require.NoError(t, initTestGitRepo(tempDir)) + require.NoError(t, addOriginRemoteToTestRepo(tempDir, "git@ghes.example.com:owner/repo.git")) + t.Chdir(tempDir) + + checkoutDir := filepath.Join(tempDir, "checkout") + require.NoError(t, os.MkdirAll(checkoutDir, 0755)) + + runtime := setupRepositoryRuntime{ + checkAuth: func(context.Context) error { + assert.Equal(t, "ghes.example.com", getGHHostFromCommandEnv(workflow.ExecGH("auth", "status"))) + return nil + }, + repoExists: func(context.Context, string) (bool, error) { return true, nil }, + ownerType: func(context.Context, string) (string, error) { return "Organization", nil }, + dirOriginRepo: func(string) (string, error) { + return "owner/repo", nil + }, + checkCleanWorktree: func(bool) error { return nil }, + } + + err := runSetupRepositoryCheckWithRuntime(SetupRepositoryCheckOptions{ + Repo: "owner/repo", + Dir: checkoutDir, + RequireOwnerType: "any", + }, runtime) + require.Error(t, err) + assert.Contains(t, err.Error(), "git checkout") + assert.Equal(t, "ghes.example.com", getGHHostFromCommandEnv(workflow.ExecGH("auth", "status"))) +} diff --git a/pkg/cli/engine_secrets.go b/pkg/cli/engine_secrets.go index 3c9b3eebc4c..f1e30c18aad 100644 --- a/pkg/cli/engine_secrets.go +++ b/pkg/cli/engine_secrets.go @@ -398,10 +398,29 @@ func promptForCopilotPATUnified(req SecretRequirement, config EngineSecretConfig } func buildCopilotPATCreationURL() string { - const baseURL = "https://github.com/settings/personal-access-tokens/new" values := url.Values{} values.Set("name", constants.CopilotGitHubToken) values.Set("user_copilot_requests", "read") + return buildPATCreationURL(values) +} + +func buildGenericPATCreationURL() string { + return buildPATCreationURL(nil) +} + +func buildPATCreationURL(values url.Values) string { + hostURL := getGitHubHost() + if hostURL == string(constants.PublicGitHubHost) { + if detectedHost := getHostFromOriginRemote(); detectedHost != "" && detectedHost != "github.com" { + hostURL = stringutil.NormalizeGitHubHostURL(detectedHost) + } + } + + baseURL := strings.TrimRight(hostURL, "/") + "/settings/personal-access-tokens/new" + if len(values) == 0 { + return baseURL + } + return baseURL + "?" + values.Encode() } @@ -409,6 +428,7 @@ func buildCopilotPATCreationURL() string { // This uses PAT-specific wording instead of "API key" since system secrets are GitHub tokens func promptForSystemTokenUnified(req SecretRequirement, config EngineSecretConfig) error { engineSecretsLog.Printf("Prompting for system token: %s", req.Name) + patURL := buildGenericPATCreationURL() fmt.Fprintln(os.Stderr, "") fmt.Fprintf(os.Stderr, "%s requires a GitHub Personal Access Token (PAT).\n", req.Name) @@ -417,7 +437,7 @@ func promptForSystemTokenUnified(req SecretRequirement, config EngineSecretConfi fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Recommended scopes: "+req.Description)) fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Create a token at:") - fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" https://github.com/settings/personal-access-tokens/new")) + fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" "+patURL)) fmt.Fprintln(os.Stderr, "") var token string diff --git a/pkg/cli/engine_secrets_test.go b/pkg/cli/engine_secrets_test.go index 8e7f5ad1c01..62251b6ced4 100644 --- a/pkg/cli/engine_secrets_test.go +++ b/pkg/cli/engine_secrets_test.go @@ -5,6 +5,7 @@ package cli import ( "net/url" "os" + "os/exec" "testing" "github.com/stretchr/testify/assert" @@ -12,6 +13,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/setutil" + "github.com/github/gh-aw/pkg/testutil" ) func TestGetRequiredSecretsForEngine(t *testing.T) { @@ -147,18 +149,62 @@ func TestGetRequiredSecretsForEngineAttributes(t *testing.T) { } func TestBuildCopilotPATCreationURL(t *testing.T) { - rawURL := buildCopilotPATCreationURL() - parsed, err := url.Parse(rawURL) - require.NoError(t, err) - - assert.Equal(t, "https", parsed.Scheme) - assert.Equal(t, "github.com", parsed.Host) - assert.Equal(t, "/settings/personal-access-tokens/new", parsed.Path) - assert.Equal(t, constants.CopilotGitHubToken, parsed.Query().Get("name"), "token name must be prefilled to COPILOT_GITHUB_TOKEN") - assert.Equal(t, "read", parsed.Query().Get("user_copilot_requests")) - assert.Empty(t, parsed.Query().Get("contents"), "Copilot PAT setup URL should not request unrelated repository permissions") - assert.Empty(t, parsed.Query().Get("issues"), "Copilot PAT setup URL should not request unrelated issue permissions") - assert.Empty(t, parsed.Query().Get("pull_requests"), "Copilot PAT setup URL should not request unrelated pull request permissions") + t.Run("defaults to public github", func(t *testing.T) { + rawURL := buildCopilotPATCreationURL() + parsed, err := url.Parse(rawURL) + require.NoError(t, err) + + assert.Equal(t, "https", parsed.Scheme) + assert.Equal(t, "github.com", parsed.Host) + assert.Equal(t, "/settings/personal-access-tokens/new", parsed.Path) + assert.Equal(t, constants.CopilotGitHubToken, parsed.Query().Get("name"), "token name must be prefilled to COPILOT_GITHUB_TOKEN") + assert.Equal(t, "read", parsed.Query().Get("user_copilot_requests")) + assert.Empty(t, parsed.Query().Get("contents"), "Copilot PAT setup URL should not request unrelated repository permissions") + assert.Empty(t, parsed.Query().Get("issues"), "Copilot PAT setup URL should not request unrelated issue permissions") + assert.Empty(t, parsed.Query().Get("pull_requests"), "Copilot PAT setup URL should not request unrelated pull request permissions") + }) + + t.Run("uses GH_HOST when gh auth is configured for enterprise", func(t *testing.T) { + t.Setenv("GH_HOST", "ghe.example.com") + + rawURL := buildCopilotPATCreationURL() + parsed, err := url.Parse(rawURL) + require.NoError(t, err) + + assert.Equal(t, "https", parsed.Scheme) + assert.Equal(t, "ghe.example.com", parsed.Host) + assert.Equal(t, "/settings/personal-access-tokens/new", parsed.Path) + }) + + t.Run("falls back to origin remote host when GH_HOST is unset", func(t *testing.T) { + t.Setenv("GH_HOST", "") + t.Setenv("GITHUB_HOST", "") + t.Setenv("GITHUB_ENTERPRISE_HOST", "") + t.Setenv("GITHUB_SERVER_URL", "") + + tmpDir := testutil.TempDir(t, "copilot-pat-url-host-*") + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Logf("Warning: failed to restore directory: %v", err) + } + }() + + require.NoError(t, os.Chdir(tmpDir)) + if err := exec.Command("git", "init").Run(); err != nil { + t.Skip("Git not available") + } + require.NoError(t, exec.Command("git", "remote", "add", "origin", "https://ghes.example.com/org/repo.git").Run()) + + rawURL := buildCopilotPATCreationURL() + parsed, err := url.Parse(rawURL) + require.NoError(t, err) + + assert.Equal(t, "https", parsed.Scheme) + assert.Equal(t, "ghes.example.com", parsed.Host) + assert.Equal(t, "/settings/personal-access-tokens/new", parsed.Path) + }) } func TestEnsureSecretAvailable_CopilotRepromptsWithOverwrite(t *testing.T) { diff --git a/pkg/cli/setup_repository.go b/pkg/cli/setup_repository.go index 2ce26728943..bdda33189c4 100644 --- a/pkg/cli/setup_repository.go +++ b/pkg/cli/setup_repository.go @@ -17,6 +17,19 @@ import ( var setupRepositoryLog = logger.New("cli:setup_repository") +func configureDefaultGHHostFromOriginRemoteIfUnset() { + if os.Getenv("GH_HOST") != "" { //nolint:osgetenvlibrary + return + } + + if detectedHost := getHostFromOriginRemote(); detectedHost != "" && detectedHost != "github.com" { + setupRepositoryLog.Printf("Auto-detected GHES host from git remote: %s", detectedHost) + workflow.SetDefaultGHHost(detectedHost) + } else if detectedHost == "github.com" { + workflow.SetDefaultGHHost("") + } +} + type SetupAuthOptions struct { Ctx context.Context JSON bool @@ -299,6 +312,8 @@ func runSetupAuthWithRuntime(opts SetupAuthOptions, runtime setupRepositoryRunti ctx = context.Background() } + configureDefaultGHHostFromOriginRemoteIfUnset() + if err := runtime.checkAuth(ctx); err != nil { return fmt.Errorf("failed to verify GitHub CLI authentication: %w", err) } @@ -353,6 +368,8 @@ func runSetupRepositoryCheckWithRuntime(opts SetupRepositoryCheckOptions, runtim ctx = context.Background() } + configureDefaultGHHostFromOriginRemoteIfUnset() + if err := runtime.checkAuth(ctx); err != nil { return fmt.Errorf("failed to verify GitHub CLI authentication: %w", err) } From c0ed882f6d2b44cbee800899a9f05262be928908 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:06:46 +0000 Subject: [PATCH 2/5] docs(adr): add draft ADR-46128 for auto-detecting GHES host from git remote --- ...8-auto-detect-ghes-host-from-git-remote.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/adr/46128-auto-detect-ghes-host-from-git-remote.md diff --git a/docs/adr/46128-auto-detect-ghes-host-from-git-remote.md b/docs/adr/46128-auto-detect-ghes-host-from-git-remote.md new file mode 100644 index 00000000000..cb5e104f6ef --- /dev/null +++ b/docs/adr/46128-auto-detect-ghes-host-from-git-remote.md @@ -0,0 +1,45 @@ +# ADR-46128: Auto-Detect GitHub Enterprise Host from Git Remote When GH_HOST Is Unset + +**Date**: 2026-07-17 +**Status**: Draft +**Deciders**: mnkiefer + +--- + +### Context + +`gh-aw` commands that create PATs or verify authentication (e.g., `doctor`, `setup auth`, `setup repository`) previously hardcoded `github.com` as the target host unless the user explicitly set `GH_HOST`. Users operating in GitHub Enterprise Server (GHES) checkouts who have not set `GH_HOST` would receive PAT creation URLs and auth guidance pointing to the wrong host, causing silent failures or confusing redirects. A reliable, zero-configuration signal for the GHES host already exists in the local git repository's `origin` remote URL. + +### Decision + +We will implement implicit GHES host detection by parsing the `origin` remote URL at command runtime. When `GH_HOST` is unset and the detected host is not `github.com`, the tool will call `workflow.SetDefaultGHHost(detectedHost)` to configure the session host before executing auth checks or constructing PAT creation URLs. PAT URL construction is refactored into a shared `buildPATCreationURL` helper that applies this detection for both Copilot and generic PAT flows. + +### Alternatives Considered + +#### Alternative 1: Require Explicit GH_HOST Configuration + +Users must set `GH_HOST` (or `GITHUB_HOST` / `GITHUB_ENTERPRISE_HOST` / `GITHUB_SERVER_URL`) before using GHES-targeting commands. The tool documents this requirement and returns a clear error when the host cannot be determined. This approach is simpler and deterministic but imposes manual setup friction on every GHES user and breaks the zero-configuration experience the tool aims for. + +#### Alternative 2: Env-Var-Only Expansion Without Git Remote Inspection + +Expand host detection to check a prioritized list of environment variables (`GITHUB_SERVER_URL`, `GITHUB_ENTERPRISE_HOST`, `GITHUB_HOST`) in addition to `GH_HOST`, without inspecting the git remote. This avoids any filesystem I/O and is safe in all working-directory contexts but fails when none of these variables are set — which is the common case for developers running locally outside CI/CD pipelines. + +### Consequences + +#### Positive +- GHES users in a checkout receive correct PAT creation URLs and auth commands without manual host configuration. +- The `buildPATCreationURL` helper consolidates host-aware URL construction, eliminating the hardcoded `github.com` string in both Copilot PAT and system PAT flows. +- New integration-style tests verify host detection end-to-end by creating real temporary git repos with remote URLs. + +#### Negative +- Auto-detection couples command behavior to the current working directory's git state; commands run outside a git repo or in a repo with a non-GitHub remote will silently fall back to `github.com` rather than surfacing a clear error. +- Git remote inspection adds I/O at command startup for `setup auth` and `setup repository check`, which could slow down commands in environments with sluggish filesystem access. +- If the `origin` remote points to a mirror or proxy (not the canonical GHES host), the detected host will be wrong, and the error may not be obvious to the user. + +#### Neutral +- The `doctor` command long description is updated to document the auto-detection behavior and the manual fallback (`gh auth login --hostname ` or `GH_HOST`), keeping user-facing guidance in sync with the implementation. +- The change is scoped to host detection at command entry; downstream `gh` invocations already use the configured default host once `workflow.SetDefaultGHHost` is called. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From e99fb6220657eb942f38d16b244a340605729ccc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:21:19 +0000 Subject: [PATCH 3/5] chore: initial plan for review comment fixes Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .github/workflows/release.lock.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 9840a38941a..b670dfbb32e 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,6 @@ # gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2d6e82aec6fea9061e1ef1cbb085324e6ed6c079230d70bbbb96611bb18cf7cf","body_hash":"646353d7bb4e5523bc85349c2cce38188190095a303f83cc95961ff145a47043","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]}# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"53b7df96c91f9c12dcc8a07bcb9ccacbed38856a","version":"v7.3.0"},{"repo":"docker/login-action","sha":"af1e73f918a031802d376d3c8bbc3fe56130a9b0","version":"v4.4.0"},{"repo":"docker/metadata-action","sha":"dc802804100637a589fabce1cb79ff13a1411302","version":"v6.2.0"},{"repo":"docker/setup-buildx-action","sha":"bb05f3f5519dd87d3ba754cc423b652a5edd6d2c","version":"v4.2.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35","digest":"sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.35@sha256:2202f63e8650b2b8b0d38033b44a05387b2b71ad3e690c4d23a34786f5462aed"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35","digest":"sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.35@sha256:755b79d0dfda82bd6b43a208d68666721e504110c5d342a4eeb199802644ff04"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35","digest":"sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.35@sha256:f69282ec7b1326ba53891c399cf5b10475c0d3ccf4e1519b33d234a5427b57d3"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} +# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) From 63e1b4f0d2bd8d8d390400511977a00562c72200 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:36:11 +0000 Subject: [PATCH 4/5] fix: address review feedback on GHES host detection - Only use git remote fallback in buildPATCreationURL when no host env vars (GITHUB_SERVER_URL, GITHUB_ENTERPRISE_HOST, GITHUB_HOST, GH_HOST) are explicitly set, preventing a silent override of an explicit GH_HOST=github.com selection - Call configureDefaultGHHostFromOriginRemoteIfUnset() at the start of runTokensBootstrap so that secret discovery and upload both target the correct GHES host when running in a GHES checkout - Fix 'defaults to public github' test to clear all host env vars and run outside a git checkout to isolate from process environment - Fix 'uses GH_HOST for enterprise' test to clear higher-priority env vars (GITHUB_SERVER_URL etc.) so GH_HOST is actually honoured - Update doctor command guidance and docs to require both authentication (gh auth login --hostname) and GH_HOST for repository diagnostics outside a checkout - Update cli_consistency_help_test.go assertion to match corrected guidance text Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- docs/src/content/docs/setup/cli.md | 2 +- pkg/cli/cli_consistency_help_test.go | 2 +- pkg/cli/doctor_command.go | 3 ++- pkg/cli/engine_secrets.go | 18 +++++++++++++++++- pkg/cli/engine_secrets_test.go | 25 +++++++++++++++++++++++++ pkg/cli/tokens_bootstrap.go | 5 +++++ 6 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/setup/cli.md b/docs/src/content/docs/setup/cli.md index 437720f05a5..daae5c36aff 100644 --- a/docs/src/content/docs/setup/cli.md +++ b/docs/src/content/docs/setup/cli.md @@ -271,7 +271,7 @@ See [Authentication](/gh-aw/reference/auth/) for details. Run diagnostics to verify CLI authentication and repository setup. -When running inside a GitHub Enterprise checkout and `GH_HOST` is unset, `doctor` auto-detects the host from the git remote. Outside a checkout, run `gh auth login --hostname ` or set `GH_HOST` explicitly. +When running inside a GitHub Enterprise checkout and `GH_HOST` is unset, `doctor` auto-detects the host from the git remote. Outside a checkout, run `gh auth login --hostname ` to authenticate and set `GH_HOST=` so repository diagnostics target the correct host. ```bash wrap gh aw doctor diff --git a/pkg/cli/cli_consistency_help_test.go b/pkg/cli/cli_consistency_help_test.go index 13c5f7dd9dd..8fdf8759d39 100644 --- a/pkg/cli/cli_consistency_help_test.go +++ b/pkg/cli/cli_consistency_help_test.go @@ -86,7 +86,7 @@ func TestCLIDocsReflectStatusAuditAndExperimentsCommands(t *testing.T) { assert.Contains(t, text, "**Options:** `--repo/-r`, `--dir/-d`, `--require-owner-type`, `--json/-j`", "doctor docs should include the --dir shorthand") assert.Contains(t, text, "`--require-owner-type` accepts `any`, `user`, or `org` and defaults to `any`", "doctor docs should document the full owner type set and default") assert.Contains(t, text, "`--dir` and `--require-owner-type` require `--repo`", "doctor docs should document the repo requirement for repository-only flags") - assert.Contains(t, text, "Outside a checkout, run `gh auth login --hostname ` or set `GH_HOST` explicitly.", "doctor docs should explain how to use enterprise hosts outside a checkout") + assert.Contains(t, text, "Outside a checkout, run `gh auth login --hostname ` to authenticate and set `GH_HOST=` so repository diagnostics target the correct host.", "doctor docs should explain that enterprise hosts outside a checkout require both authentication and host selection") } func TestSubcommandListingsUseHyphenBullets(t *testing.T) { diff --git a/pkg/cli/doctor_command.go b/pkg/cli/doctor_command.go index f1dbed351f5..b3a03b23545 100644 --- a/pkg/cli/doctor_command.go +++ b/pkg/cli/doctor_command.go @@ -20,7 +20,8 @@ repository exists, resolves the owner type, and inspects checkout state. When running inside a GitHub Enterprise checkout and GH_HOST is unset, doctor auto-detects the host from the git remote. Outside a checkout, authenticate with -gh auth login --hostname or set GH_HOST explicitly.`, +gh auth login --hostname and set GH_HOST= so repository diagnostics +target the correct host.`, Example: ` gh aw doctor gh aw doctor --json gh aw doctor --repo github/gh-aw diff --git a/pkg/cli/engine_secrets.go b/pkg/cli/engine_secrets.go index f1e30c18aad..95d540673db 100644 --- a/pkg/cli/engine_secrets.go +++ b/pkg/cli/engine_secrets.go @@ -408,9 +408,25 @@ func buildGenericPATCreationURL() string { return buildPATCreationURL(nil) } +// isAnyGitHubHostEnvVarSet returns true when any of the environment variables +// consumed by getGitHubHost() is explicitly set. When at least one is present +// the caller has made an explicit host choice and the git-remote fallback should +// not be consulted. +func isAnyGitHubHostEnvVarSet() bool { + for _, envVar := range []string{"GITHUB_SERVER_URL", "GITHUB_ENTERPRISE_HOST", "GITHUB_HOST", "GH_HOST"} { + if os.Getenv(envVar) != "" { //nolint:osgetenvlibrary + return true + } + } + return false +} + func buildPATCreationURL(values url.Values) string { hostURL := getGitHubHost() - if hostURL == string(constants.PublicGitHubHost) { + // Only consult the git remote when the caller has not made an explicit host + // choice via an environment variable. Falling back when an env var selects + // public GitHub would silently override that explicit choice. + if !isAnyGitHubHostEnvVarSet() { if detectedHost := getHostFromOriginRemote(); detectedHost != "" && detectedHost != "github.com" { hostURL = stringutil.NormalizeGitHubHostURL(detectedHost) } diff --git a/pkg/cli/engine_secrets_test.go b/pkg/cli/engine_secrets_test.go index 62251b6ced4..715fc77942a 100644 --- a/pkg/cli/engine_secrets_test.go +++ b/pkg/cli/engine_secrets_test.go @@ -150,6 +150,26 @@ func TestGetRequiredSecretsForEngineAttributes(t *testing.T) { func TestBuildCopilotPATCreationURL(t *testing.T) { t.Run("defaults to public github", func(t *testing.T) { + // Clear all host env vars so the remote-detection fallback is exercised in + // isolation. Higher-priority variables (GITHUB_SERVER_URL, etc.) are set + // by GitHub Actions and would otherwise override the expected default. + t.Setenv("GITHUB_SERVER_URL", "") + t.Setenv("GITHUB_ENTERPRISE_HOST", "") + t.Setenv("GITHUB_HOST", "") + t.Setenv("GH_HOST", "") + + // Run outside any git checkout so that getHostFromOriginRemote() has no + // remote to detect, guaranteeing the github.com default is returned. + tmpDir := testutil.TempDir(t, "copilot-pat-url-default-*") + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Logf("Warning: failed to restore directory: %v", err) + } + }() + require.NoError(t, os.Chdir(tmpDir)) + rawURL := buildCopilotPATCreationURL() parsed, err := url.Parse(rawURL) require.NoError(t, err) @@ -165,6 +185,11 @@ func TestBuildCopilotPATCreationURL(t *testing.T) { }) t.Run("uses GH_HOST when gh auth is configured for enterprise", func(t *testing.T) { + // Clear higher-priority variables first so GH_HOST (the lowest-priority + // consumer in getGitHubHost) is actually honoured. + t.Setenv("GITHUB_SERVER_URL", "") + t.Setenv("GITHUB_ENTERPRISE_HOST", "") + t.Setenv("GITHUB_HOST", "") t.Setenv("GH_HOST", "ghe.example.com") rawURL := buildCopilotPATCreationURL() diff --git a/pkg/cli/tokens_bootstrap.go b/pkg/cli/tokens_bootstrap.go index dd8f43d1eb8..40a04513390 100644 --- a/pkg/cli/tokens_bootstrap.go +++ b/pkg/cli/tokens_bootstrap.go @@ -43,6 +43,11 @@ Only required secrets are prompted for. Optional secrets are not shown.`, func runTokensBootstrap(engine, repo string, nonInteractive bool) error { tokensBootstrapLog.Printf("Running tokens bootstrap: engine=%s, repo=%s, nonInteractive=%v", engine, repo, nonInteractive) + + // Configure the gh CLI host from the git remote before any gh calls so that + // secret discovery and upload both target the correct host. + configureDefaultGHHostFromOriginRemoteIfUnset() + var repoSlug string var err error From 2a221fd03b2b99742bc8d782974a65fa32a4f68f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:09:32 +0000 Subject: [PATCH 5/5] fix: replace os.Chdir with t.Chdir and exec git calls with test helpers - Replace manual os.Getwd + defer os.Chdir + os.Chdir(tmpDir) pattern with t.Chdir(tmpDir) in both subtests to avoid mutating the process working directory for the duration of the whole process and racing with other test goroutines - Replace exec.Command("git", "init") + exec.Command("git", "remote", "add", ...) with initTestGitRepo + addOriginRemoteToTestRepo helpers which create the .git structure directly without spawning git subprocesses (no timeout risk, no git hook interference) - Remove os/exec import that is no longer needed Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/engine_secrets_test.go | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/pkg/cli/engine_secrets_test.go b/pkg/cli/engine_secrets_test.go index 715fc77942a..81d6ea07ad9 100644 --- a/pkg/cli/engine_secrets_test.go +++ b/pkg/cli/engine_secrets_test.go @@ -5,7 +5,6 @@ package cli import ( "net/url" "os" - "os/exec" "testing" "github.com/stretchr/testify/assert" @@ -161,14 +160,7 @@ func TestBuildCopilotPATCreationURL(t *testing.T) { // Run outside any git checkout so that getHostFromOriginRemote() has no // remote to detect, guaranteeing the github.com default is returned. tmpDir := testutil.TempDir(t, "copilot-pat-url-default-*") - originalDir, err := os.Getwd() - require.NoError(t, err) - defer func() { - if err := os.Chdir(originalDir); err != nil { - t.Logf("Warning: failed to restore directory: %v", err) - } - }() - require.NoError(t, os.Chdir(tmpDir)) + t.Chdir(tmpDir) rawURL := buildCopilotPATCreationURL() parsed, err := url.Parse(rawURL) @@ -208,19 +200,9 @@ func TestBuildCopilotPATCreationURL(t *testing.T) { t.Setenv("GITHUB_SERVER_URL", "") tmpDir := testutil.TempDir(t, "copilot-pat-url-host-*") - originalDir, err := os.Getwd() - require.NoError(t, err) - defer func() { - if err := os.Chdir(originalDir); err != nil { - t.Logf("Warning: failed to restore directory: %v", err) - } - }() - - require.NoError(t, os.Chdir(tmpDir)) - if err := exec.Command("git", "init").Run(); err != nil { - t.Skip("Git not available") - } - require.NoError(t, exec.Command("git", "remote", "add", "origin", "https://ghes.example.com/org/repo.git").Run()) + require.NoError(t, initTestGitRepo(tmpDir)) + require.NoError(t, addOriginRemoteToTestRepo(tmpDir, "https://ghes.example.com/org/repo.git")) + t.Chdir(tmpDir) rawURL := buildCopilotPATCreationURL() parsed, err := url.Parse(rawURL)