From 825dcc03e5dcafae497772d4560e3d233aa42f2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 07:48:19 +0000 Subject: [PATCH 1/4] Initial plan From 8ecf0ab5b462335bbbd2694660e434f736c822ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 08:17:43 +0000 Subject: [PATCH 2/4] fix: move os.Getenv/LookupEnv out of library code (env-coupling lint) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/add_interactive_engine.go | 2 +- pkg/cli/outcomes_command.go | 2 +- pkg/cli/process_env_lookup.go | 46 +++++++++++++++++++++++++++++++ pkg/workflow/workflow_data.go | 3 +- 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 pkg/cli/process_env_lookup.go diff --git a/pkg/cli/add_interactive_engine.go b/pkg/cli/add_interactive_engine.go index 7d57016c36d..3e9dce72b5f 100644 --- a/pkg/cli/add_interactive_engine.go +++ b/pkg/cli/add_interactive_engine.go @@ -67,7 +67,7 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error { if opt.EnvVarName != "" { envVar = opt.EnvVarName } - if os.Getenv(envVar) != "" { + if lookupEnv(envVar) != "" { defaultEngine = opt.Value addInteractiveLog.Printf("Found env var %s, recommending engine: %s", envVar, opt.Value) break diff --git a/pkg/cli/outcomes_command.go b/pkg/cli/outcomes_command.go index 304690cd8e7..93e311ca6ee 100644 --- a/pkg/cli/outcomes_command.go +++ b/pkg/cli/outcomes_command.go @@ -173,7 +173,7 @@ func RunOutcomes(config OutcomesConfig) error { // The --outcomes-dir flag takes precedence over the GH_AW_OUTCOMES_DIR env var. outcomesDir := config.OutcomesDir if outcomesDir == "" { - outcomesDir, _ = os.LookupEnv("GH_AW_OUTCOMES_DIR") + outcomesDir, _ = lookupEnvOk("GH_AW_OUTCOMES_DIR") } if outcomesDir != "" { writeOutcomeJSONL(outcomesDir, config.RunID, reports) diff --git a/pkg/cli/process_env_lookup.go b/pkg/cli/process_env_lookup.go new file mode 100644 index 00000000000..4af051f8057 --- /dev/null +++ b/pkg/cli/process_env_lookup.go @@ -0,0 +1,46 @@ +package cli + +import ( + "os" + "sync" + + "github.com/github/gh-aw/pkg/logger" +) + +var processEnvLookupLog = logger.New("cli:process_env_lookup") + +type envLookupFunc func(string) (string, bool) + +var ( + processEnvLookupMu sync.RWMutex + processEnvLookup envLookupFunc = os.LookupEnv +) + +// SetEnvLookup configures how CLI helpers resolve environment values. +// Passing nil restores the default process environment lookup. +func SetEnvLookup(lookup func(string) (string, bool)) { + processEnvLookupMu.Lock() + defer processEnvLookupMu.Unlock() + if lookup == nil { + processEnvLookupLog.Print("Restoring default process environment lookup (os.LookupEnv)") + processEnvLookup = os.LookupEnv + return + } + processEnvLookupLog.Print("Installing custom process environment lookup override") + processEnvLookup = lookup +} + +func lookupEnv(key string) string { + processEnvLookupMu.RLock() + defer processEnvLookupMu.RUnlock() + // Intentionally ignore the existence flag to preserve os.Getenv semantics: + // missing variables and explicitly empty variables are both treated as "". + value, _ := processEnvLookup(key) + return value +} + +func lookupEnvOk(key string) (string, bool) { + processEnvLookupMu.RLock() + defer processEnvLookupMu.RUnlock() + return processEnvLookup(key) +} diff --git a/pkg/workflow/workflow_data.go b/pkg/workflow/workflow_data.go index ef6b1de40c6..8d2bc53b431 100644 --- a/pkg/workflow/workflow_data.go +++ b/pkg/workflow/workflow_data.go @@ -2,7 +2,6 @@ package workflow import ( "context" - "os" actionpins "github.com/github/gh-aw/pkg/actionpins" "github.com/github/gh-aw/pkg/logger" @@ -228,7 +227,7 @@ func (d *WorkflowData) PinContext() *actionpins.PinContext { // for example from auto-detected git remotes). Mirror setupGHCommand's // (github_cli.go) precedence: GH_HOST wins when present; default host is // only consulted when GH_HOST is absent. - if ghHost := os.Getenv("GH_HOST"); ghHost != "" { + if ghHost := lookupProcessEnv("GH_HOST"); ghHost != "" { if ghHost != "github.com" { workflowDataLog.Print("Non-github.com GH_HOST detected; disabling hardcoded action pin fallback") pinCtx.SkipHardcodedFallback = true From 8c68bbf13f1363d118a3d9db8da7d62b6d4decb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:45:08 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20rename=20SetEnvLookup=E2=86=92SetPro?= =?UTF-8?q?cessEnvLookup,=20add=20unit=20tests=20for=20injectable=20env=20?= =?UTF-8?q?lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/process_env_lookup.go | 4 +- pkg/cli/process_env_lookup_test.go | 67 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 pkg/cli/process_env_lookup_test.go diff --git a/pkg/cli/process_env_lookup.go b/pkg/cli/process_env_lookup.go index 4af051f8057..46429cdd673 100644 --- a/pkg/cli/process_env_lookup.go +++ b/pkg/cli/process_env_lookup.go @@ -16,9 +16,9 @@ var ( processEnvLookup envLookupFunc = os.LookupEnv ) -// SetEnvLookup configures how CLI helpers resolve environment values. +// SetProcessEnvLookup configures how CLI helpers resolve environment values. // Passing nil restores the default process environment lookup. -func SetEnvLookup(lookup func(string) (string, bool)) { +func SetProcessEnvLookup(lookup func(string) (string, bool)) { processEnvLookupMu.Lock() defer processEnvLookupMu.Unlock() if lookup == nil { diff --git a/pkg/cli/process_env_lookup_test.go b/pkg/cli/process_env_lookup_test.go new file mode 100644 index 00000000000..709c22dc9d8 --- /dev/null +++ b/pkg/cli/process_env_lookup_test.go @@ -0,0 +1,67 @@ +//go:build !integration + +package cli + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetProcessEnvLookup(t *testing.T) { + t.Cleanup(func() { SetProcessEnvLookup(nil) }) + + SetProcessEnvLookup(func(key string) (string, bool) { + env := map[string]string{ + "PRESENT": "hello", + "EMPTY": "", + } + v, ok := env[key] + return v, ok + }) + + t.Run("lookupEnv returns value for present key", func(t *testing.T) { + assert.Equal(t, "hello", lookupEnv("PRESENT")) + }) + + t.Run("lookupEnv returns empty string for missing key", func(t *testing.T) { + assert.Empty(t, lookupEnv("MISSING")) + }) + + t.Run("lookupEnv returns empty string for explicitly empty key", func(t *testing.T) { + assert.Empty(t, lookupEnv("EMPTY")) + }) + + t.Run("lookupEnvOk returns value and true for present key", func(t *testing.T) { + val, ok := lookupEnvOk("PRESENT") + assert.Equal(t, "hello", val) + assert.True(t, ok) + }) + + t.Run("lookupEnvOk returns empty string and false for missing key", func(t *testing.T) { + val, ok := lookupEnvOk("MISSING") + assert.Empty(t, val) + assert.False(t, ok) + }) + + t.Run("lookupEnvOk returns empty string and true for explicitly empty key", func(t *testing.T) { + val, ok := lookupEnvOk("EMPTY") + assert.Empty(t, val) + assert.True(t, ok) + }) +} + +func TestSetProcessEnvLookupNilRestoresDefault(t *testing.T) { + t.Cleanup(func() { SetProcessEnvLookup(nil) }) + + SetProcessEnvLookup(func(key string) (string, bool) { + return "overridden", true + }) + + SetProcessEnvLookup(nil) + + // After restoring the default, missing keys should return "" / false. + val, ok := lookupEnvOk("__GH_AW_TEST_KEY_THAT_MUST_NOT_EXIST__") + assert.Empty(t, val) + assert.False(t, ok) +} From 61f05f1c76c5e773c26f3adf2864caa48d1636c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:21:24 +0000 Subject: [PATCH 4/4] fix: remove logger from pkg/cli process_env_lookup, use lookupEnv in outcomes_command Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- pkg/cli/outcomes_command.go | 2 +- pkg/cli/process_env_lookup.go | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/cli/outcomes_command.go b/pkg/cli/outcomes_command.go index 93e311ca6ee..45ccb7ff133 100644 --- a/pkg/cli/outcomes_command.go +++ b/pkg/cli/outcomes_command.go @@ -173,7 +173,7 @@ func RunOutcomes(config OutcomesConfig) error { // The --outcomes-dir flag takes precedence over the GH_AW_OUTCOMES_DIR env var. outcomesDir := config.OutcomesDir if outcomesDir == "" { - outcomesDir, _ = lookupEnvOk("GH_AW_OUTCOMES_DIR") + outcomesDir = lookupEnv("GH_AW_OUTCOMES_DIR") } if outcomesDir != "" { writeOutcomeJSONL(outcomesDir, config.RunID, reports) diff --git a/pkg/cli/process_env_lookup.go b/pkg/cli/process_env_lookup.go index 46429cdd673..ca4a64e1f28 100644 --- a/pkg/cli/process_env_lookup.go +++ b/pkg/cli/process_env_lookup.go @@ -3,12 +3,8 @@ package cli import ( "os" "sync" - - "github.com/github/gh-aw/pkg/logger" ) -var processEnvLookupLog = logger.New("cli:process_env_lookup") - type envLookupFunc func(string) (string, bool) var ( @@ -22,11 +18,9 @@ func SetProcessEnvLookup(lookup func(string) (string, bool)) { processEnvLookupMu.Lock() defer processEnvLookupMu.Unlock() if lookup == nil { - processEnvLookupLog.Print("Restoring default process environment lookup (os.LookupEnv)") processEnvLookup = os.LookupEnv return } - processEnvLookupLog.Print("Installing custom process environment lookup override") processEnvLookup = lookup }