diff --git a/README.md b/README.md index 4a83718..330927f 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-rails', groups: [:development, :test] +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::Railtie.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::Railtie.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-rails` in the `Gemfile` before those other gems and require `dotenv/rails-now`. +Whenever your application loads, these variables will be available in `ENV`: ```ruby -gem 'dotenv-rails', require: 'dotenv/rails-now' -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? diff --git a/dotenv-rails.gemspec b/dotenv-rails.gemspec index 9cb4be4..7c16c64 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 888faf0..cf4bbce 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 92166f8..31d6eb8 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 7ad8fef..1a90192 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 4e01174..391a091 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 c799ffb..b8bbe5c 100644 --- a/lib/dotenv/rails.rb +++ b/lib/dotenv/rails.rb @@ -1,21 +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 @@ -30,29 +14,61 @@ end module Dotenv - # Dotenv Railtie for using Dotenv to load environment from a file into - # Rails applications - class Railtie < Rails::Railtie + # Rails integration for using Dotenv to load ENV variables from a file + class Rails < ::Rails::Railtie + attr_accessor :overwrite, :files + + def initialize + @overwrite = false + @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 # # 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(*dotenv_files) + Dotenv.load(*files, overwrite: overwrite) end - # Public: Reload dotenv - # - # Same as `load`, but will override existing values in `ENV` def overload - Dotenv.overload(*dotenv_files) + deprecator.warn("Dotenv::Rails.overload is deprecated. Set `Dotenv::Rails.overwrite = true` and call Dotenv::Rails.load instead.") + Dotenv.overload(*files) end # Internal: `Rails.root` is nil in Rails 4.1 before the application is # 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 + + # 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 + + def deprecator # :nodoc: + @deprecator ||= ActiveSupport::Deprecation.new end # Rails uses `#method_missing` to delegate all class methods to the @@ -61,17 +77,12 @@ 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 + initializer "dotenv.deprecator" do |app| + app.deprecators[:dotenv] = deprecator end config.before_configuration { load } 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 1baabb0..08c00ca 100644 --- a/spec/dotenv/rails_spec.rb +++ b/spec/dotenv/rails_spec.rb @@ -2,26 +2,17 @@ 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::Railtie do +describe Dotenv::Rails do before do 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 + 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 @@ -30,39 +21,11 @@ def add(*items) Rails.application = nil end - context "before_configuration" do - it "calls #load" do - expect(Dotenv::Railtie.instance).to receive(:load) - ActiveSupport.run_load_hooks(:before_configuration) - end - end - - context "load" do - before { Dotenv::Railtie.load } - - it "watches .env with Spring" do - expect(Spring.watcher.items).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) - 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 + describe "files" do + it "loads files for development environment" do Rails.env = "development" - expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql( + + expect(Dotenv::Rails.files).to eql( [ Rails.root.join(".env.development.local"), Rails.root.join(".env.local"), @@ -72,27 +35,9 @@ def add(*items) ) end - it "loads .env.test before .env" do - expect(ENV["DOTENV"]).to eql("test") - end - - context "when Rails.root is nil" do - before do - allow(Rails).to receive(:root).and_return(nil) - end - - it "falls back to RAILS_ROOT" do - ENV["RAILS_ROOT"] = "/tmp" - expect(Dotenv::Railtie.root.to_s).to eql("/tmp") - end - end - end - - 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.env = "test" + expect(Dotenv::Rails.files).to eql( [ Rails.root.join(".env.test.local"), Rails.root.join(".env.test"), @@ -100,32 +45,67 @@ def add(*items) ] ) end + 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") - ] - ) + it "watches loaded files with Spring" do + path = fixture_path("plain.env") + Dotenv.load(path) + expect(Spring.watcher).to include(path.to_s) + end + + context "before_configuration" do + it "calls #load" do + expect(Dotenv::Rails.instance).to receive(:load) + ActiveSupport.run_load_hooks(:before_configuration) + end + end + + context "load" do + subject { Dotenv::Rails.load } + + it "watches .env with Spring" do + subject + expect(Spring.watcher).to include(fixture_path(".env").to_s) end - it "overloads .env with .env.test" do + it "loads .env.test before .env" do + subject expect(ENV["DOTENV"]).to eql("test") end - context "when loading a file containing already set variables" do - subject { Dotenv::Railtie.overload } + it "loads configured files" do + Dotenv::Rails.files = [fixture_path("plain.env")] + expect { subject }.to change { ENV["PLAIN"] }.from(nil).to("true") + end + + 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 "overrides any existing ENV variables" do ENV["DOTENV"] = "predefined" + expect { subject }.to(change { ENV["DOTENV"] }.from("predefined").to("test")) + end + end + end + + describe "root" do + it "returns Rails.root" do + expect(Dotenv::Rails.root).to eql(Rails.root) + end + + 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 a0accd2..8d1cdce 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