Skip to content

Refactor duplicate environment variable getters into reusable envutil package#816

Merged
lpcox merged 3 commits intomainfrom
copilot/refactor-duplicate-env-getters
Feb 7, 2026
Merged

Refactor duplicate environment variable getters into reusable envutil package#816
lpcox merged 3 commits intomainfrom
copilot/refactor-duplicate-env-getters

Conversation

Copy link
Contributor

Copilot AI commented Feb 7, 2026

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

// 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

Firewall rules blocked me from connecting to one or more addresses (expand for details)

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:

Original prompt

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:

// 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):

func getDefaultLogDir() string {
    if envLogDir := os.Getenv("MCP_GATEWAY_LOG_DIR"); envLogDir != "" {
        return envLogDir
    }
    return defaultLogDir
}

String Getter (getDefaultPayloadDir) - Nearly identical:

func getDefaultPayloadDir() string {
    if envPayloadDir := os.Getenv("MCP_GATEWAY_PAYLOAD_DIR"); envPayloadDir != "" {
        return envPayloadDir
    }
    return defaultPayloadDir
}

Integer Getter (getDefaultPayloadSizeThreshold):

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):

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:

// 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 github/gh-aw-mcpg#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.

Copilot AI and others added 2 commits February 7, 2026 18:28
…tters

- Created internal/envutil package with GetEnvString, GetEnvInt, and GetEnvBool utilities
- Added comprehensive test coverage for envutil package (46 test cases)
- Refactored flags_logging.go to use envutil (reduced from ~35 lines to ~12 lines)
- Refactored flags_difc.go to use envutil (reduced from ~9 lines to ~2 lines)
- All existing tests pass

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
All tests pass including make agent-finished verification

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor duplicate environment variable getter functions Refactor duplicate environment variable getters into reusable envutil package Feb 7, 2026
Copilot AI requested a review from lpcox February 7, 2026 18:30
@lpcox lpcox marked this pull request as ready for review February 7, 2026 18:32
Copilot AI review requested due to automatic review settings February 7, 2026 18:32
@lpcox lpcox merged commit 0744706 into main Feb 7, 2026
5 checks passed
@lpcox lpcox deleted the copilot/refactor-duplicate-env-getters branch February 7, 2026 18:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors duplicated “env var + default fallback” logic used by CLI flag default getters into a shared internal/envutil package, so environment parsing/validation is centralized and easier to reuse across internal/cmd.

Changes:

  • Added internal/envutil helpers: GetEnvString, GetEnvInt (positive int), GetEnvBool (truthy/falsy set).
  • Updated internal/cmd flag default getters (logging + DIFC) to call envutil instead of duplicating logic.
  • Added unit tests for the new envutil helpers.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
