Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
80b44d1
Add `EnvManager` source developed in Gravatar-SDK-iOS
mokagio Jul 29, 2024
5df93db
Add CHANGELOG entry for EnvManager
mokagio Apr 7, 2026
b51de9a
Make running_on_ci? private
mokagio Apr 7, 2026
1845519
Refactor EnvManager to instance-based design
mokagio Apr 7, 2026
758bcf4
Add tests for `EnvManager`
mokagio Apr 7, 2026
e84c9b9
Wire EnvManager into plugin autoload
mokagio Apr 7, 2026
a8a5f86
Add `reset!` and `configured?` to EnvManager
mokagio Apr 8, 2026
7872d46
Warn when .env file is missing at init
mokagio Apr 8, 2026
1f53169
Decouple warning output via print_warning_lambda
mokagio Apr 8, 2026
873f052
Add CI environment helpers to EnvManager
mokagio Apr 8, 2026
dae210e
Improve EnvManager setup errors
mokagio Apr 9, 2026
e8dedcb
Prevent EnvManager reconfiguration
mokagio Apr 9, 2026
3584c43
Use EnvManager error lambdas consistently
mokagio Apr 9, 2026
3a37d04
Address RuboCop violations
mokagio Apr 9, 2026
c8d99b9
Accept array input in require_env_vars!
mokagio Apr 9, 2026
2e475dd
Make set_up reconfiguration guard deterministic
mokagio Apr 10, 2026
ea0859a
Make default! raise after non-raising lambda
mokagio Apr 10, 2026
f54ec40
Make get_required_env! raise deterministically
mokagio Apr 10, 2026
e234054
Fix require_env_vars! delegation test
mokagio Apr 10, 2026
31a3731
Fix stale comment in all_classes glob
mokagio Apr 10, 2026
8574879
Escape paths in shell command suggestion
mokagio Apr 10, 2026
b319e37
Document Buildkite-CI dependency
mokagio Apr 10, 2026
3272f1e
Declare dotenv as direct runtime dependency
mokagio Apr 13, 2026
b093f12
Loosen CI detection in EnvManager
mokagio Apr 13, 2026
094865f
Use attr_reader for print_error_lambda
mokagio Apr 13, 2026
a8d55e6
Fix typo
mokagio Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _None_

### 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?


### Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ PATH
buildkit (~> 1.5)
chroma (= 0.2.0)
diffy (~> 3.3)
dotenv (~> 2.8)
fastlane (~> 2.231)
gettext (~> 3.5)
git (~> 1.3)
Expand Down
1 change: 1 addition & 0 deletions fastlane-plugin-wpmreleasetoolkit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'buildkit', '~> 1.5'
spec.add_dependency 'chroma', '0.2.0'
spec.add_dependency 'diffy', '~> 3.3'
spec.add_dependency 'dotenv', '~> 2.8'
spec.add_dependency 'fastlane', '~> 2.231'
spec.add_dependency 'gettext', '~> 3.5'
spec.add_dependency 'git', '~> 1.3'
Expand Down
4 changes: 2 additions & 2 deletions lib/fastlane/plugin/wpmreleasetoolkit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

module Fastlane
module Wpmreleasetoolkit
# 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
def self.all_classes
Dir[File.expand_path('**/{actions,helper,models,versioning}/**/*.rb', File.dirname(__FILE__))]
Dir[File.expand_path('**/{actions,env_manager,helper,models,versioning}/**/*.rb', File.dirname(__FILE__))]
end
end
end
Expand Down
173 changes: 173 additions & 0 deletions lib/fastlane/plugin/wpmreleasetoolkit/env_manager/env_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# frozen_string_literal: true

require 'dotenv'
require 'shellwords'
# TODO: It would be nice to decouple this from Fastlane.
# To give a good UX in the current use case, however, it's best to access the Fastlane UI methods directly.
require 'fastlane'
Comment on lines +3 to +7
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 introduces a direct use of Dotenv, but the gemspec doesn’t currently declare dotenv as a direct dependency (it’s only available transitively via fastlane today). Consider adding an explicit runtime dependency on dotenv to avoid the plugin breaking if Fastlane ever changes its dependency tree.

Copilot uses AI. Check for mistakes.
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.

This isn't valid anymore, right? Shall we mark it as resolved @mokagio ?


# Manages loading of environment variables from a .env and accessing them in a user-friendly way.
class EnvManager
attr_reader :env_path, :env_example_path, :print_error_lambda

Comment on lines +9 to +12
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.

@mokagio I think this comment makes sense? 🤔 iinm, I think in a couple of repos we actually have an EnvManager that will conflict with this top level EnvManager.

class << self
attr_writer :default_print_error_lambda
end

