Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
f3bb448
Isolate EnvManager instances from global ENV
mokagio Apr 21, 2026
cbecee8
Note EnvManager namespace and migration in CHANGELOG
mokagio Apr 21, 2026
18cb563
Namespace EnvManager under Fastlane::Wpmreleasetoolkit
mokagio Apr 21, 2026
a466316
Match missing-env error specs by message regex
mokagio Apr 21, 2026
f7b50f9
Drop aspirational TODOs from EnvManager
mokagio Apr 21, 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 `Fastlane::Wpmreleasetoolkit::EnvManager` for loading `.env` files and accessing required environment variables with user-friendly error messages. Repos that maintain their own `EnvManager` can switch to this centralized implementation. [#578]

### 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
186 changes: 186 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,186 @@
# frozen_string_literal: true

require 'dotenv'
require 'shellwords'
require 'fastlane'
Comment thread
mokagio marked this conversation as resolved.

module Fastlane
module Wpmreleasetoolkit
# 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

class << self
attr_writer :default_print_error_lambda
end

# Set up by loading the .env file with the given name.
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

# Parse rather than load so we don't mutate the global ENV. Each instance
# gets its own view, and values from the process environment (e.g. set by
# CI) still take precedence — see `env_value`.
@loaded_env = File.exist?(@env_path) ? Dotenv.parse(@env_path) : {}
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_var_set?(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_value(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.
#
# 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.'
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

# Process ENV takes precedence over values loaded from the .env file, matching
# the original `Dotenv.load` (no-override) semantics.
def env_var_set?(key)
ENV.key?(key) || @loaded_env.key?(key)
end

def env_value(key)
ENV.key?(key) ? ENV.fetch(key) : @loaded_env[key]
end

# 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
end
end
Loading