internal/envutil/envutil.go Introduces shared env var getter utilities (string/int/bool).
internal/envutil/envutil_test.go Adds test coverage for env var parsing and defaults.
internal/cmd/flags_logging.go Refactors logging-related default getters to use envutil.
internal/cmd/flags_difc.go Refactors DIFC default getter to use envutil.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +24 to +25
if value, err := strconv.Atoi(envValue); err == nil && value > 0 {
return value
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

GetEnvInt previously used fmt.Sscanf("%d") in the flag getter, which accepts leading/trailing whitespace (and newlines). Switching to strconv.Atoi changes behavior so values like " 4096 " will now be treated as invalid and fall back to default. To avoid a subtle regression for env var parsing, trim surrounding whitespace before Atoi (and keep the >0 validation).

Suggested change
if value, err := strconv.Atoi(envValue); err == nil && value > 0 {
return value
envValue = strings.TrimSpace(envValue)
if envValue != "" {
if value, err := strconv.Atoi(envValue); err == nil && value > 0 {
return value
}

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +148
func TestGetEnvInt(t *testing.T) {
tests := []struct {
name string
envKey string
envValue string
setEnv bool
defaultValue int
expected int
}{
{
name: "env var set with valid positive int - returns env value",
envKey: "TEST_INT_VAR",
envValue: "2048",
setEnv: true,
defaultValue: 1024,
expected: 2048,
},
{
name: "env var not set - returns default",
envKey: "TEST_INT_VAR",
setEnv: false,
defaultValue: 1024,
expected: 1024,
},
{
name: "env var empty string - returns default",
envKey: "TEST_INT_VAR",
envValue: "",
setEnv: true,
defaultValue: 1024,
expected: 1024,
},
{
name: "env var with non-numeric value - returns default",
envKey: "TEST_INT_VAR",
envValue: "invalid",
setEnv: true,
defaultValue: 1024,
expected: 1024,
},
{
name: "env var with negative value - returns default",
envKey: "TEST_INT_VAR",
envValue: "-100",
setEnv: true,
defaultValue: 1024,
expected: 1024,
},
{
name: "env var with zero - returns default",
envKey: "TEST_INT_VAR",
envValue: "0",
setEnv: true,
defaultValue: 1024,
expected: 1024,
},
{
name: "env var with very large int - returns env value",
envKey: "TEST_INT_VAR",
envValue: "10240",
setEnv: true,
defaultValue: 1024,
expected: 10240,
},
{
name: "env var with small positive int - returns env value",
envKey: "TEST_INT_VAR",
envValue: "1",
setEnv: true,
defaultValue: 1024,
expected: 1,
},
{
name: "env var with float value - returns default",
envKey: "TEST_INT_VAR",
envValue: "123.45",
setEnv: true,
defaultValue: 1024,
expected: 1024,
},
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

The GetEnvInt tests don't cover whitespace-padded values (e.g. " 2048 ", "2048\n"), which were accepted by the previous implementation using fmt.Sscanf("%d"). Add a couple of cases to lock in the intended behavior (either accept via TrimSpace or explicitly document/reject) so future refactors don't reintroduce parsing regressions.

Copilot uses AI. Check for mistakes.
lpcox added a commit that referenced this pull request Feb 7, 2026
Automated analysis reported ~200 lines of duplicate code across 3
patterns. Investigation shows all significant duplication already
eliminated through prior refactoring.

## Findings

**Pattern 1: Logger initialization (~120 lines)** ✅ Resolved
- Generic helpers: `initGlobalLogger[T]()`, `closeGlobalLogger[T]()` in
`global_helpers.go`
- Generic file init: `initLogger[T]()` with customizable fallback in
`common.go`
- Handles all 4 logger types (FileLogger, JSONLLogger, MarkdownLogger,
ServerFileLogger)

**Pattern 2: Flag env getters (~40 lines)** ✅ Resolved  
- Extracted to `internal/envutil` package in PR #816
- `GetEnvString()`, `GetEnvInt()`, `GetEnvBool()` with validation
- All flag files migrated

**Pattern 3: Global log functions (~96 lines)** ⚠️ By design
- Thin public API wrappers (8 lines each, 119+ call sites)
- `markdown_logger.go` already uses `logWithMarkdown()` helper
- `server_file_logger.go` has different signature and dual-logging
behavior
- Standard Go idiom for stable APIs

## Result

160 lines of actual duplication eliminated. Remaining "duplication" is
intentional thin wrappers following Go best practices. No code changes
required.

> [!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-build1073420489/b275/launcher.test
/tmp/go-build1073420489/b275/launcher.test
-test.testlogfile=/tmp/go-build1073420489/b275/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true go --global
64/pkg/tool/linux_amd64/asm pull.rebase` (dns block)
> - `invalid-host-that-does-not-exist-12345.com`
> - Triggering command: `/tmp/go-build1073420489/b260/config.test
/tmp/go-build1073420489/b260/config.test
-test.testlogfile=/tmp/go-build1073420489/b260/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true go
ternal/fips140/d--64 64/pkg/tool/linu-o user.name` (dns block)
> - `nonexistent.local`
> - Triggering command: `/tmp/go-build1073420489/b275/launcher.test
/tmp/go-build1073420489/b275/launcher.test
-test.testlogfile=/tmp/go-build1073420489/b275/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true go --global
64/pkg/tool/linux_amd64/asm pull.rebase` (dns block)
> - `slow.example.com`
> - Triggering command: `/tmp/go-build1073420489/b275/launcher.test
/tmp/go-build1073420489/b275/launcher.test
-test.testlogfile=/tmp/go-build1073420489/b275/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true go --global
64/pkg/tool/linux_amd64/asm pull.rebase` (dns block)
> - `this-host-does-not-exist-12345.com`
> - Triggering command: `/tmp/go-build1073420489/b284/mcp.test
/tmp/go-build1073420489/b284/mcp.test
-test.testlogfile=/tmp/go-build1073420489/b284/testlog.txt
-test.paniconexit0 -test.timeout=10m0s -test.v=true _.a
ternal/fips140/ed25519/cast.go 64/pkg/tool/linux_amd64/compile` (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 Analysis
Report</issue_title>
> <issue_description># 🔍 Duplicate Code Analysis Report
> 
> *Analysis of commit db3ff1f*
> 
> ## Summary
> 
> This analysis identified **3 significant duplication patterns** in the
codebase that impact maintainability and create opportunities for
refactoring. The patterns span multiple packages including
`internal/logger` and `internal/cmd`, with a total of **~200+ lines of
duplicated or highly similar code**.
> 
> ## Detected Patterns
> 
> This analysis found 3 significant duplication patterns:
> 
> 1. **Logger Initialization Boilerplate** - Severity: High - See
sub-issue #aw_abc123def456
> 2. **Flag Environment Variable Getters** - Severity: Medium - See
sub-issue #aw_789ghi012jkl
> 3. **Global Logging Function Repetition** - Severity: Medium - See
sub-issue #aw_345mno678pqr
> 
> ## Overall Impact
> 
> - **Total Duplicated Lines**: ~200+ lines of similar/duplicated code
> - **Affected Files**: 8 Go files across 2 packages (logger, cmd)
> - **Maintainability Risk**: High - Changes to patterns require updates
across multiple files
> - **Refactoring Priority**: High for Pattern 1, Medium for Patterns
2-3
> 
> ## Key Findings
> 
> ### 1. Logger Package (High Priority)
> - **4 logger files** share nearly identical initialization patterns
> - Each logger reimplements: file opening, error handling, mutex setup,
global state management
> - Total duplication: ~120+ lines across FileLogger, JSONLLogger,
MarkdownLogger, ServerFileLogger
> 
> ### 2. Flag Package (Medium Priority)
> - **3 getDefault* functions** with identical structure in
`flags_logging.go`
> - Additional similar function in `flags_difc.go`
> - Pattern: Check env var → parse/validate → fallback to default
> - Total duplication: ~40+ lines
> 
> ### 3. Global Logging Functions (Medium Priority)
> - **4 LogLevel functions** (Info/Warn/Error/Debug) repeated across 3
logger files
> - Each set: 4 functions × ~8 lines = ~32 lines per file
> - Total duplication: ~96+ lines across file_logger.go,
markdown_logger.go, server_file_logger.go
> 
> ## Next Steps
> 
> 1. Review individual pattern sub-issues for detailed analysis
> 2. Prioritize refactoring based on severity and impact (start with
Pattern 1)
> 3. Create implementation plan for highest priority patterns
> 4. Consider extracting common utilities to shared helper functions
> 
> ## Analysis Metadata
> 
> - **Analyzed Files**: 68 Go files (non-test)
> - **Detection Method**: Serena semantic code analysis + pattern
matching
> - **Commit**: db3ff1f
> - **Analysis Date**: 2026-02-07T10:08:14.613Z
> - **Trigger**: Recent commit adding configurable payload size
threshold (#790)
> 
> 
> 
> 
> > AI generated by [Duplicate Code
Detector](https://github.com/github/gh-aw-mcpg/actions/runs/21778382116)
> > - [x] expires <!-- gh-aw-expires: 2026-02-14T10:13:16.858Z --> on
Feb 14, 2026, 10:13 AM UTC
> 
> <!-- gh-aw-agentic-workflow: Duplicate Code Detector, engine: copilot,
run: https://github.com/github/gh-aw-mcpg/actions/runs/21778382116 -->
> 
> <!-- gh-aw-workflow-id: duplicate-code-detector
--></issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</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 #797

<!-- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants