Skip to content

Add EnvManager #578

Merged
mokagio merged 31 commits intotrunkfrom
mokagio/env-manager
Apr 21, 2026
Merged

Add EnvManager #578
mokagio merged 31 commits intotrunkfrom
mokagio/env-manager

Conversation

@mokagio
Copy link
Copy Markdown
Contributor

@mokagio mokagio commented Jul 29, 2024

What does it do?

Adds EnvManager, a helper class for loading .env files and accessing environment variables with clear error messages.

Originally developed in the Gravatar-SDK-iOS repo, then adapted for the release toolkit.

Note

The diff is big, but it's 2/3 tests

  • Loads env vars from a .env file via Dotenv at initialization
  • get_required_env!(key) returns the value or raises with a context-aware message (CI vs. local, file exists vs. missing)
  • require_env_vars!(*keys) validates multiple keys at once, failing fast on the first missing one — accepts both varargs and arrays
  • Warns at init when the .env file is missing (suppressed on CI)
  • Instance-based design with class-level convenience methods (set_up, get_required_env!, reset!, configured?) for backward compatibility
  • Error and warning output decoupled via injectable lambdas, defaulting to Fastlane UI
  • CI environment helpers (build_number, branch_name, commit_hash, pull_request_number, pr_number_or_branch_name)

Example adoption

See Automattic/simplenote-ios#1742 for an example of migrating a project from a local EnvManager copy to the release-toolkit version.

