Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion actions/setup-cli/install.sh

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pkg/workflow/action_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
100 changes: 100 additions & 0 deletions pkg/workflow/action_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package workflow

import (
"context"
"os"
"slices"
"strings"
"testing"

Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The cleanup for the "GH_HOST unset" case doesn't restore the original GH_HOST value if it was set in the environment before this test ran — the t.Cleanup just calls os.Unsetenv again instead of restoring what was there.

💡 Suggested fix using os.LookupEnv
} else {
    original, hadVal := 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 hadVal {
            os.Setenv("GH_HOST", original) (nolint/redacted):errcheck
        } else {
            os.Unsetenv("GH_HOST") (nolint/redacted):errcheck
        }
    })
}

This mirrors what t.Setenv does internally, ensuring the test doesn't permanently unset GH_HOST for other tests running in the same process (e.g. in CI where GH_HOST may be set globally).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the "GH_HOST unset" branch now saves the original value with os.LookupEnv and restores it (or re-unsets) in t.Cleanup, exactly as suggested.

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)
}
}
})
}
}
21 changes: 21 additions & 0 deletions pkg/workflow/github_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,24 @@ func SetGHHostEnv(cmd *exec.Cmd, host string) {
cmd.Env = append(cmd.Env, "GH_HOST="+host)
}
}

// ForceGHHostEnv forces GH_HOST=<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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] ForceGHHostEnv has two branches: cmd.Env == nil (hydrates from os.Environ()) and cmd.Env != nil (uses the existing slice). The new tests only exercise the nil-Env branch because ExecGHContext always returns a cmd with nil Env. The non-nil branch — including its dedup logic — has no test coverage.

💡 Suggested test

Add a case to the existing table (or a separate TestForceGHHostEnvWithPresetCmdEnv) that pre-populates cmd.Env with a stale GH_HOST entry:

cmd := exec.Command("gh", "api", "/test")
cmd.Env = []string{"GH_HOST=stale.ghe.com", "OTHER=value"}
ForceGHHostEnv(cmd, "github.com")
// assert: exactly one GH_HOST=github.com, no stale entry, OTHER preserved

This would guard the dedup loop and ensure SetGHHostEnv + ForceGHHostEnv chaining (if it ever occurs) doesn't produce duplicate entries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: added TestForceGHHostEnvWithPresetCmdEnv which pre-populates cmd.Env with ["GH_HOST=stale.ghe.com", "OTHER=value"], calls ForceGHHostEnv, then asserts exactly one GH_HOST=github.com entry and that OTHER=value is preserved. This covers the non-nil branch and the dedup loop.

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)
}
4 changes: 4 additions & 0 deletions pkg/workflow/github_cli_wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading