-
Notifications
You must be signed in to change notification settings - Fork 9
Add EnvManager
#578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Add EnvManager
#578
Changes from all commits
80b44d1
5df93db
b51de9a
1845519
758bcf4
e84c9b9
a8a5f86
7872d46
1f53169
873f052
dae210e
e8dedcb
3584c43
3a37d04
c8d99b9
2e475dd
ea0859a
f54ec40
e234054
31a3731
8574879
b319e37
3272f1e
b093f12
094865f
a8d55e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
|
||
|
|
||
| # 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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. | ||
|
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 | ||
There was a problem hiding this comment.
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
EnvManagerper 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?