├── lib ├── capistrano-secrets-yml.rb └── capistrano │ ├── secrets_yml │ ├── version.rb │ ├── paths.rb │ └── helpers.rb │ ├── secrets_yml.rb │ └── tasks │ └── secrets_yml.rake ├── .gitignore ├── Rakefile ├── Gemfile ├── CHANGELOG.md ├── LICENSE.md ├── capistrano-secrets-yml.gemspec └── README.md /lib/capistrano-secrets-yml.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | Gemfile.lock 3 | *.gem -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in *.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/capistrano/secrets_yml/version.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module SecretsYml 3 | VERSION = '1.2.1' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/capistrano/secrets_yml.rb: -------------------------------------------------------------------------------- 1 | require "capistrano/secrets_yml/paths" 2 | require "capistrano/secrets_yml/helpers" 3 | load File.expand_path("../tasks/secrets_yml.rake", __FILE__) 4 | -------------------------------------------------------------------------------- /lib/capistrano/secrets_yml/paths.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | module Capistrano 4 | module SecretsYml 5 | module Paths 6 | 7 | def secrets_yml_local_path 8 | Pathname.new fetch(:secrets_yml_local_path) 9 | end 10 | 11 | def secrets_yml_remote_path 12 | shared_path.join fetch(:secrets_yml_remote_path) 13 | end 14 | 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### master 4 | - move `capistrano-secrets-yml.rb` file to the proper dir (not sure why it is 5 | required though) 6 | 7 | ### v1.0.0, 2014-10-07 8 | - added a check if `secrets.yml` is removed from git 9 | - add content to the README 10 | - improve checks for `secrets.yml` 11 | 12 | ### v0.0.1, 2014-10-07 13 | - started the project 14 | - first working version 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Bruno Sutic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /capistrano-secrets-yml.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'capistrano/secrets_yml/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = 'capistrano-secrets-yml' 8 | gem.version = Capistrano::SecretsYml::VERSION 9 | gem.authors = ['Bruno Sutic'] 10 | gem.email = ['bruno.sutic@gmail.com'] 11 | gem.description = <<-EOF.gsub(/^\s+/, '') 12 | Capistrano tasks for automating `secrets.yml` file handling for Rails 4+ apps. 13 | 14 | This plugins syncs contents of your local secrets file and copies that to 15 | the remote server. 16 | EOF 17 | gem.summary = 'Capistrano tasks for automating `secrets.yml` file handling for Rails 4+ apps.' 18 | gem.homepage = 'https://github.com/capistrano-plugins/capistrano-secrets-yml' 19 | 20 | gem.files = `git ls-files`.split($/) 21 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 22 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 23 | gem.require_paths = ['lib'] 24 | 25 | gem.license = 'MIT' 26 | 27 | gem.add_dependency 'capistrano', '>= 3.10' 28 | gem.add_dependency 'sshkit', '>= 1.17.0' 29 | gem.add_development_dependency 'rake', '~> 12.3' 30 | end 31 | -------------------------------------------------------------------------------- /lib/capistrano/secrets_yml/helpers.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | module Capistrano 4 | module SecretsYml 5 | module Helpers 6 | 7 | def local_secrets_yml(env) 8 | @local_secrets_yml ||= YAML.load_file(secrets_yml_local_path) 9 | @local_secrets_yml[env] 10 | end 11 | 12 | def secrets_yml_env 13 | fetch(:secrets_yml_env).to_s 14 | end 15 | 16 | def secrets_yml_content 17 | { secrets_yml_env => local_secrets_yml(secrets_yml_env) }.to_yaml 18 | end 19 | 20 | # error helpers 21 | 22 | def check_git_tracking_error 23 | puts 24 | puts "Error - please remove '#{fetch(:secrets_yml_local_path)}' from git:" 25 | puts 26 | puts " $ git rm --cached #{fetch(:secrets_yml_local_path)}" 27 | puts 28 | puts "and gitignore it:" 29 | puts 30 | puts " $ echo '#{fetch(:secrets_yml_local_path)}' >> .gitignore" 31 | puts 32 | end 33 | 34 | def check_config_present_error 35 | puts 36 | puts "Error - '#{secrets_yml_env}' config not present in '#{fetch(:secrets_yml_local_path)}'." 37 | puts "Please populate it." 38 | puts 39 | end 40 | 41 | def check_secrets_file_exists_error 42 | puts 43 | puts "Error - '#{fetch(:secrets_yml_local_path)}' file does not exists, and it's required." 44 | puts 45 | end 46 | 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/secrets_yml.rake: -------------------------------------------------------------------------------- 1 | include Capistrano::SecretsYml::Paths 2 | include Capistrano::SecretsYml::Helpers 3 | namespace :load do task :defaults do 4 | set :secrets_yml_local_path, "config/secrets.yml" 5 | set :secrets_yml_remote_path, "config/secrets.yml" 6 | set :secrets_yml_env, -> { fetch(:rails_env) || fetch(:stage) } 7 | end 8 | end 9 | 10 | namespace :secrets_yml do 11 | 12 | task :check_secrets_file_exists do 13 | next if File.exist?(secrets_yml_local_path) 14 | check_secrets_file_exists_error 15 | exit 1 16 | end 17 | 18 | task :check_git_tracking do 19 | next unless system("git ls-files #{fetch(:secrets_yml_local_path)} --error-unmatch >/dev/null 2>&1") 20 | check_git_tracking_error 21 | exit 1 22 | end 23 | 24 | task :check_config_present do 25 | next unless local_secrets_yml(secrets_yml_env).nil? 26 | check_config_present_error 27 | exit 1 28 | end 29 | 30 | desc "secrets.yml file checks" 31 | task :check do 32 | raise(":deploy_to in your app/config/deploy/\#{environment}.rb file cannot contain ~") if shared_path.to_s.include?('~') # SCP doesn't support ~ in the path 33 | invoke "secrets_yml:check_secrets_file_exists" 34 | invoke "secrets_yml:check_git_tracking" 35 | invoke "secrets_yml:check_config_present" 36 | end 37 | 38 | desc "Setup `secrets.yml` file on the server(s)" 39 | task setup: [:check] do 40 | content = secrets_yml_content 41 | on release_roles :all do 42 | execute :mkdir, "-pv", File.dirname(secrets_yml_remote_path) 43 | Net::SCP.upload!(self.host.hostname, self.host.user, StringIO.new(content), secrets_yml_remote_path, ssh: { port: self.host.port }) 44 | end 45 | end 46 | 47 | # Update `linked_files` after the deploy starts so that users' 48 | # `secrets_yml_remote_path` override is respected. 49 | task :secrets_yml_symlink do 50 | set :linked_files, fetch(:linked_files, []).push(fetch(:secrets_yml_remote_path)) 51 | end 52 | after "deploy:started", "secrets_yml:secrets_yml_symlink" 53 | 54 | end 55 | 56 | desc "Server setup tasks" 57 | task :setup do 58 | invoke "secrets_yml:setup" 59 | end 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capistrano::SecretsYml 2 | 3 | Capistrano tasks for handling `secrets.yml` when deploying Rails 4+ apps. 4 | 5 | ### Install 6 | 7 | Add this to `Gemfile`: 8 | 9 | group :development do 10 | gem 'capistrano', '~> 3.10.0' 11 | gem 'capistrano-secrets-yml', '~> 1.1.0' 12 | end 13 | 14 | And then: 15 | 16 | $ bundle install 17 | 18 | ### Setup and usage 19 | 20 | - Make sure your local `config/secrets.yml` is not git tracked. It **should be on 21 | the disk**, but gitignored. 22 | 23 | - Populate production secrets in local `config/secrets.yml`: 24 | 25 | production: 26 | secret_key_base: d6ced... 27 | 28 | - Add to `Capfile`: 29 | 30 | require 'capistrano/secrets_yml' 31 | 32 | - Create `secrets.yml` file on the remote server by executing this task: 33 | 34 | $ bundle exec cap production setup 35 | 36 | You can now proceed with other deployment tasks. 37 | 38 | #### What if a new config is added to secrets file? 39 | 40 | - add it to local `config/secrets.yml`: 41 | 42 | production: 43 | secret_key_base: d6ced... 44 | foobar: some_other_secret 45 | 46 | - if you're working in a team where other people have the deploy rights, compare 47 | you local `secrets.yml` with the one on the server. This is to ensure you 48 | didn't miss an update. 49 | - copy to the server: 50 | 51 | if you only need to update secrets.yml 52 | 53 | $ bundle exec cap production secrets_yml:setup 54 | 55 | or if other Capistrano plugins need to be setup 56 | 57 | $ bundle exec cap production setup 58 | 59 | - notify your colleagues that have the deploy rights that the remote 60 | `secrets.yml` has been updated so they can change their copy. 61 | 62 | 63 | ### How it works 64 | 65 | When you execute `$ bundle exec production setup`: 66 | 67 | - secrets from your local `secrets.yml` are copied to the servers in your config/deploy/{environment}.rb files using the user: value. a.
68 | - only "stage" secrets are copied: if you are deploying to `production`, 69 | only production secrets are copied there 70 | - on the server secrets file is located in `#{shared_path}/config/secrets.yml` 71 | 72 | On deployment: 73 | 74 | - secrets file is automatically symlinked to `#{current_path}/config/secrets.yml` 75 | 76 | ### Configuration 77 | 78 | None. 79 | 80 | ### More Capistrano automation? 81 | 82 | Check out [capistrano-plugins](https://github.com/capistrano-plugins) github org. 83 | 84 | ### FAQ 85 | 86 | - shouldn't we be keeping configuration in environment variables as per 87 | [12 factor app rules](http://12factor.net/config)? 88 | 89 | On Heroku, yes.
90 | With Capistrano, those env vars still have to be written somewhere on the disk 91 | and used with a tool like [dotenv](https://github.com/bkeepers/dotenv). 92 | 93 | Since we have to keep configuration on the disk anyway, it probably makes 94 | sense to use Rails 4 built-in `secrets.yml` mechanism. 95 | 96 | ### License 97 | 98 | [MIT](LICENSE.md) 99 | --------------------------------------------------------------------------------