Populate process ENV from EnvManager by default#723
Conversation
PR #578 switched `EnvManager` from `Dotenv.load` to `Dotenv.parse` to gain instance isolation and clean `reset!` semantics. The trade was net negative: fastlane actions read their `default_value:` from `ENV.fetch(...)`, so without ENV mutation the loaded values are invisible to actions like `app_store_connect_api_key`, `match`, or `upload_to_testflight` — defeating the affordance `EnvManager` exists to provide. Layer parsed values into process `ENV` by default, with no-override semantics so pre-existing `ENV` entries (e.g. set by CI) still win. Track which keys we added so `reset!` can roll back precisely, without disturbing pre-existing entries. Pass `mutate_env: false` to opt out and keep the parse-only / instance-isolation semantics for callers (mostly tests) that want them. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR changes EnvManager’s default behavior to once again populate the process ENV from the loaded .env file (while preserving “no-override” semantics where pre-existing ENV values win). It adds an opt-out (mutate_env: false) for callers that want the parse-only / per-instance isolation behavior introduced previously, and updates reset behavior to roll back any default-instance ENV mutations.
Changes:
- Default
EnvManagerinitialization now layers parsed.envvalues into processENV(no-override), withmutate_env: falseto opt out. - Track and roll back
ENVmutations viarestore_env!, called fromEnvManager.reset!. - Update specs to cover default mutation behavior, opt-out isolation, and rollback semantics; add a breaking-change changelog entry.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb |
Adds mutate_env defaulting to true, implements restore_env!, and makes reset! roll back default-instance mutations. |
spec/env_manager_spec.rb |
Updates and extends tests to validate default ENV mutation, opt-out behavior, and rollback/idempotency. |
CHANGELOG.md |
Documents the breaking behavior change and the mutate_env: false opt-out. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def restore_env! | ||
| @mutated_keys.each { |key| ENV.delete(key) } | ||
| @mutated_keys = [] |
There was a problem hiding this comment.
Good catch — fixed in 97a2002.
@mutated_keys is now @mutations (a key => value hash recording what this instance wrote), and restore_env! only deletes when ENV[key] still equals the value we set. Any later overwrite by another caller is left in place. Added a regression test in spec/env_manager_spec.rb.
Posted by Claude (Opus 4.7) on behalf of @mokagio with approval.
`restore_env!` previously deleted every key this instance had set, even if a later caller had overwritten the value via `ENV[key] = ...`. That contradicts the intent of "remove only what this instance added" and could silently drop a caller's update. Track the value written for each mutated key and delete only when `ENV[key]` still matches. Addresses #723 (comment). --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| ### Breaking Changes | ||
|
|
||
| _None_ | ||
| - `EnvManager`: populate the process `ENV` from the loaded `.env` file by default (no-override semantics — pre-existing `ENV` values win), so fastlane actions that read their `default_value:` from `ENV` find loaded secrets without callers having to thread them through explicitly. Pass `mutate_env: false` to opt out and keep the parse-only / instance-isolation semantics introduced in [#578]. `EnvManager.reset!` now also rolls back the keys it added. [#XXX] |
There was a problem hiding this comment.
Already addressed in the current PR head: the changelog entry now references #723 instead of the placeholder.
Posted by Codex (GPT-5) on behalf of @mokagio with approval.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The `mutate_env: true` default in #723 is a breaking behavior change for anyone on a `14.4.x`+ release that depended on `ENV` staying pristine. Give them the `mutate_env: false` escape hatch under a new 14.x-to-15.0.0 section. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for picking this up @iangmaia ! Looks like Copilot has nothing to add to its previous reviews, which I had already addressed.
What do you think of this? For live validation, see 4-ghe-Automattic/readablog/files#diff-61bacd7fc8b74e42763f36b953e5dfd00182b336a71446ea1c8ddcfbfcb8f147R10-R15 and 147-ghe-Automattic/everyfirst-app/files#diff-d09ea66f8227784ff4393d88a19836f321c915ae10031d16c93d67e6283ab55fR10-R15 |
|
@mokagio I've disabled auto-merge just in case we want to merge @oguzkocer 's #733 first and release a new version before adding this breaking change on |
|
|
||
| ### `EnvManager` populates the process `ENV` by default | ||
|
|
||
| `EnvManager` now layers the values from the loaded `.env` file into the process `ENV` when you call `set_up`/`new`, so fastlane actions that resolve their `default_value:` via `ENV.fetch(...)` can see them. Pre-existing `ENV` entries always win (no-override), and `reset!` removes only the keys this instance added. |
There was a problem hiding this comment.
This mentions set_up/new, then says reset! removes keys this instance added. Correct me if I'm wrong, but reset! only restores the default instance created by set_up and callers using new need restore_env! instead?
| # Always parse rather than load: it lets us track exactly which keys we | ||
| # add to `ENV` so `reset!` can undo only those, without disturbing keys | ||
| # that pre-existed in the process environment. | ||
| @loaded_env = File.exist?(@env_path) ? Dotenv.parse(@env_path) : {} |
There was a problem hiding this comment.
💭 One concern, maybe a bit of an edge case: if I understand correctly, it seems Dotenv.parse + manual ENV mutation does not fully match Dotenv.load semantics for interpolation.
If ENV['A'] already exists and the .env has A=from_file plus B=${A}, Dotenv.load resolves B from the pre-existing ENV['A'], but this implementation resolves it from the file value.
I wonder if a possible solution could be to use Dotenv.load when mutate_env: true, record the ENV delta for rollback, and keep Dotenv.parse only for mutate_env: false? 🤔
iangmaia
left a comment
There was a problem hiding this comment.
Approving to unblock.
I left a couple of comments, I'll leave them up to you. I'd just be careful when finally merging this as it is a breaking version, so perhaps good to keep an eye and coordinate with other PRs we have in the queue.
…utate-env-default-true

What does it do?
Flips
EnvManager's default back to populating the processENVfrom the loaded.envfile (no-override semantics — pre-existingENVwins).Adds a
mutate_env: falseopt-out for callers who want to preserve the parse-only / instance-isolation semantics introduced in #578.Why
#578 switched
EnvManagerfromDotenv.loadtoDotenv.parseto make instances independent of processENVand to givereset!something to undo.Alas, fastlane actions look up their
default_value:viaENV.fetch(...), so withENVpristine, calls likeapp_store_connect_api_keycan't see anything loaded from the.envfile.This PR keeps the structure of #578 (parse + per-instance dict) but layers values into
ENVby default.reset!tracks the keys it added and removes only those, so pre-existingENVentries are never disturbed.Migration
This is technically a breaking behavior change for anyone who upgraded to a release that included #578 and depended on
ENVstaying pristine.Mitigation: pass
mutate_env: falsetoset_up(ornew) to restore the previous behavior.It's unfortunate to have to do a major version bump for this little used feature, but versions are cheap so we're better off keeping semantic integrity, IMHO.
Checklist before requesting a review
bundle exec rubocopto test for code style violations and recommendations.specs/*_spec.rb) if applicable.bundle exec rspecto run the whole test suite and ensure all your tests pass.CHANGELOG.mdfile to describe your changes under the appropriate existing###subsection of the existing## Trunksection.MIGRATION.mdfile — pending a decision on whether this ships in a major or minor bump.