Checklist before requesting a review

  • Run bundle exec rubocop to test for code style violations and recommendations
  • Add Unit Tests (aka specs/*_spec.rb) if applicable
  • Run bundle exec rspec to run the whole test suite and ensure all your tests pass
  • Make sure you added an entry in the CHANGELOG.md file to describe your changes under the appropriate existing ### subsection of the existing ## Trunk section.
  • If applicable, add an entry in the MIGRATION.md file to describe how the changes will affect the migration from the previous major version and what the clients will need to change and consider.

@mokagio mokagio changed the title Add EnvManager source developed in Gravatar-SDK-iOS Add EnvManager Jul 29, 2024
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Jul 29, 2024

1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@mokagio mokagio self-assigned this Nov 15, 2024
mokagio and others added 6 commits April 7, 2026 16:55
---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
It's an internal detail, not part of the public API.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Class methods delegate to a `@default` instance, so the existing
`EnvManager.set_up` / `EnvManager.get_required_env!` API is preserved.
This allows multiple independent configurations when needed and
makes the class testable without global state.

Also fixes the empty-value check to use the configurable
`@print_error_lambda` instead of a hardcoded `UI.user_error!` call.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Covers initialization (path defaults, Dotenv loading),
`get_required_env!` (happy path, three error branches, empty value),
`require_env_vars!`, and class-level convenience methods.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Without this, the class isn't available to gem consumers.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
@mokagio mokagio force-pushed the mokagio/env-manager branch from d58a39e to e84c9b9 Compare April 7, 2026 09:55
mokagio and others added 9 commits April 8, 2026 10:56
Consumers (and tests) had no way to clear the class-level `@default` instance.
`reset!` enables proper teardown; `configured?` lets callers check state.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
`Dotenv.load` silently succeeds on missing files, deferring the error to `get_required_env!` time.
An early warning helps developers notice misconfiguration sooner.
Skipped on CI where .env files are never expected.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Same pattern as `print_error_lambda` — defaults to `UI.important` but can be injected.
Tests now use a capturing lambda instead of mocking `FastlaneCore::UI`.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
`build_number`, `branch_name`, `commit_hash`, `pull_request_number`, and `pr_number_or_branch_name` wrap Buildkite env vars with sensible defaults and type coercion.

Addresses AINFRA-829.
See also https://github.com/Automattic/Gravatar-iOS/pull/9/files#r2161057223.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Callers building a key list dynamically will naturally have an array.
Without `.flatten`, passing that array wraps it in the splat,
iterating over one element (the array itself) instead of its contents.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
mokagio added a commit to Automattic/simplenote-ios that referenced this pull request Apr 9, 2026
The local `EnvManager` copy is now provided by the release-toolkit gem.
This validates the release-toolkit API against a real consumer.

The `Gemfile` temporarily points to the `mokagio/env-manager` branch
until wordpress-mobile/release-toolkit#578 is merged and released.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
@mokagio mokagio requested a review from Copilot April 9, 2026 06:09
Copy link
Copy Markdown
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

Adds a new EnvManager utility to the release-toolkit Fastlane plugin to load .env files and provide clearer, context-aware environment variable access (including CI metadata helpers), along with an accompanying RSpec test suite.

Changes:

  • Introduce EnvManager class with instance API plus class-level convenience methods (set_up, get_required_env!, require_env_vars!, reset!, configured?).
  • Add comprehensive specs covering initialization behavior, required env var access, and CI helper methods.
  • Update plugin autoload glob to include the new env_manager directory and document the addition in CHANGELOG.md.

Reviewed changes

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

File Description
lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Adds the EnvManager implementation (Dotenv loading, error/warning output injection, CI helpers, class-level delegation).
spec/env_manager_spec.rb Adds specs validating EnvManager behavior and error messaging.
lib/fastlane/plugin/wpmreleasetoolkit.rb Ensures the plugin loads Ruby files from the new env_manager directory.
CHANGELOG.md Notes the new feature in Trunk.

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

# This preserves the existing API: `EnvManager.set_up(...)` then `EnvManager.get_required_env!(...)`.

def self.set_up(**args)
default_print_error_lambda.call('EnvManager is already configured. Call `EnvManager.reset!` before calling `EnvManager.set_up(...)` again.') if configured?
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

.set_up relies on default_print_error_lambda raising to prevent reconfiguration, but if a caller injects a non-raising lambda the method will continue and overwrite @default anyway. Make this guard deterministic by raising/returning immediately after emitting the error.

Suggested change
default_print_error_lambda.call('EnvManager is already configured. Call `EnvManager.reset!` before calling `EnvManager.set_up(...)` again.') if configured?
if configured?
default_print_error_lambda.call('EnvManager is already configured. Call `EnvManager.reset!` before calling `EnvManager.set_up(...)` again.')
return @default
end

Copilot uses AI. Check for mistakes.
def self.default!
return @default if configured?

default_print_error_lambda.call('EnvManager is not configured. Call `EnvManager.set_up(...)` first.')
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

default! calls default_print_error_lambda but does not raise/return afterward; with a non-raising lambda this will return nil and callers will fail later with less clear errors. Consider raising a specific exception (or returning after calling the lambda) so the failure mode is consistent.

Suggested change
default_print_error_lambda.call('EnvManager is not configured. Call `EnvManager.set_up(...)` first.')
message = 'EnvManager is not configured. Call `EnvManager.set_up(...)` first.'
default_print_error_lambda.call(message)
raise RuntimeError, message

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +63
if running_on_ci?
@print_error_lambda.call(message)
elsif File.exist?(@env_path)
@print_error_lambda.call("#{message} Consider adding it to #{@env_path}.")
else
env_file_dir = File.dirname(@env_path)
env_file_name = File.basename(@env_path)

@print_error_lambda.call <<~MSG
#{env_file_name} not found in #{env_file_dir} while looking for env var #{key}.

Please copy #{@env_example_path} to #{@env_path} and fill in the value for #{key}.

mkdir -p #{env_file_dir} && cp #{@env_example_path} #{@env_path}
MSG
end
end

value = ENV.fetch(key)

@print_error_lambda.call("Env var for key #{key} is set but empty. Please set a value for #{key}.") if value.to_s.empty?
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

get_required_env! assumes @print_error_lambda will raise; otherwise execution continues to ENV.fetch(key) (raising KeyError) and the error message paths above are bypassed. To keep behavior consistent with the method’s bang semantics, explicitly raise (or return after a raising call) when the key is missing or the value is empty.

Suggested change
if running_on_ci?
@print_error_lambda.call(message)
elsif File.exist?(@env_path)
@print_error_lambda.call("#{message} Consider adding it to #{@env_path}.")
else
env_file_dir = File.dirname(@env_path)
env_file_name = File.basename(@env_path)
@print_error_lambda.call <<~MSG
#{env_file_name} not found in #{env_file_dir} while looking for env var #{key}.
Please copy #{@env_example_path} to #{@env_path} and fill in the value for #{key}.
mkdir -p #{env_file_dir} && cp #{@env_example_path} #{@env_path}
MSG
end
end
value = ENV.fetch(key)
@print_error_lambda.call("Env var for key #{key} is set but empty. Please set a value for #{key}.") if value.to_s.empty?
error_message =
if running_on_ci?
message
elsif File.exist?(@env_path)
"#{message} Consider adding it to #{@env_path}."
else
env_file_dir = File.dirname(@env_path)
env_file_name = File.basename(@env_path)
<<~MSG
#{env_file_name} not found in #{env_file_dir} while looking for env var #{key}.
Please copy #{@env_example_path} to #{@env_path} and fill in the value for #{key}.
mkdir -p #{env_file_dir} && cp #{@env_example_path} #{@env_path}
MSG
end
@print_error_lambda.call(error_message)
raise KeyError, error_message
end
value = ENV.fetch(key)
if value.to_s.empty?
message = "Env var for key #{key} is set but empty. Please set a value for #{key}."
@print_error_lambda.call(message)
raise ArgumentError, message
end

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +57
@print_error_lambda.call <<~MSG
#{env_file_name} not found in #{env_file_dir} while looking for env var #{key}.

Please copy #{@env_example_path} to #{@env_path} and fill in the value for #{key}.

mkdir -p #{env_file_dir} && cp #{@env_example_path} #{@env_path}
MSG
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The setup instructions include a shell command built from interpolated paths. If any path contains spaces or shell metacharacters, the copy/paste command can break or behave unexpectedly. Quote/escape the paths in the suggested command (e.g., via Shellwords.escape).

Copilot uses AI. Check for mistakes.
Comment thread spec/env_manager_spec.rb Outdated
Comment on lines +359 to +360
expect(described_class.get_required_env!('CK1')).to eq('v1')
expect(described_class.get_required_env!('CK2')).to eq('v2')
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

This example is labeled as testing .require_env_vars! delegation, but it only calls .get_required_env! and never exercises .require_env_vars!. Update the expectation to call described_class.require_env_vars!(...) so the delegation is actually covered.

Suggested change
expect(described_class.get_required_env!('CK1')).to eq('v1')
expect(described_class.get_required_env!('CK2')).to eq('v2')
expect { described_class.require_env_vars!('CK1', 'CK2') }.not_to raise_error

Copilot uses AI. Check for mistakes.
Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb
@@ -6,7 +6,7 @@ module Fastlane
module Wpmreleasetoolkit
# Return all .rb files inside the "actions", "helper" and "models" directories
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

The comment above all_classes says it returns files from only actions, helper, and models, but the glob also includes versioning and now env_manager. Update the comment to reflect the actual directories being loaded.

Suggested change
# Return all .rb files inside the "actions", "helper" and "models" directories
# Return all .rb files inside the "actions", "env_manager", "helper", "models", and "versioning" directories

Copilot uses AI. Check for mistakes.
mokagio and others added 6 commits April 10, 2026 10:24
The guard relied on `default_print_error_lambda` raising to prevent
overwriting `@default`.
With a non-raising lambda, execution continued and silently replaced
the configured instance.

Return the existing instance after emitting the error so the guard
works regardless of whether the lambda raises.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
`default!` relied on the error lambda raising to halt execution.
With a non-raising lambda, it returned `nil`, causing confusing
`NoMethodError`s downstream.

Now explicitly raises after calling the lambda as a safety net.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Both the missing-key and empty-value branches relied on the error
lambda raising to halt execution.
With a non-raising lambda, the missing-key path fell through to
`ENV.fetch` (less helpful `KeyError`), and the empty-value path
silently returned the empty string.

Now raises `KeyError` / `ArgumentError` after calling the lambda as
a safety net.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
The test was calling `.get_required_env!` instead of
`.require_env_vars!`, so the delegation was never exercised.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Paths containing spaces or shell metacharacters broke the copy-paste
`mkdir -p ... && cp ...` command in the setup instructions.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
@mokagio mokagio marked this pull request as ready for review April 10, 2026 00:46
@mokagio mokagio requested review from Copilot and iangmaia April 10, 2026 00:46
Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Outdated
Copy link
Copy Markdown
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

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


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

Comment thread spec/env_manager_spec.rb
Comment thread spec/env_manager_spec.rb
Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Outdated
Comment on lines +154 to +156

def self.default_print_error_lambda
@default&.instance_variable_get(:@print_error_lambda) || @default_print_error_lambda || ->(message) { FastlaneCore::UI.user_error!(message) }
Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Outdated
mokagio and others added 3 commits April 13, 2026 15:55
`EnvManager` requires `dotenv` directly.
It was only available transitively through `fastlane`,
which would break the plugin if Fastlane ever dropped it.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A strict `ENV['CI'] == 'true'` check misses providers that set
`CI=1` or other truthy values. Accept any non-empty value other
than `false`/`0`.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces `instance_variable_get(:@print_error_lambda)` with a
proper reader. Less brittle if the ivar is ever renamed.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
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

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.


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

Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Outdated
Comment thread CHANGELOG.md Outdated
### New Features

_None_
- Add `EnvManager` class for loading `.env` files and accessing required environment variables with user-friendly error messages. [#578]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I understand this won't necessarily break compatibility (unless it conflicts to top level EnvManager per repo, but we could fix that here as pointed out above).
But maybe we could still provide some migration instructions for repos, for the sake of clarity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Once EnvManager is namespaced (#3 / per discussion above), there is no class collision risk, so I think we can keep the migration nudge to a single sentence in the existing entry rather than a dedicated section. Updated in cbecee8 to Add Fastlane::Wpmreleasetoolkit::EnvManager … Repos that maintain their own EnvManager can switch to this centralized implementation. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Works for me 👍

Comment thread lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb Outdated
mokagio and others added 5 commits April 21, 2026 15:39
`Dotenv.load` mutates the process `ENV`,
so multiple `EnvManager` instances pointing at different `.env` files
would silently share state through a single global namespace
and `reset!` couldn't undo it.

Use `Dotenv.parse` instead and store the result in `@loaded_env`,
then look up keys in process `ENV` first (preserving the no-override
semantics of the original `Dotenv.load`) and fall back to the parsed file.

Per Ian's suggestion in PR #578.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mention the fully qualified name so repos with a homegrown `EnvManager`
know they can drop it in favour of the toolkit one without name clashes.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the rest of the plugin's conventions
and avoids colliding with the top-level `EnvManager` classes
some consumer repos currently define.

No top-level alias is introduced:
the whole point of the move is to leave the global constant free.

Per Copilot/Ian's suggestion in PR #578.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Copilot's suggestion in PR #578.

Considered two options:

A) Constrain to class + message: `raise_error(KeyError, /…/)`.
B) Match by message regex only.

Picked B as the less constrained option.
The dedicated "raises KeyError even when the error lambda does not raise" spec
already pins down the exception class contract,
so re-checking it on every message variant just couples the test
to an internal detail that a refactor might legitimately change.

Note: the previous form, `raise_error("…")`,
already matched any class with that exact message — Copilot's premise
that it defaulted to `RuntimeError` is not quite right —
so this change mainly relaxes the message match from exact-string
to substring/pattern.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Ian's suggestion in PR #578.

The repo-URL-guessing TODO is a "let's make the lib smarter" wish
we don't actually want.
The Fastlane-decoupling TODO has sat there for a while
without a real reason to act on it
and the rationale comment beside it
just contrasts against a TODO that's now gone.

---

Generated with the help of Claude Code, https://claude.com/claude-code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mokagio
Copy link
Copy Markdown
Contributor Author

mokagio commented Apr 21, 2026

Thanks @iangmaia ! I think I addressed all your suggestions now.

@mokagio mokagio merged commit 32f1d1d into trunk Apr 21, 2026
6 checks passed
@mokagio mokagio deleted the mokagio/env-manager branch April 21, 2026 21:08
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.

4 participants