Skip to content

feat: make environment loading transactional - #4

Merged
cmilesio merged 3 commits into
mainfrom
chore/library-quality-pass
Jul 16, 2026
Merged

feat: make environment loading transactional#4
cmilesio merged 3 commits into
mainfrom
chore/library-quality-pass

Conversation

@cmilesio

@cmilesio cmilesio commented Jul 15, 2026

Copy link
Copy Markdown
Member

Why

Environment variables are process-global state. A failed load must not leave a new endpoint paired with an old credential or destroy the last working configuration.

Previously, files were applied one at a time. A later parse or write failure could happen after earlier values had already changed, and reload began by unsetting previously loaded keys. This pass makes loading transactional and deterministic.

What

  • Discover and parse every selected file before changing the process environment.
  • Apply affected keys in stable order and roll back the exact before-image on failure.
  • Preserve the difference between unset and explicitly empty variables.
  • Track file-owned keys so reload restores removed keys to their pre-load ambient value.
  • Keep the last successful configuration active when reload fails.
  • Synchronize Load, Reload, and IsEnvLoaded state.
  • Search each filename independently within the documented ten-directory bound; the nearest regular file wins.
  • Let base-file APP_ENV select the application layer in the same load.
  • Correct MustGetInt, MustGetBool, native-width GetUint, map trimming, and enum fallback behavior.
  • Make scope ordering deterministic and improve Podman/Docker/container detection.
  • Document that Dump does not redact secrets.

Before / after

Scenario Before After
Malformed file Panic or partial application Error with no environment change
Later-layer failure Earlier layers could remain active Nothing is published until the full plan succeeds
Write failure Partial update possible Applied writes are rolled back
Removed file key Always unset on reload Original ambient value is restored
Empty vs. unset Distinction lost Exact presence is retained
Failed reload Could disturb working state Last successful state remains active
Concurrent load Unsynchronized globals Shared synchronized state
Invalid MustGet* Could become 0 or false Panics as a required-value failure

Reload now behaves like ownership transfer:

before Load:   DATABASE_HOST=db.internal
.env:          DATABASE_HOST=db.local
after Load:    DATABASE_HOST=db.local
.env removes:  DATABASE_HOST
after Reload:  DATABASE_HOST=db.internal

Compatibility

This is not a breaking API release: public function signatures remain unchanged.

Loading and reloading now publish all selected files atomically because preserving partial application would leave mixed credentials and endpoints. APP_ENV declared in the base file now selects its application layer during that same load, so deployments that previously selected a layer only through the ambient environment should review precedence. Invalid MustGetInt and MustGetBool values now panic because the Must contract represents a required valid value, while map trimming and enum fallback changes correct parsing inconsistencies.

Applications that depended on partial loads, destructive reload unsets, or invalid required values should update, but those paths were failures of the existing contract rather than supported extension points.

Notes

Public function signatures remain unchanged, and LoadEnvFileIfExists remains an alias for Load.

Behavior to account for:

  • loader failures are returned instead of panicking;
  • removed file-owned keys restore their original value;
  • invalid required ints and bools now panic;
  • native-width uints are accepted;
  • map keys and values are trimmed;
  • enum fallbacks do not need to appear in the allowed configured-value list.

Review

  1. loader.go contains planning, application, rollback, and ownership tracking.
  2. loader_test.go covers empty/unset restoration, failed writes, reloads, discovery, and concurrency.
  3. env.go, container.go, host.go, and scope.go contain the getter and runtime corrections.

@codecov-commenter

codecov-commenter commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cmilesio cmilesio changed the title Harden environment loading feat: make environment loading transactional Jul 15, 2026
@cmilesio
cmilesio merged commit 1c20120 into main Jul 16, 2026
2 checks passed
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