From 8fcb8a67f1813f513bbf5511e8f3d548a75b7a94 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 11:48:41 -0500 Subject: [PATCH 1/8] Refactor Railtie to allow customizing files --- lib/dotenv/rails.rb | 29 ++++++----- spec/dotenv/rails_spec.rb | 105 ++++++++++++++++++++------------------ 2 files changed, 72 insertions(+), 62 deletions(-) diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb index c799ffb4..caa70316 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -33,19 +33,31 @@ module Dotenv # Dotenv Railtie for using Dotenv to load environment from a file into # Rails applications class Railtie < Rails::Railtie + def initialize + config.dotenv = ActiveSupport::OrderedOptions.new.merge!( + mode: :load, + files: [ + root.join(".env.#{Rails.env}.local"), + (root.join(".env.local") unless Rails.env.test?), + root.join(".env.#{Rails.env}"), + root.join(".env") + ].compact + ) + end + # Public: Load dotenv # # This will get called during the `before_configuration` callback, but you # can manually call `Dotenv::Railtie.load` if you needed it sooner. def load - Dotenv.load(*dotenv_files) + Dotenv.load(*config.dotenv.files) end # Public: Reload dotenv # # Same as `load`, but will override existing values in `ENV` def overload - Dotenv.overload(*dotenv_files) + Dotenv.overload(*config.dotenv.files) end # Internal: `Rails.root` is nil in Rails 4.1 before the application is @@ -61,17 +73,8 @@ def self.load instance.load end - private - - def dotenv_files - [ - root.join(".env.#{Rails.env}.local"), - (root.join(".env.local") unless Rails.env.test?), - root.join(".env.#{Rails.env}"), - root.join(".env") - ].compact + config.before_configuration do + config.dotenv.mode == :load ? load : overload end - - config.before_configuration { load } end end diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb index 1baabb0a..35f3ca3a 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -17,9 +17,11 @@ def add(*items) describe Dotenv::Railtie do before do + # Remove the singleton instance if it exists + Dotenv::Railtie.remove_instance_variable(:@instance) + Rails.env = "test" - allow(Rails).to receive(:root) - .and_return Pathname.new(File.expand_path("../../fixtures", __FILE__)) + allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join('../fixtures') Rails.application = double(:application) Spring.watcher = SpecWatcher.new end @@ -30,10 +32,47 @@ def add(*items) Rails.application = nil end + describe "config.dotenv.files" do + it "loads files for development environment" do + Rails.env = "development" + + expect(Dotenv::Railtie.config.dotenv.files).to eql( + [ + Rails.root.join(".env.development.local"), + Rails.root.join(".env.local"), + Rails.root.join(".env.development"), + Rails.root.join(".env") + ] + ) + end + + it "does not load .env.local in test rails environment" do + Rails.env = "test" + expect(Dotenv::Railtie.config.dotenv.files).to eql( + [ + Rails.root.join(".env.test.local"), + Rails.root.join(".env.test"), + Rails.root.join(".env") + ] + ) + end + end + context "before_configuration" do - it "calls #load" do - expect(Dotenv::Railtie.instance).to receive(:load) - ActiveSupport.run_load_hooks(:before_configuration) + context "with mode = :load" do + it "calls #load" do + expect(Dotenv::Railtie.instance).to receive(:load) + ActiveSupport.run_load_hooks(:before_configuration) + end + end + + context "with mode = :overload" do + before { Dotenv::Railtie.config.dotenv.mode = :overload } + + it "calls #overload" do + expect(Dotenv::Railtie.instance).to receive(:overload) + ActiveSupport.run_load_hooks(:before_configuration) + end end end @@ -50,32 +89,16 @@ def add(*items) expect(Spring.watcher.items).to include(path) end - it "does not load .env.local in test rails environment" do - expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql( - [ - Rails.root.join(".env.test.local"), - Rails.root.join(".env.test"), - Rails.root.join(".env") - ] - ) - end - - it "does load .env.local in development environment" do - Rails.env = "development" - expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql( - [ - Rails.root.join(".env.development.local"), - Rails.root.join(".env.local"), - Rails.root.join(".env.development"), - Rails.root.join(".env") - ] - ) - end - it "loads .env.test before .env" do expect(ENV["DOTENV"]).to eql("test") end + it "loads configured files" do + expect(Dotenv).to receive(:load).with("custom.env") + Dotenv::Railtie.config.dotenv.files = ["custom.env"] + Dotenv::Railtie.load + end + context "when Rails.root is nil" do before do allow(Rails).to receive(:root).and_return(nil) @@ -91,32 +114,16 @@ def add(*items) context "overload" do before { Dotenv::Railtie.overload } - it "does not load .env.local in test rails environment" do - expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql( - [ - Rails.root.join(".env.test.local"), - Rails.root.join(".env.test"), - Rails.root.join(".env") - ] - ) - end - - it "does load .env.local in development environment" do - Rails.env = "development" - expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql( - [ - Rails.root.join(".env.development.local"), - Rails.root.join(".env.local"), - Rails.root.join(".env.development"), - Rails.root.join(".env") - ] - ) - end - it "overloads .env with .env.test" do expect(ENV["DOTENV"]).to eql("test") end + it "loads configured files" do + expect(Dotenv).to receive(:overload).with("custom.env") + Dotenv::Railtie.config.dotenv.files = ["custom.env"] + Dotenv::Railtie.overload + end + context "when loading a file containing already set variables" do subject { Dotenv::Railtie.overload } From ce2018f05eb16d14f2bb378b6030802bd00b2992 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 11:56:31 -0500 Subject: [PATCH 2/8] Avoid modifying Rails.env --- lib/dotenv/rails.rb | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb index caa70316..02be8920 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -1,23 +1,5 @@ require "dotenv" -# Fix for rake tasks loading in development -# -# Dotenv loads environment variables when the Rails application is initialized. -# When running `rake`, the Rails application is initialized in development. -# Rails includes some hacks to set `RAILS_ENV=test` when running `rake test`, -# but rspec does not include the same hacks. -# -# See https://github.com/bkeepers/dotenv/issues/219 -if defined?(Rake.application) - task_regular_expression = /^(default$|parallel:spec|spec(:|$))/ - if Rake.application.top_level_tasks.grep(task_regular_expression).any? - environment = Rake.application.options.show_tasks ? "development" : "test" - Rails.env = ENV["RAILS_ENV"] ||= environment - end -end - -Dotenv.instrumenter = ActiveSupport::Notifications - # Watch all loaded env files with Spring begin require "spring/commands" @@ -37,9 +19,9 @@ def initialize config.dotenv = ActiveSupport::OrderedOptions.new.merge!( mode: :load, files: [ - root.join(".env.#{Rails.env}.local"), - (root.join(".env.local") unless Rails.env.test?), - root.join(".env.#{Rails.env}"), + root.join(".env.#{env}.local"), + (root.join(".env.local") unless env.test?), + root.join(".env.#{env}"), root.join(".env") ].compact ) @@ -67,6 +49,25 @@ def root Rails.root || Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd) end + def env + env = Rails.env + + # Dotenv loads environment variables when the Rails application is initialized. + # When running `rake`, the Rails application is initialized in development. + # Rails includes some hacks to set `RAILS_ENV=test` when running `rake test`, + # but rspec does not include the same hacks. + # + # See https://github.com/bkeepers/dotenv/issues/219 + if defined?(Rake.application) + task_regular_expression = /^(default$|parallel:spec|spec(:|$))/ + if Rake.application.top_level_tasks.grep(task_regular_expression).any? + env = ActiveSupport::EnvironmentInquirer.new(Rake.application.options.show_tasks ? "development" : "test") + end + end + + env + end + # Rails uses `#method_missing` to delegate all class methods to the # instance, which means `Kernel#load` gets called here. We don't want that. def self.load @@ -74,6 +75,7 @@ def self.load end config.before_configuration do + Dotenv.instrumenter = ActiveSupport::Notifications config.dotenv.mode == :load ? load : overload end end From f0523393aae5f29252e13a5d2b75ca8a5479bb3a Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 12:14:21 -0500 Subject: [PATCH 3/8] Rename Dotenv::Railtie => Dotenv::Rails --- lib/dotenv/rails.rb | 16 +++++++++++++--- spec/dotenv/rails_spec.rb | 30 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb index 02be8920..d8a33552 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -14,7 +14,11 @@ module Dotenv # Dotenv Railtie for using Dotenv to load environment from a file into # Rails applications - class Railtie < Rails::Railtie + class Rails < ::Rails::Railtie + def self.deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end + def initialize config.dotenv = ActiveSupport::OrderedOptions.new.merge!( mode: :load, @@ -46,11 +50,11 @@ def overload # initialized, so this falls back to the `RAILS_ROOT` environment variable, # or the current working directory. def root - Rails.root || Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd) + ::Rails.root || Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd) end def env - env = Rails.env + env = ::Rails.env # Dotenv loads environment variables when the Rails application is initialized. # When running `rake`, the Rails application is initialized in development. @@ -74,9 +78,15 @@ def self.load instance.load end + initializer "dotenv.deprecator" do |app| + app.deprecators[:dotenv] = Dotenv::Railtie.deprecator + end + config.before_configuration do Dotenv.instrumenter = ActiveSupport::Notifications config.dotenv.mode == :load ? load : overload end end + + Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Dotenv::Railtie", "Dotenv::Rails", Dotenv::Rails.deprecator) end diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb index 35f3ca3a..30f99eb7 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -15,10 +15,10 @@ def add(*items) end end -describe Dotenv::Railtie do +describe Dotenv::Rails do before do # Remove the singleton instance if it exists - Dotenv::Railtie.remove_instance_variable(:@instance) + Dotenv::Rails.remove_instance_variable(:@instance) Rails.env = "test" allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join('../fixtures') @@ -36,7 +36,7 @@ def add(*items) it "loads files for development environment" do Rails.env = "development" - expect(Dotenv::Railtie.config.dotenv.files).to eql( + expect(Dotenv::Rails.config.dotenv.files).to eql( [ Rails.root.join(".env.development.local"), Rails.root.join(".env.local"), @@ -48,7 +48,7 @@ def add(*items) it "does not load .env.local in test rails environment" do Rails.env = "test" - expect(Dotenv::Railtie.config.dotenv.files).to eql( + expect(Dotenv::Rails.config.dotenv.files).to eql( [ Rails.root.join(".env.test.local"), Rails.root.join(".env.test"), @@ -61,23 +61,23 @@ def add(*items) context "before_configuration" do context "with mode = :load" do it "calls #load" do - expect(Dotenv::Railtie.instance).to receive(:load) + expect(Dotenv::Rails.instance).to receive(:load) ActiveSupport.run_load_hooks(:before_configuration) end end context "with mode = :overload" do - before { Dotenv::Railtie.config.dotenv.mode = :overload } + before { Dotenv::Rails.config.dotenv.mode = :overload } it "calls #overload" do - expect(Dotenv::Railtie.instance).to receive(:overload) + expect(Dotenv::Rails.instance).to receive(:overload) ActiveSupport.run_load_hooks(:before_configuration) end end end context "load" do - before { Dotenv::Railtie.load } + before { Dotenv::Rails.load } it "watches .env with Spring" do expect(Spring.watcher.items).to include(Rails.root.join(".env").to_s) @@ -95,8 +95,8 @@ def add(*items) it "loads configured files" do expect(Dotenv).to receive(:load).with("custom.env") - Dotenv::Railtie.config.dotenv.files = ["custom.env"] - Dotenv::Railtie.load + Dotenv::Rails.config.dotenv.files = ["custom.env"] + Dotenv::Rails.load end context "when Rails.root is nil" do @@ -106,13 +106,13 @@ def add(*items) it "falls back to RAILS_ROOT" do ENV["RAILS_ROOT"] = "/tmp" - expect(Dotenv::Railtie.root.to_s).to eql("/tmp") + expect(Dotenv::Rails.root.to_s).to eql("/tmp") end end end context "overload" do - before { Dotenv::Railtie.overload } + before { Dotenv::Rails.overload } it "overloads .env with .env.test" do expect(ENV["DOTENV"]).to eql("test") @@ -120,12 +120,12 @@ def add(*items) it "loads configured files" do expect(Dotenv).to receive(:overload).with("custom.env") - Dotenv::Railtie.config.dotenv.files = ["custom.env"] - Dotenv::Railtie.overload + Dotenv::Rails.config.dotenv.files = ["custom.env"] + Dotenv::Rails.overload end context "when loading a file containing already set variables" do - subject { Dotenv::Railtie.overload } + subject { Dotenv::Rails.overload } it "overrides any existing ENV variables" do ENV["DOTENV"] = "predefined" From 6ed39f396991a55b38a65fea089651b2665a7e17 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 12:34:51 -0500 Subject: [PATCH 4/8] Refactor Rails integration --- lib/dotenv/rails.rb | 35 +++++++++++++++++------------------ spec/dotenv/rails_spec.rb | 31 +++++++++---------------------- 2 files changed, 26 insertions(+), 40 deletions(-) diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb index d8a33552..4b4569e5 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -12,23 +12,18 @@ end module Dotenv - # Dotenv Railtie for using Dotenv to load environment from a file into - # Rails applications + # Rails integration for using Dotenv to load ENV variables from a file class Rails < ::Rails::Railtie - def self.deprecator # :nodoc: - @deprecator ||= ActiveSupport::Deprecation.new - end + attr_accessor :mode, :files def initialize - config.dotenv = ActiveSupport::OrderedOptions.new.merge!( - mode: :load, - files: [ - root.join(".env.#{env}.local"), - (root.join(".env.local") unless env.test?), - root.join(".env.#{env}"), - root.join(".env") - ].compact - ) + @mode = :load + @files = [ + root.join(".env.#{env}.local"), + (root.join(".env.local") unless env.test?), + root.join(".env.#{env}"), + root.join(".env") + ].compact end # Public: Load dotenv @@ -36,14 +31,14 @@ def initialize # This will get called during the `before_configuration` callback, but you # can manually call `Dotenv::Railtie.load` if you needed it sooner. def load - Dotenv.load(*config.dotenv.files) + Dotenv.load(*files) end # Public: Reload dotenv # # Same as `load`, but will override existing values in `ENV` def overload - Dotenv.overload(*config.dotenv.files) + Dotenv.overload(*files) end # Internal: `Rails.root` is nil in Rails 4.1 before the application is @@ -72,6 +67,10 @@ def env env end + def deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new + end + # Rails uses `#method_missing` to delegate all class methods to the # instance, which means `Kernel#load` gets called here. We don't want that. def self.load @@ -79,12 +78,12 @@ def self.load end initializer "dotenv.deprecator" do |app| - app.deprecators[:dotenv] = Dotenv::Railtie.deprecator + app.deprecators[:dotenv] = deprecator end config.before_configuration do Dotenv.instrumenter = ActiveSupport::Notifications - config.dotenv.mode == :load ? load : overload + mode == :load ? load : overload end end diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb index 30f99eb7..46176432 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -2,19 +2,6 @@ require "rails" require "dotenv/rails" -# Fake watcher for Spring -class SpecWatcher - attr_reader :items - - def initialize - @items = [] - end - - def add(*items) - @items |= items - end -end - describe Dotenv::Rails do before do # Remove the singleton instance if it exists @@ -23,7 +10,7 @@ def add(*items) Rails.env = "test" allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join('../fixtures') Rails.application = double(:application) - Spring.watcher = SpecWatcher.new + Spring.watcher = Set.new # Responds to #add end after do @@ -32,11 +19,11 @@ def add(*items) Rails.application = nil end - describe "config.dotenv.files" do + describe "files" do it "loads files for development environment" do Rails.env = "development" - expect(Dotenv::Rails.config.dotenv.files).to eql( + expect(Dotenv::Rails.files).to eql( [ Rails.root.join(".env.development.local"), Rails.root.join(".env.local"), @@ -48,7 +35,7 @@ def add(*items) it "does not load .env.local in test rails environment" do Rails.env = "test" - expect(Dotenv::Rails.config.dotenv.files).to eql( + expect(Dotenv::Rails.files).to eql( [ Rails.root.join(".env.test.local"), Rails.root.join(".env.test"), @@ -67,7 +54,7 @@ def add(*items) end context "with mode = :overload" do - before { Dotenv::Rails.config.dotenv.mode = :overload } + before { Dotenv::Rails.mode = :overload } it "calls #overload" do expect(Dotenv::Rails.instance).to receive(:overload) @@ -80,13 +67,13 @@ def add(*items) before { Dotenv::Rails.load } it "watches .env with Spring" do - expect(Spring.watcher.items).to include(Rails.root.join(".env").to_s) + expect(Spring.watcher).to include(Rails.root.join(".env").to_s) end it "watches other loaded files with Spring" do path = fixture_path("plain.env") Dotenv.load(path) - expect(Spring.watcher.items).to include(path) + expect(Spring.watcher).to include(path) end it "loads .env.test before .env" do @@ -95,7 +82,7 @@ def add(*items) it "loads configured files" do expect(Dotenv).to receive(:load).with("custom.env") - Dotenv::Rails.config.dotenv.files = ["custom.env"] + Dotenv::Rails.files = ["custom.env"] Dotenv::Rails.load end @@ -120,7 +107,7 @@ def add(*items) it "loads configured files" do expect(Dotenv).to receive(:overload).with("custom.env") - Dotenv::Rails.config.dotenv.files = ["custom.env"] + Dotenv::Rails.files = ["custom.env"] Dotenv::Rails.overload end From ee55464340ad38bf0b11123e20de502d6d703c86 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 12:45:06 -0500 Subject: [PATCH 5/8] Include Rails integration in `dotenv` gem --- README.md | 12 ++++++------ dotenv-rails.gemspec | 4 +--- dotenv.gemspec | 3 +-- lib/dotenv.rb | 2 ++ lib/dotenv/load.rb | 3 ++- lib/dotenv/rails-now.rb | 10 +++++----- lib/dotenv/rails.rb | 4 ++-- spec/dotenv/rails_spec.rb | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4a83718c..c83bdf44 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ But it is not always practical to set environment variables on development machi Add this line to the top of your application's Gemfile: ```ruby -gem 'dotenv-rails', groups: [:development, :test] +gem 'dotenv', groups: [:development, :test] ``` And then execute: @@ -24,7 +24,7 @@ $ bundle #### Note on load order -dotenv is initialized in your Rails app during the `before_configuration` callback, which is fired when the `Application` constant is defined in `config/application.rb` with `class Application < Rails::Application`. If you need it to be initialized sooner, you can manually call `Dotenv::Railtie.load`. +dotenv is initialized in your Rails app during the `before_configuration` callback, which is fired when the `Application` constant is defined in `config/application.rb` with `class Application < Rails::Application`. If you need it to be initialized sooner, you can manually call `Dotenv::Rails.load`. ```ruby # config/application.rb @@ -32,16 +32,16 @@ Bundler.require(*Rails.groups) # Load dotenv only in development or test environment if ['development', 'test'].include? ENV['RAILS_ENV'] - Dotenv::Railtie.load + Dotenv::Rails.load end HOSTNAME = ENV['HOSTNAME'] ``` -If you use gems that require environment variables to be set before they are loaded, then list `dotenv-rails` in the `Gemfile` before those other gems and require `dotenv/rails-now`. +If you use gems that require environment variables to be set before they are loaded, then list `dotenv` in the `Gemfile` before those other gems and require `dotenv/load`. ```ruby -gem 'dotenv-rails', require: 'dotenv/rails-now' +gem 'dotenv', require: 'dotenv/load' gem 'gem-that-requires-env-variables' ``` @@ -94,7 +94,7 @@ To ensure `.env` is loaded in rake, load the tasks: require 'dotenv/tasks' task mytask: :dotenv do - # things that require .env + # things that require .env end ``` diff --git a/dotenv-rails.gemspec b/dotenv-rails.gemspec index 9cb4be41..7c16c647 100644 --- a/dotenv-rails.gemspec +++ b/dotenv-rails.gemspec @@ -7,9 +7,7 @@ Gem::Specification.new "dotenv-rails", Dotenv::VERSION do |gem| gem.description = gem.summary = "Autoload dotenv in Rails." gem.homepage = "https://github.com/bkeepers/dotenv" gem.license = "MIT" - gem.files = `git ls-files lib | grep rails`.split( - $OUTPUT_RECORD_SEPARATOR - ) + ["README.md", "LICENSE"] + gem.files = `git ls-files lib | grep dotenv-rails.rb`.split("\n") + ["README.md", "LICENSE"] gem.add_dependency "dotenv", Dotenv::VERSION gem.add_dependency "railties", ">= 3.2" diff --git a/dotenv.gemspec b/dotenv.gemspec index 888faf09..cf4bbce9 100644 --- a/dotenv.gemspec +++ b/dotenv.gemspec @@ -8,8 +8,7 @@ Gem::Specification.new "dotenv", Dotenv::VERSION do |gem| gem.homepage = "https://github.com/bkeepers/dotenv" gem.license = "MIT" - gem_files = `git ls-files README.md LICENSE lib bin | grep -v rails` - gem.files = gem_files.split($OUTPUT_RECORD_SEPARATOR) + gem.files = `git ls-files README.md LICENSE lib bin | grep -v dotenv-rails.rb`.split("\n") gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.add_development_dependency "rake" diff --git a/lib/dotenv.rb b/lib/dotenv.rb index 92166f8a..31d6eb8c 100644 --- a/lib/dotenv.rb +++ b/lib/dotenv.rb @@ -69,3 +69,5 @@ def require_keys(*keys) raise MissingKeys, missing_keys end end + +require "dotenv/rails" if defined?(Rails::Railtie) diff --git a/lib/dotenv/load.rb b/lib/dotenv/load.rb index 7ad8fef3..1a90192b 100644 --- a/lib/dotenv/load.rb +++ b/lib/dotenv/load.rb @@ -1,2 +1,3 @@ require "dotenv" -Dotenv.load + +defined?(Dotenv::Rails) ? Dotenv::Rails.load : Dotenv.load diff --git a/lib/dotenv/rails-now.rb b/lib/dotenv/rails-now.rb index 4e01174a..391a0919 100644 --- a/lib/dotenv/rails-now.rb +++ b/lib/dotenv/rails-now.rb @@ -1,10 +1,10 @@ # If you use gems that require environment variables to be set before they are -# loaded, then list `dotenv-rails` in the `Gemfile` before those other gems and -# require `dotenv/rails-now`. +# loaded, then list `dotenv` in the `Gemfile` before those other gems and +# require `dotenv/load`. # -# gem "dotenv-rails", require: "dotenv/rails-now" +# gem "dotenv", require: "dotenv/load" # gem "gem-that-requires-env-variables" # -require "dotenv/rails" -Dotenv::Railtie.load +require "dotenv/load" +warn '[DEPRECATION] `require "dotenv/rails-now"` is deprecated. Use `require "dotenv/load"` instead.', caller(1..1).first diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb index 4b4569e5..729b41a5 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -29,7 +29,7 @@ def initialize # Public: Load dotenv # # This will get called during the `before_configuration` callback, but you - # can manually call `Dotenv::Railtie.load` if you needed it sooner. + # can manually call `Dotenv::Rails.load` if you needed it sooner. def load Dotenv.load(*files) end @@ -83,7 +83,7 @@ def self.load config.before_configuration do Dotenv.instrumenter = ActiveSupport::Notifications - mode == :load ? load : overload + (mode == :load) ? load : overload end end diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb index 46176432..96727a0f 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -8,7 +8,7 @@ Dotenv::Rails.remove_instance_variable(:@instance) Rails.env = "test" - allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join('../fixtures') + allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join("../fixtures") Rails.application = double(:application) Spring.watcher = Set.new # Responds to #add end From 706b32ac0a7cdfe5fc7fd0a4623011e3e4612ef9 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 14:34:57 -0500 Subject: [PATCH 6/8] Use `Dotenv::Rails.overwrite = true` instead of overload --- lib/dotenv/rails.rb | 17 ++++---- spec/dotenv/rails_spec.rb | 86 ++++++++++++++++----------------------- spec/spec_helper.rb | 2 +- 3 files changed, 43 insertions(+), 62 deletions(-) diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb index 729b41a5..b8bbe5c6 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -1,5 +1,7 @@ require "dotenv" +Dotenv.instrumenter = ActiveSupport::Notifications + # Watch all loaded env files with Spring begin require "spring/commands" @@ -14,10 +16,10 @@ module Dotenv # Rails integration for using Dotenv to load ENV variables from a file class Rails < ::Rails::Railtie - attr_accessor :mode, :files + attr_accessor :overwrite, :files def initialize - @mode = :load + @overwrite = false @files = [ root.join(".env.#{env}.local"), (root.join(".env.local") unless env.test?), @@ -31,13 +33,11 @@ def initialize # This will get called during the `before_configuration` callback, but you # can manually call `Dotenv::Rails.load` if you needed it sooner. def load - Dotenv.load(*files) + Dotenv.load(*files, overwrite: overwrite) end - # Public: Reload dotenv - # - # Same as `load`, but will override existing values in `ENV` def overload + deprecator.warn("Dotenv::Rails.overload is deprecated. Set `Dotenv::Rails.overwrite = true` and call Dotenv::Rails.load instead.") Dotenv.overload(*files) end @@ -81,10 +81,7 @@ def self.load app.deprecators[:dotenv] = deprecator end - config.before_configuration do - Dotenv.instrumenter = ActiveSupport::Notifications - (mode == :load) ? load : overload - end + config.before_configuration { load } end Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Dotenv::Railtie", "Dotenv::Rails", Dotenv::Rails.deprecator) diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb index 96727a0f..1bbb6278 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -5,7 +5,7 @@ describe Dotenv::Rails do before do # Remove the singleton instance if it exists - Dotenv::Rails.remove_instance_variable(:@instance) + Dotenv::Rails.remove_instance_variable(:@instance) rescue nil Rails.env = "test" allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join("../fixtures") @@ -45,81 +45,65 @@ end end - context "before_configuration" do - context "with mode = :load" do - it "calls #load" do - expect(Dotenv::Rails.instance).to receive(:load) - ActiveSupport.run_load_hooks(:before_configuration) - end - end - - context "with mode = :overload" do - before { Dotenv::Rails.mode = :overload } + it "watches loaded files with Spring" do + path = fixture_path("plain.env") + Dotenv.load(path) + expect(Spring.watcher).to include(path.to_s) + end - it "calls #overload" do - expect(Dotenv::Rails.instance).to receive(:overload) - ActiveSupport.run_load_hooks(:before_configuration) - end + context "before_configuration" do + it "calls #load" do + expect(Dotenv::Rails).to receive(:load) + ActiveSupport.run_load_hooks(:before_configuration, ) end end context "load" do - before { Dotenv::Rails.load } + subject { Dotenv::Rails.load } it "watches .env with Spring" do - expect(Spring.watcher).to include(Rails.root.join(".env").to_s) - end - - it "watches other loaded files with Spring" do - path = fixture_path("plain.env") - Dotenv.load(path) - expect(Spring.watcher).to include(path) + subject + expect(Spring.watcher).to include(fixture_path(".env").to_s) end it "loads .env.test before .env" do + subject expect(ENV["DOTENV"]).to eql("test") end it "loads configured files" do - expect(Dotenv).to receive(:load).with("custom.env") - Dotenv::Rails.files = ["custom.env"] - Dotenv::Rails.load + Dotenv::Rails.files = [fixture_path("plain.env")] + expect { subject }.to change { ENV["PLAIN"] }.from(nil).to("true") end - context "when Rails.root is nil" do - before do - allow(Rails).to receive(:root).and_return(nil) + context "with overwrite = true" do + before { Dotenv::Rails.overwrite = true } + + it "overwrites .env with .env.test" do + subject + expect(ENV["DOTENV"]).to eql("test") end - it "falls back to RAILS_ROOT" do - ENV["RAILS_ROOT"] = "/tmp" - expect(Dotenv::Rails.root.to_s).to eql("/tmp") + it "overrides any existing ENV variables" do + ENV["DOTENV"] = "predefined" + expect { subject }.to(change { ENV["DOTENV"] }.from("predefined").to("test")) end end end - context "overload" do - before { Dotenv::Rails.overload } - - it "overloads .env with .env.test" do - expect(ENV["DOTENV"]).to eql("test") - end - - it "loads configured files" do - expect(Dotenv).to receive(:overload).with("custom.env") - Dotenv::Rails.files = ["custom.env"] - Dotenv::Rails.overload + describe "root" do + it "returns Rails.root" do + expect(Dotenv::Rails.root).to eql(Rails.root) end - context "when loading a file containing already set variables" do - subject { Dotenv::Rails.overload } - - it "overrides any existing ENV variables" do - ENV["DOTENV"] = "predefined" + context "when Rails.root is nil" do + before do + allow(Rails).to receive(:root).and_return(nil) + end - expect do - subject - end.to(change { ENV["DOTENV"] }.from("predefined").to("test")) + it "falls back to RAILS_ROOT" do + ENV["RAILS_ROOT"] = "/tmp" + expect(Dotenv::Rails.root.to_s).to eql("/tmp") end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a0accd29..8d1cdcec 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,5 +7,5 @@ end def fixture_path(name) - File.join(File.expand_path("../fixtures", __FILE__), name) + Pathname.new(__dir__).join("./fixtures", name) end From 1350739979a626d242944631fc897358e84d21d0 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 14:36:03 -0500 Subject: [PATCH 7/8] Update and reorganize README --- README.md | 169 ++++++++++++++++++++++++++---------------------------- 1 file changed, 82 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index c83bdf44..330927f9 100644 --- a/README.md +++ b/README.md @@ -8,52 +8,34 @@ But it is not always practical to set environment variables on development machi ## Installation -### Rails - -Add this line to the top of your application's Gemfile: +Add this line to the top of your application's Gemfile and run `bundle install`: ```ruby gem 'dotenv', groups: [:development, :test] ``` -And then execute: - -```console -$ bundle -``` - -#### Note on load order - -dotenv is initialized in your Rails app during the `before_configuration` callback, which is fired when the `Application` constant is defined in `config/application.rb` with `class Application < Rails::Application`. If you need it to be initialized sooner, you can manually call `Dotenv::Rails.load`. - -```ruby -# config/application.rb -Bundler.require(*Rails.groups) +## Usage -# Load dotenv only in development or test environment -if ['development', 'test'].include? ENV['RAILS_ENV'] - Dotenv::Rails.load -end +Add your application configuration to your `.env` file in the root of your project: -HOSTNAME = ENV['HOSTNAME'] +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE ``` -If you use gems that require environment variables to be set before they are loaded, then list `dotenv` in the `Gemfile` before those other gems and require `dotenv/load`. +Whenever your application loads, these variables will be available in `ENV`: ```ruby -gem 'dotenv', require: 'dotenv/load' -gem 'gem-that-requires-env-variables' +config.fog_directory = ENV['S3_BUCKET'] ``` -### Sinatra or Plain ol' Ruby +### Rails -Install the gem: +Dotenv will automatically load when your Rails app boots. See [Customizing Rails](#customizing-rails) to change which files are loaded and when. -```console -$ gem install dotenv -``` +### Sinatra / Ruby -As early as possible in your application bootstrap process, load `.env`: +Load Dotenv as early as possible in your application bootstrap process: ```ruby require 'dotenv/load' @@ -70,7 +52,21 @@ require 'dotenv' Dotenv.load('file1.env', 'file2.env') ``` -Alternatively, you can use the `dotenv` executable to launch your application: +### Rake + +To ensure `.env` is loaded in rake, load the tasks: + +```ruby +require 'dotenv/tasks' + +task mytask: :dotenv do + # things that require .env +end +``` + +### CLI + +You can use the `dotenv` executable load `.env` before launching your application: ```console $ dotenv ./script.rb @@ -88,47 +84,47 @@ The `dotenv` executable can optionally ignore missing files with the `-i` or `-- $ dotenv -i -f ".env.local,.env" ./script.rb ``` -To ensure `.env` is loaded in rake, load the tasks: +### Load Order -```ruby -require 'dotenv/tasks' +If you use gems that require environment variables to be set before they are loaded, then list `dotenv` in the `Gemfile` before those other gems and require `dotenv/load`. -task mytask: :dotenv do - # things that require .env -end +```ruby +gem 'dotenv', require: 'dotenv/load' +gem 'gem-that-requires-env-variables' ``` -## Usage +### Customizing Rails -Add your application configuration to your `.env` file in the root of your project: +Dotenv will load the following files depending on `RAILS_ENV`, with the last file listed having the highest precedence: -```shell -S3_BUCKET=YOURS3BUCKET -SECRET_KEY=YOURSECRETKEYGOESHERE -``` +* **development**: `.env`, `.env.development`, `.env.local`, `.env.development.local` +* **test**: `.env`, `.env.test`, `.env.test.local` - Note that it will **not** load `.env.local`. +* **development**: `.env`, `.env.production`, `.env.local`, `.env.production.local` -Whenever your application loads, these variables will be available in `ENV`: +These files are loaded during the `before_configuration` callback, which is fired when the `Application` constant is defined in `config/application.rb` with `class Application < Rails::Application`. If you need it to be initialized sooner, or need to customize the loading process, you can do so at the top of `application.rb` ```ruby -config.fog_directory = ENV['S3_BUCKET'] -``` +# config/application.rb +Bundler.require(*Rails.groups) -You may also add `export` in front of each line so you can `source` the file in bash: +# Load .env.local in test +Dotenv::Rails.files.unshift(".env.local") if ENV["RAILS_ENV"] == "test" -```shell -export S3_BUCKET=YOURS3BUCKET -export SECRET_KEY=YOURSECRETKEYGOESHERE +module YourApp + class Application < Rails::Application + # ... + end +end ``` -### Multi-line values +Available options: -If you need multiline variables, for example private keys, you can double quote strings and use the `\n` character for newlines: +* `Dotenv::Rails.files` - list of files to be loaded, in order of precedence. +* `Dotenv::Rails.overwrite` - Overwrite exiting `ENV` variables with contents of `.env*` files -```shell -PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n" -``` +### Multi-line values -Alternatively, multi-line values with line breaks are now supported for quoted values. +Multi-line values with line breaks must be surrounded with double quotes. ```shell PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- @@ -138,7 +134,12 @@ HkVN9... -----END DSA PRIVATE KEY-----" ``` -This is particularly helpful when using the Heroku command line plugin [`heroku-config`](https://github.com/xavdid/heroku-config) to pull configuration variables down that may have line breaks. +Prior to 3.0, dotenv would replace `\n` in quoted strings with a newline, but that behavior is deprecated. To use the old behavior, set `DOTENV_LINEBREAK_MODE=legacy` before any variables that include `\n`: + +```shell +DOTENV_LINEBREAK_MODE=legacy +PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n" +``` ### Command Substitution @@ -172,6 +173,15 @@ SECRET_KEY=YOURSECRETKEYGOESHERE # comment SECRET_HASH="something-with-a-#-hash" ``` +### Exports + +For compatability, you may also add `export` in front of each line so you can `source` the file in bash: + +```shell +export S3_BUCKET=YOURS3BUCKET +export SECRET_KEY=YOURSECRETKEYGOESHERE +``` + ### Required Keys If a particular configuration value is required but not set, it's appropriate to raise an error. @@ -197,36 +207,7 @@ Dotenv.parse(".env.local", ".env") This method returns a hash of the ENV var name/value pairs. -## Frequently Answered Questions - -### Can I use dotenv in production? - -dotenv was originally created to load configuration variables into `ENV` in *development*. There are typically better ways to manage configuration in production environments - such as `/etc/environment` managed by [Puppet](https://github.com/puppetlabs/puppet) or [Chef](https://github.com/chef/chef), `heroku config`, etc. - -However, some find dotenv to be a convenient way to configure Rails applications in staging and production environments, and you can do that by defining environment-specific files like `.env.production` or `.env.test`. - -If you use this gem to handle env vars for multiple Rails environments (development, test, production, etc.), please note that env vars that are general to all environments should be stored in `.env`. Then, environment specific env vars should be stored in `.env.`. - -### What other .env* files can I use? - -`dotenv-rails` will override in the following order (highest defined variable overrides lower): - -| Hierarchy Priority | Filename | Environment | Should I `.gitignore`it? | Notes | -| ------------------ | ------------------------ | -------------------- | --------------------------------------------------- | ------------------------------------------------------------ | -| 1st (highest) | `.env.development.local` | Development | Yes! | Local overrides of environment-specific settings. | -| 1st | `.env.test.local` | Test | Yes! | Local overrides of environment-specific settings. | -| 1st | `.env.production.local` | Production | Yes! | Local overrides of environment-specific settings. | -| 2nd | `.env.local` | Wherever the file is | Definitely. | Local overrides. This file is loaded for all environments _except_ `test`. | -| 3rd | `.env.development` | Development | No. | Shared environment-specific settings | -| 3rd | `.env.test` | Test | No. | Shared environment-specific settings | -| 3rd | `.env.production` | Production | No. | Shared environment-specific settings | -| Last | `.env` | All Environments | Depends (See [below](#should-i-commit-my-env-file)) | The Original® | - - -### Should I commit my .env file? - -Credentials should only be accessible on the machines that need access to them. Never commit sensitive information to a repository that is not needed by every development machine and server. - +### Templates You can use the `-t` or `--template` flag on the dotenv cli to create a template of your `.env` file. @@ -251,6 +232,20 @@ S3_BUCKET=S3_BUCKET SECRET_KEY=SECRET_KEY ``` +## Frequently Answered Questions + +### Can I use dotenv in production? + +dotenv was originally created to load configuration variables into `ENV` in *development*. There are typically better ways to manage configuration in production environments - such as `/etc/environment` managed by [Puppet](https://github.com/puppetlabs/puppet) or [Chef](https://github.com/chef/chef), `heroku config`, etc. + +However, some find dotenv to be a convenient way to configure Rails applications in staging and production environments, and you can do that by defining environment-specific files like `.env.production` or `.env.test`. + +If you use this gem to handle env vars for multiple Rails environments (development, test, production, etc.), please note that env vars that are general to all environments should be stored in `.env`. Then, environment specific env vars should be stored in `.env.`. + +### Should I commit my .env file? + +Credentials should only be accessible on the machines that need access to them. Never commit sensitive information to a repository that is not needed by every development machine and server. + Personally, I prefer to commit the `.env` file with development-only settings. This makes it easy for other developers to get started on the project without compromising credentials for other environments. If you follow this advice, make sure that all the credentials for your development environment are different from your other deployments and that the development credentials do not have access to any confidential data. ### Why is it not overriding existing `ENV` variables? From 870b88311d3c2bd2ea5d2ee31e74d555ab62ee30 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sat, 20 Jan 2024 14:42:24 -0500 Subject: [PATCH 8/8] Remove instance after test --- spec/dotenv/rails_spec.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb index 1bbb6278..08c00cac 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -4,15 +4,17 @@ describe Dotenv::Rails do before do - # Remove the singleton instance if it exists - Dotenv::Rails.remove_instance_variable(:@instance) rescue nil - Rails.env = "test" allow(Rails).to receive(:root).and_return Pathname.new(__dir__).join("../fixtures") Rails.application = double(:application) Spring.watcher = Set.new # Responds to #add end + after do + # Remove the singleton instance if it exists + Dotenv::Rails.remove_instance_variable(:@instance) + end + after do # Reset Spring.watcher = nil @@ -53,8 +55,8 @@ context "before_configuration" do it "calls #load" do - expect(Dotenv::Rails).to receive(:load) - ActiveSupport.run_load_hooks(:before_configuration, ) + expect(Dotenv::Rails.instance).to receive(:load) + ActiveSupport.run_load_hooks(:before_configuration) end end