AGENTS.md - dotenv-merge Development Guide
๐ฏ Project Overview
dotenv-merge is a format-specific implementation of the *-merge gem family for .env files. It provides intelligent environment file merging using line-based analysis (no AST parser available for .env format).
Core Philosophy: Intelligent .env file merging that preserves structure, comments, and formatting while applying updates from templates.
Repository: https://github.com/kettle-rb/dotenv-merge
Current Version: 1.0.3
Required Ruby: >= 3.2.0 (currently developed against Ruby 4.0.1)
๐๏ธ Architecture: Line-Based Implementation
What dotenv-merge Provides
-
Dotenv::Merge::SmartMergerโ .env-specific SmartMerger implementation -
Dotenv::Merge::FileAnalysisโ .env file analysis with key-value extraction -
Dotenv::Merge::LineNodeโ Line-based node representation -
Dotenv::Merge::EntryNodeโ Key-value pair node -
Dotenv::Merge::MergeResultโ .env-specific merge result -
Dotenv::Merge::ConflictResolverโ .env conflict resolution -
Dotenv::Merge::FreezeNodeโ .env freeze block support -
Dotenv::Merge::DebugLoggerโ .env-specific debug logging
Key Dependencies
| Gem | Role |
|---|---|
ast-merge (~> 4.0) |
Base classes and shared infrastructure (uses Text::SmartMerger pattern) |
version_gem (~> 1.1) |
Version management |
No Parser Backend
dotenv-merge uses line-based parsing (similar to Ast::Merge::Text::SmartMerger):
| Approach | Parser | Platform | Notes |
|---|---|---|---|
| Line-based | None (regex) | All platforms | Parses KEY=value lines with regex |
๐ Project Structure
lib/dotenv/merge/
โโโ smart_merger.rb # Main SmartMerger implementation
โโโ file_analysis.rb # .env file analysis
โโโ line_node.rb # Line representation
โโโ entry_node.rb # Key-value pair node
โโโ merge_result.rb # Merge result object
โโโ conflict_resolver.rb # Conflict resolution
โโโ freeze_node.rb # Freeze block support
โโโ debug_logger.rb # Debug logging
โโโ version.rb
spec/dotenv/merge/
โโโ smart_merger_spec.rb
โโโ file_analysis_spec.rb
โโโ entry_node_spec.rb
โโโ integration/
๐ง Development Workflows
Running Tests
# Full suite
bundle exec rspec
# Single file (disable coverage threshold check)
K_SOUP_COV_MIN_HARD=false bundle exec rspec spec/dotenv/merge/smart_merger_spec.rb
Note: Always run commands in the project root (/home/pboling/src/kettle-rb/ast-merge/vendor/dotenv-merge). Allow direnv to load environment variables first by doing a plain cd before running commands.
Coverage Reports
cd /home/pboling/src/kettle-rb/ast-merge/vendor/dotenv-merge
bin/rake coverage && bin/kettle-soup-cover -d
๐ Project Conventions
API Conventions
SmartMerger API
-
mergeโ Returns a String (the merged .env content) -
merge_resultโ Returns a MergeResult object -
to_son MergeResult returns the merged content as a string
.env-Specific Features
Key-Value Matching:
# Template
DATABASE_URL=postgres://localhost/template_db
API_KEY=template_key
# Destination
DATABASE_URL=postgres://localhost/production_db
CUSTOM_VAR=keep_this
Freeze Blocks:
# dotenv-merge:freeze
SECRET_KEY=custom_secret_dont_override
CUSTOM_TOKEN=abc123
# dotenv-merge:unfreeze
DATABASE_URL=postgres://localhost/db
Comment Preservation:
# Database configuration
DATABASE_URL=postgres://localhost/db
# API keys (do not commit to git)
API_KEY=your_key_here
kettle-dev Tooling
This project uses kettle-dev for gem maintenance automation:
- Rakefile: Sourced from kettle-dev template
- CI Workflows: GitHub Actions and GitLab CI managed via kettle-dev
-
Releases: Use
kettle-releasefor automated release process
Version Requirements
- Ruby >= 3.2.0 (gemspec), developed against Ruby 4.0.1 (
.tool-versions) -
ast-merge>= 4.0.0 required
๐งช Testing Patterns
No Parser Dependency Tags
Since dotenv-merge uses line-based parsing (no external parser), there are no special dependency tags needed:
โ CORRECT:
RSpec.describe Dotenv::Merge::SmartMerger do
# No special tags needed - always runs
end
โ WRONG:
before do
skip "Requires parser" unless parser_available? # NOT NEEDED
end
Shared Examples
dotenv-merge uses shared examples from ast-merge:
it_behaves_like "Ast::Merge::FileAnalyzable"
it_behaves_like "Ast::Merge::ConflictResolverBase"
it_behaves_like "a reproducible merge", "scenario_name", { preference: :template }
๐ Critical Files
| File | Purpose |
|---|---|
lib/dotenv/merge/smart_merger.rb |
Main .env SmartMerger implementation |
lib/dotenv/merge/file_analysis.rb |
.env file analysis and key extraction |
lib/dotenv/merge/entry_node.rb |
Key-value pair abstraction |
lib/dotenv/merge/debug_logger.rb |
.env-specific debug logging |
spec/spec_helper.rb |
Test suite entry point |
.envrc |
Coverage thresholds and environment configuration |
๐ Common Tasks
# Run all specs with coverage
bundle exec rake spec
# Generate coverage report
bundle exec rake coverage
# Check code quality
bundle exec rake reek
bundle exec rake rubocop_gradual
# Prepare and release
kettle-changelog && kettle-release
๐ Integration Points
-
ast-merge: Inherits base classes (SmartMergerBase,FileAnalyzable, etc.) -
Line-based parsing: Similar to
Ast::Merge::Text::SmartMergerpattern -
RSpec: Full integration via
ast/merge/rspec -
SimpleCov: Coverage tracked for
lib/**/*.rb; spec directory excluded
๐ก Key Insights
- Line-based parsing: No AST parser exists for .env format; uses regex to parse KEY=value
- Key matching: Environment variables matched by key name (case-sensitive)
- Comment preservation: Comments on their own lines are preserved
-
Export handling: Lines starting with
exportare supported - Quote handling: Single quotes, double quotes, and no quotes all supported
-
Freeze blocks use
# dotenv-merge:freeze: Standard comment syntax - Cross-platform: Pure Ruby, no native dependencies
๐ซ Common Pitfalls
-
Keys are case-sensitive:
DATABASE_URLanddatabase_urlare different -
No whitespace normalization:
KEY=valueandKEY = valueare treated differently -
Quote differences matter:
KEY="value"andKEY=valueare preserved as-is - Do NOT load vendor gems โ They are not part of this project; they do not exist in CI
-
Use
tmp/for temporary files โ Never use/tmpor other system directories -
Do NOT chain
cdwith&&โ Runcdas a separate command sodirenvloads ENV
๐ง .env-Specific Notes
Line Types
# Comment line
KEY=value # Key-value pair
export KEY=value # Exported variable
KEY="quoted value" # Quoted value
KEY='single quoted' # Single-quoted value
# Empty line
Parsing Rules
# Valid formats
KEY=value
KEY="value with spaces"
KEY='value with "quotes"'
export DATABASE_URL=postgres://localhost/db
MULTILINE="line 1
line 2" # Not recommended but supported by some parsers
Merge Behavior
- Keys: Matched by exact key name (case-sensitive)
- Comments: Preserved on their own lines
-
Exports:
exportkeyword preserved if present - Quotes: Quote style preserved from source
- Freeze blocks: Protect customizations from template updates
- Order: Key order preserved from destination unless new keys added
EntryNode Structure
entry = Dotenv::Merge::EntryNode.new(
key: "DATABASE_URL",
value: "postgres://localhost/db",
line: "DATABASE_URL=postgres://localhost/db",
has_export: false,
quote_char: nil # or '"' or "'"
)
entry.key # "DATABASE_URL"
entry.value # "postgres://localhost/db"
entry.to_s # "DATABASE_URL=postgres://localhost/db"