diff --git a/actions/setup-cli/install.sh b/actions/setup-cli/install.sh index c7a5ed2ffed..1c565c9e893 100755 --- a/actions/setup-cli/install.sh +++ b/actions/setup-cli/install.sh @@ -1,7 +1,7 @@ #!/bin/bash set +o histexpand -# Kept in sync with actions/setup-cli/install.sh — edit this file, then copy to that path. +# Script sync note: install-gh-aw.sh is canonical. actions/setup-cli/install.sh is copied from install-gh-aw.sh. # Script to download and install gh-aw binary for the current OS and architecture # Supports: Linux, macOS (Darwin), FreeBSD, Windows (Git Bash/MSYS/Cygwin) diff --git a/pkg/workflow/action_resolver.go b/pkg/workflow/action_resolver.go index 15c9ae02820..9c6a187e2eb 100644 --- a/pkg/workflow/action_resolver.go +++ b/pkg/workflow/action_resolver.go @@ -201,6 +201,7 @@ func (r *ActionResolver) resolveFromGitHub(ctx context.Context, repo, version st // Annotated tags have type "tag" and their SHA points to the tag object, // not the underlying commit. We must peel to get the commit SHA. cmd := ExecGHContext(callCtx, "api", apiPath, "--jq", "[.object.sha, .object.type] | @tsv") + ForceGHHostEnv(cmd, "github.com") output, err := cmd.Output() if err != nil { return "", fmt.Errorf("failed to resolve %s@%s: %w", repo, version, err) @@ -226,8 +227,8 @@ func (r *ActionResolver) resolveFromGitHub(ctx context.Context, repo, version st // caller context (ctx), not from callCtx, so we don't accidentally shrink // the budget for subsequent peels. peelCtx, peelCancel := context.WithTimeout(ctx, 30*time.Second) - defer peelCancel() cmd2 := ExecGHContext(peelCtx, "api", tagPath, "--jq", "[.object.sha, .object.type] | @tsv") + ForceGHHostEnv(cmd2, "github.com") output2, peelErr := cmd2.Output() peelCancel() if peelErr != nil { diff --git a/pkg/workflow/action_resolver_test.go b/pkg/workflow/action_resolver_test.go index 8f59f79d558..bd1ef33a2d6 100644 --- a/pkg/workflow/action_resolver_test.go +++ b/pkg/workflow/action_resolver_test.go @@ -4,6 +4,8 @@ package workflow import ( "context" + "os" + "slices" "strings" "testing" @@ -256,3 +258,101 @@ func TestActionResolverGetUsedCacheKeysReturnsCopy(t *testing.T) { t.Error("Expected resolver used cache keys to be immutable via returned map") } } + +// TestForceGHHostEnvWithPresetCmdEnv verifies the non-nil cmd.Env branch of +// ForceGHHostEnv: a stale GH_HOST in a pre-populated cmd.Env is replaced, +// other env entries are preserved, and there is exactly one GH_HOST entry. +func TestForceGHHostEnvWithPresetCmdEnv(t *testing.T) { + cmd := ExecGHContext(context.Background(), "api", "/test") + cmd.Env = []string{"GH_HOST=stale.ghe.com", "OTHER=value"} + + ForceGHHostEnv(cmd, "github.com") + + var ghHostEntries []string + preservedOther := false + for _, e := range cmd.Env { + if strings.HasPrefix(e, "GH_HOST=") { + ghHostEntries = append(ghHostEntries, e) + } + if e == "OTHER=value" { + preservedOther = true + } + } + if len(ghHostEntries) != 1 { + t.Errorf("expected exactly one GH_HOST entry, got: %v", ghHostEntries) + } else if ghHostEntries[0] != "GH_HOST=github.com" { + t.Errorf("expected GH_HOST=github.com, got %q", ghHostEntries[0]) + } + if !preservedOther { + t.Error("expected OTHER=value to be preserved in cmd.Env") + } +} + +// GH_HOST=github.com on the command environment regardless of the process-level +// GH_HOST setting, including when GH_HOST is unset, set to a GHE host, or already +// set to github.com. +func TestForceGHHostEnvSetsGitHubCom(t *testing.T) { + tests := []struct { + name string + ghHost string + unsetIt bool + }{ + { + name: "GH_HOST unset", + unsetIt: true, + }, + { + name: "GH_HOST set to GHE host", + ghHost: "myorg.ghe.com", + }, + { + name: "GH_HOST already set to github.com", + ghHost: "github.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.unsetIt { + // Truly unset GH_HOST and restore the original value (or re-unset) + // after the subtest so the env does not leak to subsequent tests. + original, wasSet := os.LookupEnv("GH_HOST") + if err := os.Unsetenv("GH_HOST"); err != nil { + t.Fatalf("failed to unset GH_HOST: %v", err) + } + t.Cleanup(func() { + if wasSet { + os.Setenv("GH_HOST", original) //nolint:errcheck + } else { + os.Unsetenv("GH_HOST") //nolint:errcheck + } + }) + } else { + t.Setenv("GH_HOST", tt.ghHost) + } + + cmd := ExecGHContext(context.Background(), "api", "/repos/actions/checkout/git/ref/tags/v4", "--jq", "[.object.sha, .object.type] | @tsv") + ForceGHHostEnv(cmd, "github.com") + + // The command env must contain exactly GH_HOST=github.com and not + // any other GH_HOST value. + if cmd.Env == nil { + t.Fatal("expected cmd.Env to be set after ForceGHHostEnv, got nil") + } + + found := slices.ContainsFunc(cmd.Env, func(e string) bool { + return e == "GH_HOST=github.com" + }) + if !found { + t.Errorf("expected GH_HOST=github.com in cmd.Env, got: %v", cmd.Env) + } + + // Verify that no other GH_HOST value is present (i.e. GHE host is not inherited). + for _, e := range cmd.Env { + if strings.HasPrefix(e, "GH_HOST=") && e != "GH_HOST=github.com" { + t.Errorf("unexpected GH_HOST entry in cmd.Env: %q", e) + } + } + }) + } +} diff --git a/pkg/workflow/github_cli.go b/pkg/workflow/github_cli.go index 2b377474e13..80f191e1a23 100644 --- a/pkg/workflow/github_cli.go +++ b/pkg/workflow/github_cli.go @@ -223,3 +223,24 @@ func SetGHHostEnv(cmd *exec.Cmd, host string) { cmd.Env = append(cmd.Env, "GH_HOST="+host) } } + +// ForceGHHostEnv forces GH_HOST= on the command's environment, overriding +// any GH_HOST already present in the process environment or cmd.Env. +// Unlike SetGHHostEnv, this always sets GH_HOST — including for "github.com" — +// so that a GHE host in the process environment cannot be inherited by the subprocess. +func ForceGHHostEnv(cmd *exec.Cmd, host string) { + if host == "" { + return + } + base := cmd.Env + if base == nil { + base = os.Environ() + } + filtered := make([]string, 0, len(base)+1) + for _, e := range base { + if !strings.HasPrefix(e, "GH_HOST=") { + filtered = append(filtered, e) + } + } + cmd.Env = append(filtered, "GH_HOST="+host) +} diff --git a/pkg/workflow/github_cli_wasm.go b/pkg/workflow/github_cli_wasm.go index a8b3e02708e..1ce876e20cc 100644 --- a/pkg/workflow/github_cli_wasm.go +++ b/pkg/workflow/github_cli_wasm.go @@ -42,3 +42,7 @@ func RunGHContext(ctx context.Context, spinnerMessage string, args ...string) ([ func RunGHCombined(spinnerMessage string, args ...string) ([]byte, error) { return nil, errors.New("gh CLI not available in Wasm") } + +func ForceGHHostEnv(cmd *exec.Cmd, host string) { + // no-op in Wasm: gh CLI subprocesses are not run +}