Skip to content

Commit f68249c

Browse files
authored
refactor: centralize env access via internal/env package (#27)
* refactor: replace context.TODO() with context.Background() and harden CI/CD - Replace all context.TODO() calls in production and test code with context.Background() or proper context propagation - Enable shadow and nilness govet linters; re-enable select staticcheck checks (SA4006, S1011, S1034) - Fix CI coverage threshold comment (50% -> 60%) - Extract markdownlint config to .markdownlint-cli2.jsonc - Pin actions/setup-go to SHA in setup-deps and hawk actions - Change setup-deps default branch from dev to main - Add tracking issue references to all t.Skip() calls * refactor: centralize env access via internal/env package - Create internal/env/env.go with zero-dependency Getenv wrapper - Update config.Getenv to delegate to env.Getenv - Migrate os.Getenv calls across internal/ to env.Getenv or config.Getenv - Update AGENTS.md policy: env.Getenv for simple reads, config.EnvManager for profile/secret management - Add exceptions for runtime probes and telemetry * fix: resolve 145 golangci-lint shadow and nilness issues This commit resolves all 145 golangci-lint issues that surfaced when the shadow, nilness, and additional staticcheck checks were enabled: - 142 shadow (govet): renamed inner shadowed variables to context-specific names (statErr, writeErr, unmarshalErr, etc.) - 22 model/provider shadows: renamed to modelName/providerName - 2 nilness tautologies (non-nil != nil): removed redundant nil checks - 1 staticcheck SA4006 (unused value): removed dead assignment No function signatures, exported types, or behavior changed. No linters were disabled or skipped. .golangci.yml is unchanged. Build and lint pass cleanly. The CI lint check on PR #26 will now pass.
1 parent d1bf315 commit f68249c

9 files changed

Lines changed: 50 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ test: add coverage for guardian
246246

247247
## Anti-Patterns
248248

249-
- **No `os.Getenv` in `internal/`** — use `config.EnvManager` to centralize env access. Exception: `internal/observability/oteltrace/` for telemetry env vars.
249+
- **No `os.Getenv` in `internal/`** — use `env.Getenv` (in `internal/env/`) for simple reads, or `config.Getenv` if the package can import `internal/config` without cycles. `config.EnvManager` is for profile/secret management. Exceptions: `internal/observability/oteltrace/` for telemetry env vars; runtime environment probes (e.g. `TMUX`, `STY`, `TERM_PROGRAM`, `SHELL`, `GOPATH`) which are set by the OS/terminal and not by config.
250250
- **No `panic()` for error handling** — return `error` values. Exception: `init()` functions for package-level assertions.
251251
- **No `fmt.Print` for logging** — use `logger.Logger` with structured fields. Exception: `internal/onboarding/` and `internal/engine/scaffold/` for user-facing CLI output.
252252
- **No API keys in settings.json** — use OS secret store via `credentials` package and `/config` command.

internal/auth/auth.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,19 @@ func (s *SecureStorage) setMacOS(account, token string) error {
9393
return err
9494
}
9595

96+
func homeDir() string {
97+
if runtime.GOOS == "windows" {
98+
if d := os.Getenv("USERPROFILE"); d != "" {
99+
return d
100+
}
101+
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
102+
}
103+
home, _ := os.UserHomeDir()
104+
return home
105+
}
106+
96107
func (s *SecureStorage) getFile(account string) (string, error) {
97-
path := filepath.Join(os.Getenv("HOME"), ".hawk", ".tokens")
108+
path := filepath.Join(homeDir(), ".hawk", ".tokens")
98109
data, err := os.ReadFile(path)
99110
if err != nil {
100111
return "", err
@@ -107,7 +118,7 @@ func (s *SecureStorage) getFile(account string) (string, error) {
107118
}
108119

109120
func (s *SecureStorage) setFile(account, token string) error {
110-
path := filepath.Join(os.Getenv("HOME"), ".hawk", ".tokens")
121+
path := filepath.Join(homeDir(), ".hawk", ".tokens")
111122
var tokens map[string]string
112123
if data, err := os.ReadFile(path); err == nil {
113124
_ = json.Unmarshal(data, &tokens)

internal/autoinit/autoinit.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"os"
1818
"path/filepath"
1919
"strings"
20+
21+
"github.com/GrayCodeAI/hawk/internal/config"
2022
)
2123

2224
// markerName is the file written under the project's .hawk directory once an
@@ -64,7 +66,7 @@ type Decision struct {
6466

6567
// Disabled reports whether auto-init is disabled via the environment.
6668
func Disabled() bool {
67-
return isTruthy(os.Getenv(disableEnv))
69+
return isTruthy(config.Getenv(disableEnv))
6870
}
6971

7072
// HasContext reports whether root already contains a project context file.
@@ -105,7 +107,7 @@ func MaybeRun(ctx context.Context, opts Options) (Decision, error) {
105107

106108
disabled := opts.disableEnvValue
107109
if disabled == "" {
108-
disabled = os.Getenv(disableEnv)
110+
disabled = config.Getenv(disableEnv)
109111
}
110112
if isTruthy(disabled) {
111113
return Decision{Skipped: "disabled via " + disableEnv}, nil

internal/config/envmanager.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"sync"
1313

1414
"github.com/GrayCodeAI/eyrie/credentials"
15+
"github.com/GrayCodeAI/hawk/internal/env"
1516
)
1617

1718
// EnvVar represents a single environment variable with metadata.
@@ -32,6 +33,13 @@ type EnvManager struct {
3233
mu sync.RWMutex
3334
}
3435

36+
// Getenv returns the value of an environment variable.
37+
// Delegates to internal/env to avoid import cycles for callers in packages
38+
// that already import internal/config.
39+
func Getenv(key string) string {
40+
return env.Getenv(key)
41+
}
42+
3543
// NewEnvManager creates a new EnvManager with initialized maps.
3644
func NewEnvManager() *EnvManager {
3745
return &EnvManager{

internal/env/env.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package env
2+
3+
import "os"
4+
5+
// Getenv returns the value of an environment variable.
6+
// This is the centralized access point for all env var reads in internal/
7+
// packages. Packages that cannot import internal/config due to import cycles
8+
// use this package instead. The config package also delegates to this function.
9+
//
10+
// Using this function instead of os.Getenv makes env access grep-able and
11+
// allows future migration to a more sophisticated env management layer.
12+
func Getenv(key string) string {
13+
return os.Getenv(key)
14+
}

internal/resilience/health/diagnostics.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
"github.com/GrayCodeAI/eyrie/credentials"
16+
"github.com/GrayCodeAI/hawk/internal/config"
1617
)
1718

1819
// DiagnosticResult holds the outcome of a single diagnostic check.
@@ -367,7 +368,7 @@ func checkAPIKeySet() DiagnosticResult {
367368

368369
func checkModelConfigured() DiagnosticResult {
369370
start := time.Now()
370-
model := os.Getenv("HAWK_MODEL")
371+
model := config.Getenv("HAWK_MODEL")
371372
if model == "" {
372373
return DiagnosticResult{
373374
Name: "model_configured",

internal/tool/safety.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
"net"
77
"net/http"
88
"net/url"
9-
"os"
109
"path/filepath"
1110
"regexp"
1211
"strings"
1312
"time"
1413

14+
"github.com/GrayCodeAI/hawk/internal/env"
1515
"github.com/GrayCodeAI/hawk/internal/home"
1616
)
1717

@@ -231,7 +231,7 @@ func IsSensitivePath(path string) string {
231231
}
232232
}
233233

234-
if cfgDir := strings.TrimSpace(os.Getenv("HAWK_CONFIG_DIR")); cfgDir != "" {
234+
if cfgDir := strings.TrimSpace(env.Getenv("HAWK_CONFIG_DIR")); cfgDir != "" {
235235
customProv := filepath.Clean(filepath.Join(cfgDir, "provider.json"))
236236
if clean == customProv {
237237
return "access to provider.json is blocked for security (API credentials)"

internal/tool/web_search_brave.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import (
77
"io"
88
"net/http"
99
"net/url"
10-
"os"
1110
"time"
11+
12+
"github.com/GrayCodeAI/hawk/internal/env"
1213
)
1314

1415
// braveClient is a Brave Search API client.
@@ -24,7 +25,7 @@ type braveClient struct {
2425
// the BRAVE_SEARCH_API_KEY environment variable.
2526
func newBraveClient() *braveClient {
2627
return &braveClient{
27-
apiKey: os.Getenv("BRAVE_SEARCH_API_KEY"),
28+
apiKey: env.Getenv("BRAVE_SEARCH_API_KEY"),
2829
http: &http.Client{
2930
Timeout: 15 * time.Second,
3031
},

internal/tool/web_search_searxng.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
"io"
88
"net/http"
99
"net/url"
10-
"os"
1110
"strings"
1211
"time"
12+
13+
"github.com/GrayCodeAI/hawk/internal/env"
1314
)
1415

1516
// searxngClient is a SearXNG API client.
@@ -23,7 +24,7 @@ type searxngClient struct {
2324
// newSearxngClient creates a new SearXNG client, reading the instance URL from
2425
// the SEARXNG_URL environment variable.
2526
func newSearxngClient() *searxngClient {
26-
baseURL := os.Getenv("SEARXNG_URL")
27+
baseURL := env.Getenv("SEARXNG_URL")
2728
// Ensure no trailing slash
2829
baseURL = strings.TrimRight(baseURL, "/")
2930
return &searxngClient{

0 commit comments

Comments
 (0)