# Set up by loading the .env file with the given name.
#
# TODO: We could go one step and guess the name based on the repo URL.
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'd remove TODO's like this one (imo we probably don't need the lib to be that "smart"?).

def initialize(
env_file_name:,
env_file_folder: File.join(Dir.home, '.a8c-apps'),
example_env_file_path: 'fastlane/example.env',
print_error_lambda: ->(message) { FastlaneCore::UI.user_error!(message) },
print_warning_lambda: ->(message) { FastlaneCore::UI.important(message) }
)
@env_path = File.join(env_file_folder, env_file_name)
@env_example_path = example_env_file_path
@print_error_lambda = print_error_lambda
@print_warning_lambda = print_warning_lambda

unless File.exist?(@env_path) || running_on_ci?
@print_warning_lambda.call("Warning: env file not found at #{@env_path}. Environment variables may not be loaded.")
end

Dotenv.load(@env_path)
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.

This makes it not really instance based, right? As instances will share a global state.
initialize loads into ENV, get_required_env! reads from ENV, reset!only clears@default, so managers are not isolated 🤔 I think this either needs to become an explicitly singleton-only helper or the implementation needs to stop using ENV` as per-instance storage.

I think we could do something like:

@loaded_env = File.exist?(@env_path) ? Dotenv.parse(@env_path) : {}

wdyt?

end

# Use this instead of getting values from `ENV` directly. It will throw an error if the requested value is missing or empty.
def get_required_env!(key)
unless ENV.key?(key)
message = "Environment variable '#{key}' is not set."

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 #{Shellwords.shellescape(env_file_dir)} && cp #{Shellwords.shellescape(@env_example_path)} #{Shellwords.shellescape(@env_path)}
MSG
end

@print_error_lambda.call(error_message)
raise KeyError, error_message
end

value = ENV.fetch(key)

if value.to_s.empty?
empty_message = "Env var for key #{key} is set but empty. Please set a value for #{key}."
@print_error_lambda.call(empty_message)
raise ArgumentError, empty_message
end

value
end

# Use this to ensure all env vars a lane requires are set.
#
# The best place to call this is at the start of a lane, to fail early.
def require_env_vars!(*keys)
keys.flatten.each { |key| get_required_env!(key) }
end

# CI environment helpers — read common metadata from the CI provider.
Comment thread
mokagio marked this conversation as resolved.
#
# Notice that given Buildkite is the only CI provider we use, they are Buildkite-dependent.
#
# If this were to be adopted more broadly, we'd need a two-tier approach:
# 1. Detect which CI is in use
# 2. Use its specific env vars
# 3. Maybe fallback to best guess or outright error if no vendor detected

def build_number
ENV.fetch('BUILDKITE_BUILD_NUMBER', '0')
end

def branch_name
ENV.fetch('BUILDKITE_BRANCH', nil)
end

def commit_hash
ENV.fetch('BUILDKITE_COMMIT', nil)
end

# Returns the PR number as an Integer, or nil if not running on a PR build.
# Buildkite sets BUILDKITE_PULL_REQUEST to 'false' (not nil) when not on a PR.
def pull_request_number
pr_num = ENV.fetch('BUILDKITE_PULL_REQUEST', 'false')
pr_num == 'false' ? nil : Integer(pr_num)
end

# Returns a human-readable label: "PR #123" for PR builds, or the branch name otherwise.
def pr_number_or_branch_name
pull_request_number&.then { |num| "PR ##{num}" } || branch_name
end

# Class-level convenience methods that delegate to a default instance.
# This preserves the existing API: `EnvManager.set_up(...)` then `EnvManager.get_required_env!(...)`.

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

@default = new(**args)
end

def self.get_required_env!(key)
default!.get_required_env!(key)
end

def self.require_env_vars!(*keys)
default!.require_env_vars!(*keys)
end

# Clears the default instance, useful for test teardown.
def self.reset!
@default = nil
end

# Returns true if a default instance has been configured via `.set_up`.
def self.configured?
!@default.nil?
end

def self.default!
return @default if configured?

message = 'EnvManager is not configured. Call `EnvManager.set_up(...)` first.'
Comment on lines +148 to +150
default_print_error_lambda.call(message)
raise message
end

def self.default_print_error_lambda
@default&.print_error_lambda || @default_print_error_lambda || ->(message) { FastlaneCore::UI.user_error!(message) }
end

private

# Consider any non-empty, non-falsy value of `CI` to mean we're running on CI.
# Most CI providers set `CI=true`, but some use `CI=1`.
#
# Note: the CI helpers above (`build_number`, etc.) remain Buildkite-specific —
# see the block comment on them. Detection via `CI` is a de facto standard and
# cheap to generalize; value-fetching is not.
def running_on_ci?
value = ENV.fetch('CI', nil)
return false if value.nil? || value.empty?

!%w[false 0].include?(value.downcase)
end
end
Loading