Refactor duplicate environment variable getters into reusable envutil package#816
Refactor duplicate environment variable getters into reusable envutil package#816
Conversation
…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>
There was a problem hiding this comment.
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/envutilhelpers:GetEnvString,GetEnvInt(positive int),GetEnvBool(truthy/falsy set). - Updated
internal/cmdflag default getters (logging + DIFC) to callenvutilinstead of duplicating logic. - Added unit tests for the new
envutilhelpers.
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.
| if value, err := strconv.Atoi(envValue); err == nil && value > 0 { | ||
| return value |
There was a problem hiding this comment.
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).
| 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 | |
| } |
| 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, | ||
| }, | ||
| } |
There was a problem hiding this comment.
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.
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.
Four
getDefault*()functions ininternal/cmdduplicated the same environment variable getter pattern with minor variations (string, int, bool).Changes
Created
internal/envutilpackage with generic environment variable utilities:GetEnvString(key, default)- string valuesGetEnvInt(key, default)- integer values with positive validationGetEnvBool(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
Impact
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/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/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/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/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/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 theinternal/cmdpackage 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
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:
Code Examples
String Getter (getDefaultLogDir):
String Getter (getDefaultPayloadDir) - Nearly identical:
Integer Getter (getDefaultPayloadSizeThreshold):
Boolean Getter (getDefaultEnableDIFC):
Impact Analysis
Maintainability
Bug Risk
> 0, but what about negative values elsewhere?Code Bloat
Refactoring Recommendations
1. Create Generic Environment Getter Utility
Effort: Low-Medium (2-4 hours)
Extract common pattern into type-safe generic utility: