Commit 0744706
authored
Refactor duplicate environment variable getters into reusable envutil package (#816)
Four `getDefault*()` functions in `internal/cmd` duplicated the same
environment variable getter pattern with minor variations (string, int,
bool).
## Changes
- **Created `internal/envutil` package** with generic environment
variable utilities:
- `GetEnvString(key, default)` - string values
- `GetEnvInt(key, default)` - integer values with positive validation
- `GetEnvBool(key, default)` - boolean values (supports:
`1/true/yes/on`, `0/false/no/off`)
- **Refactored flag getters** to use new utilities:
- `flags_logging.go`: 3 getters (log dir, payload dir, payload size
threshold)
- `flags_difc.go`: 1 getter (DIFC enable flag)
- **Added comprehensive test coverage** (46 test cases covering edge
cases and validation)
## Before/After
```go
// Before: 11 lines per getter
func getDefaultPayloadSizeThreshold() int {
if envThreshold := os.Getenv("MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD"); envThreshold != "" {
var threshold int
if _, err := fmt.Sscanf(envThreshold, "%d", &threshold); err == nil && threshold > 0 {
return threshold
}
}
return defaultPayloadSizeThreshold
}
// After: 1 line
func getDefaultPayloadSizeThreshold() int {
return envutil.GetEnvInt("MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD", defaultPayloadSizeThreshold)
}
```
## Impact
- Reduced getter code by 75% (~49 lines → ~12 lines)
- Centralized validation logic (positive integers, case-insensitive
booleans)
- Future flags can use these utilities without reimplementing patterns
> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `example.com`
> - Triggering command: `/tmp/go-build542317655/b279/launcher.test
/tmp/go-build542317655/b279/launcher.test
-test.testlogfile=/tmp/go-build542317655/b279/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/cgo`
(dns block)
> - `invalid-host-that-does-not-exist-12345.com`
> - Triggering command: `/tmp/go-build542317655/b264/config.test
/tmp/go-build542317655/b264/config.test
-test.testlogfile=/tmp/go-build542317655/b264/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/cgo
64/src/encoding/pem/pem.go x_amd64/vet` (dns block)
> - `nonexistent.local`
> - Triggering command: `/tmp/go-build542317655/b279/launcher.test
/tmp/go-build542317655/b279/launcher.test
-test.testlogfile=/tmp/go-build542317655/b279/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/cgo`
(dns block)
> - `slow.example.com`
> - Triggering command: `/tmp/go-build542317655/b279/launcher.test
/tmp/go-build542317655/b279/launcher.test
-test.testlogfile=/tmp/go-build542317655/b279/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/cgo`
(dns block)
> - `this-host-does-not-exist-12345.com`
> - Triggering command: `/tmp/go-build542317655/b288/mcp.test
/tmp/go-build542317655/b288/mcp.test
-test.testlogfile=/tmp/go-build542317655/b288/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true
ache/go/1.25.6/x64/src/runtime/c-errorsas .cfg tnet/tools/as` (dns
block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent)
(admins only)
>
> </details>
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
----
*This section details on the original issue you should resolve*
<issue_title>[duplicate-code] Duplicate Code Pattern: Flag Environment
Variable Getters</issue_title>
<issue_description># 🔍 Duplicate Code Pattern: Flag Environment Variable
Getters
*Part of duplicate code analysis: #797*
## Summary
Multiple `getDefault*()` functions in the `internal/cmd` package follow
an identical pattern for retrieving configuration values from
environment variables with fallback to hardcoded defaults. This pattern
is repeated 4 times across flag files with only minor variations.
## Duplication Details
### Pattern: Environment Variable Getter Functions
- **Severity**: Medium
- **Occurrences**: 4 instances (3 in flags_logging.go, 1 in
flags_difc.go)
- **Locations**:
- `internal/cmd/flags_logging.go`:
- `getDefaultLogDir()` (lines 36-41)
- `getDefaultPayloadDir()` (lines 45-50)
- `getDefaultPayloadSizeThreshold()` (lines 54-64)
- `internal/cmd/flags_difc.go`:
- `getDefaultEnableDIFC()` (lines 30-38)
### Duplicated Structure
All functions follow the same pattern:
```go
// Pattern 1: String environment variable
func getDefault(Name)() string {
if env(Name) := os.Getenv("MCP_GATEWAY_(NAME)"); env(Name) != "" {
return env(Name)
}
return default(Name)
}
// Pattern 2: Boolean environment variable
func getDefault(Name)() bool {
if env(Name) := os.Getenv("MCP_GATEWAY_(NAME)"); env(Name) != "" {
switch strings.ToLower(env(Name)) {
case "1", "true", "yes", "on":
return true
}
}
return default(Name)
}
// Pattern 3: Integer environment variable with validation
func getDefault(Name)() int {
if env(Name) := os.Getenv("MCP_GATEWAY_(NAME)"); env(Name) != "" {
var value int
if _, err := fmt.Sscanf(env(Name), "%d", &value); err == nil && value > 0 {
return value
}
}
return default(Name)
}
```
### Code Examples
**String Getter (getDefaultLogDir)**:
```go
func getDefaultLogDir() string {
if envLogDir := os.Getenv("MCP_GATEWAY_LOG_DIR"); envLogDir != "" {
return envLogDir
}
return defaultLogDir
}
```
**String Getter (getDefaultPayloadDir)** - Nearly identical:
```go
func getDefaultPayloadDir() string {
if envPayloadDir := os.Getenv("MCP_GATEWAY_PAYLOAD_DIR"); envPayloadDir != "" {
return envPayloadDir
}
return defaultPayloadDir
}
```
**Integer Getter (getDefaultPayloadSizeThreshold)**:
```go
func getDefaultPayloadSizeThreshold() int {
if envThreshold := os.Getenv("MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD"); envThreshold != "" {
var threshold int
if _, err := fmt.Sscanf(envThreshold, "%d", &threshold); err == nil && threshold > 0 {
return threshold
}
}
return defaultPayloadSizeThreshold
}
```
**Boolean Getter (getDefaultEnableDIFC)**:
```go
func getDefaultEnableDIFC() bool {
if envDIFC := os.Getenv("MCP_GATEWAY_ENABLE_DIFC"); envDIFC != "" {
switch strings.ToLower(envDIFC) {
case "1", "true", "yes", "on":
return true
}
}
return defaultEnableDIFC
}
```
## Impact Analysis
### Maintainability
- **Medium Risk**: New flags require copy-paste of getter pattern
- **Boilerplate Heavy**: Each new configuration value needs 6-11 lines
of getter code
- **Validation Inconsistency**: Integer validation inline, boolean
validation via switch, no validation for strings
### Bug Risk
- **Low-Medium Risk**: Inconsistent validation approaches could lead to
bugs
- **Example**: Integer validation checks `> 0`, but what about negative
values elsewhere?
- **Missing Features**: No support for default value overrides,
validation error messages
### Code Bloat
- **Total Lines**: ~40 lines of similar getter functions
- **Future Growth**: Every new flag adds 6-11 lines of duplicated code
## Refactoring Recommendations
### 1. Create Generic Environment Getter Utility
**Effort**: Low-Medium (2-4 hours)
Extract common pattern into type-safe generic utility:
```go
// Package envutil provides utilities for reading configuration from environment variables
package envutil
import (
"fmt"
"os"
"strconv"
"strings"
)
// GetEnvString returns string value from env var or default
func GetEnvString(envKey, defaultValue string) string {
if value := os.Getenv(envKey); value != "" {
return value
}
return defaultValue
}
// GetEnvInt returns integer value from env var or default
// Validates that value is positive (> 0)
func GetEnvInt(envKey string, defaultValue int) int {
if envValue := os.Getenv(envKey); envValue != "" {
if value, err := strconv.Atoi(envValue); err == nil && value > 0 {
return value
}
}
return defaultValue
}
// GetEnvBool returns boolean value from env var or default
// Accepts: "1", "true", "yes", "on" (case-insensitive)
func GetEnvBool(envKey string, defaultValue bool) bool {
if envV...
</details>
> **Custom agent used: agentic-workflows**
> GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes #799
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.4 files changed
Lines changed: 503 additions & 30 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
7 | | - | |
8 | | - | |
| 6 | + | |
9 | 7 | | |
10 | 8 | | |
11 | 9 | | |
| |||
28 | 26 | | |
29 | 27 | | |
30 | 28 | | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
37 | | - | |
| 29 | + | |
38 | 30 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
7 | | - | |
8 | | - | |
| 6 | + | |
9 | 7 | | |
10 | 8 | | |
11 | 9 | | |
| |||
34 | 32 | | |
35 | 33 | | |
36 | 34 | | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
| 35 | + | |
41 | 36 | | |
42 | 37 | | |
43 | 38 | | |
44 | 39 | | |
45 | 40 | | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 41 | + | |
50 | 42 | | |
51 | 43 | | |
52 | 44 | | |
53 | 45 | | |
54 | 46 | | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
| 47 | + | |
64 | 48 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
0 commit comments