Skip to content

Commit 5455f2a

Browse files
authored
fix: pin GH_HOST=github.com for extension upgrade in GHE environments (#34752)
1 parent a8bc72c commit 5455f2a

5 files changed

Lines changed: 77 additions & 8 deletions

File tree

.changeset/patch-fix-ghe-extension-upgrade-host.md

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cli/update_check.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,10 @@ func checkForUpdates(noCheckUpdate bool, verbose bool) {
219219
func getLatestRelease(includePrereleases bool) (string, error) {
220220
updateCheckLog.Print("Querying GitHub API for latest release...")
221221

222-
// Create GitHub REST client using go-gh
223-
client, err := api.NewRESTClient(api.ClientOptions{})
222+
// Always target github.com explicitly: gh-aw is only published to github.com,
223+
// and users in mixed-host environments (e.g. a GHE active auth host) must
224+
// still reach the canonical registry to get the correct release metadata.
225+
client, err := api.NewRESTClient(api.ClientOptions{Host: "github.com"})
224226
if err != nil {
225227
return "", fmt.Errorf("failed to create GitHub client: %w", err)
226228
}

pkg/cli/update_extension_check.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ func upgradeExtensionIfOutdated(verbose bool, includePrereleases bool) (bool, st
117117
// version.
118118
if includePrereleases && !needsRenameWorkaround() {
119119
updateExtensionCheckLog.Printf("Prerelease upgrade on macOS: skipping gh extension upgrade (uses /releases/latest, ignores prereleases), using pin-based install for %s", latestVersion)
120-
removeCmd := exec.Command("gh", "extension", "remove", extensionRepo)
120+
removeCmd := ghCmdForExtension("extension", "remove", extensionRepo)
121121
removeCmd.Stdout = os.Stderr
122122
removeCmd.Stderr = os.Stderr
123123
if removeErr := removeCmd.Run(); removeErr != nil {
124124
updateExtensionCheckLog.Printf("Could not remove extension before pin-based install (continuing anyway): %v", removeErr)
125125
}
126-
pinCmd := exec.Command("gh", "extension", "install", extensionRepo, "--pin", latestVersion)
126+
pinCmd := ghCmdForExtension("extension", "install", extensionRepo, "--pin", latestVersion)
127127
pinCmd.Stdout = os.Stderr
128128
pinCmd.Stderr = os.Stderr
129129
if pinErr := pinCmd.Run(); pinErr != nil {
@@ -144,7 +144,7 @@ func upgradeExtensionIfOutdated(verbose bool, includePrereleases bool) (bool, st
144144
// rename+retry path succeeds and the user is not shown a confusing failure.
145145
var firstAttemptBuf bytes.Buffer
146146
firstAttemptOut := firstAttemptWriter(os.Stderr, &firstAttemptBuf)
147-
firstCmd := exec.Command("gh", extensionUpgradeArgs()...)
147+
firstCmd := ghCmdForExtension(extensionUpgradeArgs()...)
148148
firstCmd.Stdout = firstAttemptOut
149149
firstCmd.Stderr = firstAttemptOut
150150
firstErr := firstCmd.Run()
@@ -248,7 +248,7 @@ func upgradeExtensionIfOutdated(verbose bool, includePrereleases bool) (bool, st
248248
// been moved to the OS temp directory (above) so the remove step can always
249249
// succeed. In both cases we clear backupPath after a successful remove to
250250
// avoid a misleading restore attempt on subsequent failures.
251-
removeCmd := exec.Command("gh", "extension", "remove", extensionRepo)
251+
removeCmd := ghCmdForExtension("extension", "remove", extensionRepo)
252252
removeCmd.Stdout = os.Stderr
253253
removeCmd.Stderr = os.Stderr
254254
if removeErr := removeCmd.Run(); removeErr == nil {
@@ -258,7 +258,7 @@ func upgradeExtensionIfOutdated(verbose bool, includePrereleases bool) (bool, st
258258
updateExtensionCheckLog.Printf("Could not remove extension before reinstall (will attempt install anyway): %v", removeErr)
259259
}
260260

261-
retryCmd := exec.Command("gh", "extension", "install", extensionRepo, "--pin", latestVersion)
261+
retryCmd := ghCmdForExtension("extension", "install", extensionRepo, "--pin", latestVersion)
262262
retryCmd.Stdout = os.Stderr
263263
retryCmd.Stderr = os.Stderr
264264
if retryErr := retryCmd.Run(); retryErr != nil {
@@ -426,6 +426,26 @@ func extensionUpgradeArgs() []string {
426426
return []string{"extension", "upgrade", extensionRepo, "--force"}
427427
}
428428

429+
// ghCmdForExtension creates an exec.Cmd for a gh CLI invocation that must
430+
// target github.com. gh-aw is only published to github.com; in mixed-host
431+
// environments where GH_HOST points at a GHE instance, the default gh
432+
// commands would hit the wrong host and report "already up to date" or fail.
433+
// Pinning GH_HOST=github.com in the child process environment prevents that.
434+
func ghCmdForExtension(args ...string) *exec.Cmd {
435+
cmd := exec.Command("gh", args...)
436+
// Inherit the full environment so that PATH, HOME, etc. remain intact,
437+
// then override (or add) GH_HOST to ensure github.com is always used.
438+
env := make([]string, 0, len(os.Environ())+1)
439+
for _, e := range os.Environ() {
440+
if !strings.HasPrefix(e, "GH_HOST=") {
441+
env = append(env, e)
442+
}
443+
}
444+
env = append(env, "GH_HOST=github.com")
445+
cmd.Env = env
446+
return cmd
447+
}
448+
429449
func prereleaseChannelNotice(currentVersion, latestStable string, includePrereleases bool) []string {
430450
if includePrereleases || latestStable == "" || !isPrereleaseVersion(currentVersion) {
431451
return nil

pkg/cli/update_extension_check_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path/filepath"
1010
"runtime"
11+
"strings"
1112
"testing"
1213

1314
"github.com/stretchr/testify/assert"
@@ -341,3 +342,38 @@ func TestRenderReleaseVersion(t *testing.T) {
341342
assert.Equal(t, "v0.74.8", renderReleaseVersion("v0.74.8"))
342343
assert.Equal(t, "v0.75.3-beta.1 (pre-release)", renderReleaseVersion("v0.75.3-beta.1"))
343344
}
345+
346+
// TestGhCmdForExtension verifies that ghCmdForExtension always pins
347+
// GH_HOST=github.com so that GHE-authenticated environments do not
348+
// redirect extension upgrade/install/remove commands to the wrong host.
349+
func TestGhCmdForExtension(t *testing.T) {
350+
t.Run("sets GH_HOST to github.com", func(t *testing.T) {
351+
cmd := ghCmdForExtension("extension", "list")
352+
ghHost := ""
353+
for _, e := range cmd.Env {
354+
if v, ok := strings.CutPrefix(e, "GH_HOST="); ok {
355+
ghHost = v
356+
}
357+
}
358+
assert.Equal(t, "github.com", ghHost, "GH_HOST must be github.com")
359+
})
360+
361+
t.Run("overrides existing GH_HOST set to a GHE instance", func(t *testing.T) {
362+
t.Setenv("GH_HOST", "ghe.example.com")
363+
364+
cmd := ghCmdForExtension("extension", "upgrade", extensionRepo, "--force")
365+
ghHostValues := []string{}
366+
for _, e := range cmd.Env {
367+
if v, ok := strings.CutPrefix(e, "GH_HOST="); ok {
368+
ghHostValues = append(ghHostValues, v)
369+
}
370+
}
371+
require.Len(t, ghHostValues, 1, "exactly one GH_HOST entry must be present")
372+
assert.Equal(t, "github.com", ghHostValues[0], "GH_HOST must be overridden to github.com")
373+
})
374+
375+
t.Run("uses gh as executable", func(t *testing.T) {
376+
cmd := ghCmdForExtension("extension", "list")
377+
assert.Equal(t, "gh", filepath.Base(cmd.Path), "executable must be gh")
378+
})
379+
}

pkg/workflow/testdata/TestWasmGolden_AllEngines/codex.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ jobs:
495495
fi
496496
# shellcheck disable=SC1003
497497
sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \
498-
-- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex exec${GH_AW_MODEL_DETECTION_CODEX:+ --model "$GH_AW_MODEL_DETECTION_CODEX"} -c web_search="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
498+
-- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/codex_harness.cjs codex exec${GH_AW_MODEL_DETECTION_CODEX:+ --model "$GH_AW_MODEL_DETECTION_CODEX"} -c web_search="disabled" -c fetch="disabled" --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
499499
env:
500500
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}
501501
CODEX_HOME: /tmp/gh-aw/mcp-config

0 commit comments

Comments
 (0)