├── .bundle └── config ├── .gitignore ├── .rvmrc ├── CHANGELOG ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO ├── autotest └── discover.rb ├── heroku-rails.gemspec ├── lib ├── generators │ ├── heroku │ │ └── config_generator.rb │ └── templates │ │ ├── heroku.rake │ │ └── heroku.yml ├── heroku-rails.rb ├── heroku-rails │ ├── config.rb │ ├── railtie.rb │ └── runner.rb └── heroku │ └── rails │ └── tasks.rb └── spec ├── fixtures └── heroku-config.yml ├── heroku └── rails │ ├── heroku_config_spec.rb │ └── heroku_runner_spec.rb └── spec_helper.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- {} 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | pkg 3 | Thumbs.db 4 | tags 5 | Gemfile.lock 6 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm use 1.9.2@heroku_rails_dev -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Heroku Rails 2 | 3 | v0.4.4 4 | ============================================ 5 | Fix heroku rake 6 | Upgrade heroku authorization 7 | Fix deprecation warning 8 | 9 | v0.2.0 10 | ============================================ 11 | Added Heroku Settings tasks, and rails generators 12 | 13 | Cleaned out lots of tasks that are now handled by editing config/heroku.yml and running `rake heroku:setup` 14 | 15 | Namespaced tasks such as heroku:deploy, heroku:console, etc. The only top level task now is `rake all ...` which sets all the environments. Individual environment tasks (e.g. `rake production ...`) are still generated without a namespace. 16 | 17 | 18 | v0.0.1 19 | ============================================ 20 | Initial fork and reorganization 21 | 22 | 23 | Heroku Sans 24 | v0.2.0 25 | ============================================ 26 | 27 | Elijah Miller 28 | Fix newline error in gem manifest 29 | Improve instructions 30 | 31 | Glenn Roberts 32 | Gem-ify it 33 | Improve generation of gems manifest 34 | 35 | 36 | v0.0.1. 37 | ============================================ 38 | Initial release. 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | 4 | gem "ruby-debug", :platforms => :mri_18 5 | gem "ruby-debug19", :platforms => :mri_19 6 | 7 | gem "autotest", ">= 0" 8 | gem "growl-glue", ">= 0" 9 | gem "rdoc" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Heroku Rails License: 2 | Copyright (c) 2010 Jacques Crocker 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | Heroku Sans License: 24 | Copyright (c) 2008 Elijah Miller 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining 27 | a copy of this software and associated documentation files (the 28 | "Software"), to deal in the Software without restriction, including 29 | without limitation the rights to use, copy, modify, merge, publish, 30 | distribute, sublicense, and/or sell copies of the Software, and to 31 | permit persons to whom the Software is furnished to do so, subject to 32 | the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be 35 | included in all copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 38 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 39 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 40 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 41 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 42 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 43 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Heroku Rails 2 | ============= 3 | 4 | Easier configuration and deployment of Rails apps on Heroku 5 | 6 | Configure your Heroku environment via a YML file (config/heroku.yml) that defines all your environments, addons, and environment variables. 7 | 8 | Heroku Rails also handles asset packaging (via jammit), deployment of assets to s3 (via jammit-s3). 9 | 10 | ## Install 11 | 12 | ### Rails 3 13 | 14 | Add this to your Gemfile: 15 | 16 | group :development do 17 | gem 'heroku-rails' 18 | end 19 | 20 | ### Rails 2 21 | 22 | To install add the following to config/environment.rb: 23 | 24 | config.gem 'heroku-rails' 25 | 26 | Rake tasks are not automatically loaded from gems, so you’ll need to add the following to your Rakefile: 27 | 28 | begin 29 | require 'heroku/rails/tasks' 30 | rescue LoadError 31 | STDERR.puts "Run `rake gems:install` to install heroku-rails" 32 | end 33 | 34 | ## Configure 35 | 36 | In config/heroku.yml you will need add the Heroku apps that you would like to attach to this project. You can generate this file and edit it by running: 37 | 38 | rails generate heroku:config 39 | 40 | ### Example Configuration File 41 | 42 | apps: 43 | production: awesomeapp 44 | staging: awesomeapp-staging 45 | legacy: awesomeapp-legacy 46 | 47 | stacks: 48 | all: bamboo-mri-1.9.2 49 | legacy: bamboo-ree-1.8.7 50 | 51 | config: 52 | all: 53 | BUNDLE_WITHOUT: "test:development" 54 | production: 55 | MONGODB_URI: "mongodb://[username:password@]host1[:port1][/database]" 56 | staging: 57 | MONGODB_URI: "mongodb://[username:password@]host1[:port1][/database]" 58 | 59 | collaborators: 60 | all: 61 | - "heroku1@somedomain.com" 62 | - "heroku2@somedomain.com" 63 | 64 | domains: 65 | production: 66 | - "awesomeapp.com" 67 | - "www.awesomeapp.com" 68 | 69 | addons: 70 | all: 71 | - newrelic:bronze 72 | # add any other addons here 73 | 74 | production: 75 | - ssl:piggyback 76 | - cron:daily 77 | # list production env specific addons here 78 | 79 | 80 | ### Setting up Heroku 81 | 82 | To set heroku up (using your heroku.yml), just run. 83 | 84 | rake all heroku:setup 85 | 86 | This will create the heroku apps you have defined, and create the settings for each. 87 | 88 | Run `rake heroku:setup` every time you edit the heroku.yml. It will only make incremental changes (based on what you've added/removed). If nothing has changed in the heroku.yml since the last `heroku:setup`, then no heroku changes will be sent. 89 | 90 | 91 | ## Usage 92 | 93 | After configuring your Heroku apps you can use rake tasks to control the 94 | apps. 95 | 96 | rake production heroku:deploy 97 | 98 | A rake task with the shorthand name of each app is now available and adds that 99 | server to the list that subsequent commands will execute on. Because this list 100 | is additive, you can easily select which servers to run a command on. 101 | 102 | rake demo staging heroku:restart 103 | 104 | A special rake task 'all' is created that causes any further commands to 105 | execute on all heroku apps. 106 | 107 | Need to add remotes for each app? 108 | 109 | rake all heroku:remotes 110 | 111 | A full list of tasks provided: 112 | 113 | rake all # Select all Heroku apps for later command 114 | rake heroku:deploy # Deploys, migrates and restarts latest code. 115 | rake heroku:apps # Lists configured apps 116 | rake heroku:info # Queries the heroku status info on each app 117 | rake heroku:console # Opens a remote console 118 | rake heroku:capture # Captures a bundle on Heroku 119 | rake heroku:remotes # Add git remotes for all apps in this project 120 | rake heroku:migrate # Migrates and restarts remote servers 121 | rake heroku:restart # Restarts remote servers 122 | 123 | rake heroku:setup # runs all heroku setup scripts 124 | rake heroku:setup:addons # sets up the heroku addons 125 | rake heroku:setup:collaborators # sets up the heroku collaborators 126 | rake heroku:setup:config # sets up the heroku config env variables 127 | rake heroku:setup:domains # sets up the heroku domains 128 | rake heroku:setup:stacks # sets the correct stack for each heroku app 129 | 130 | rake heroku:db:setup # Migrates and restarts remote servers 131 | 132 | You can easily alias frequently used tasks within your application's Rakefile: 133 | 134 | task :deploy => ["heroku:deploy"] 135 | task :console => ["heroku:console"] 136 | task :capture => ["heroku:capture"] 137 | 138 | With this in place, you can be a bit more terse: 139 | 140 | rake staging console 141 | rake all deploy 142 | 143 | ### Deploy Hooks 144 | 145 | You can easily hook into the deploy process by defining any of the following rake tasks. 146 | 147 | When you ran `rails generate heroku:config`, it created a list of empty rake tasks within lib/tasks/heroku.rake. Edit these rake tasks to provide custom logic for before/after deployment. 148 | 149 | namespace :heroku do 150 | # runs before all the deploys complete 151 | task :before_deploy do 152 | 153 | end 154 | 155 | # runs before each push to a particular heroku deploy environment 156 | task :before_each_deploy do 157 | 158 | end 159 | 160 | # runs after each push to a particular heroku deploy environment 161 | task :after_each_deploy do 162 | 163 | end 164 | 165 | # runs after all the deploys complete 166 | task :after_deploy do 167 | 168 | end 169 | end 170 | 171 | 172 | ## About Heroku Rails 173 | 174 | ### Links 175 | 176 | Homepage:: 177 | 178 | Issue Tracker:: 179 | 180 | ### License 181 | 182 | License:: Copyright (c) 2010 Jacques Crocker released under the MIT license. 183 | 184 | ## Forked from Heroku Sans 185 | 186 | Heroku Rails is a fork and rewrite/reorganiziation of the heroku_sans gem. Heroku Sans is a simple and elegant set of Rake tasks for managing Heroku environments. Check out that project here: 187 | 188 | ### Heroku Sans Contributors 189 | 190 | * Elijah Miller (elijah.miller@gmail.com) 191 | * Glenn Roberts (glenn.roberts@siyelo.com) 192 | * Damien Mathieu (42@dmathieu.com) 193 | 194 | ### Heroku Sans License 195 | 196 | License:: Copyright (c) 2009 Elijah Miller , released under the MIT license. 197 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup 3 | 4 | require 'rake' 5 | require 'rubygems/package_task' 6 | 7 | gemspec = eval(File.read('heroku-rails.gemspec')) 8 | Gem::PackageTask.new(gemspec) do |pkg| 9 | pkg.gem_spec = gemspec 10 | end 11 | 12 | desc "build the gem and release it to rubygems.org" 13 | task :release => :gem do 14 | puts "Tagging #{gemspec.version}..." 15 | system "git tag -a #{gemspec.version} -m 'Tagging #{gemspec.version}'" 16 | puts "Pushing to Github..." 17 | system "git push --tags" 18 | puts "Pushing to rubygems.org..." 19 | system "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem" 20 | end 21 | 22 | require "rspec" 23 | require "rspec/core/rake_task" 24 | 25 | RSpec::Core::RakeTask.new(:spec) do |spec| 26 | spec.pattern = "spec/**/*_spec.rb" 27 | end 28 | 29 | RSpec::Core::RakeTask.new('spec:progress') do |spec| 30 | spec.rspec_opts = %w(--format progress) 31 | spec.pattern = "spec/**/*_spec.rb" 32 | end 33 | 34 | require "rdoc/task" 35 | RDoc::Task.new do |rdoc| 36 | rdoc.rdoc_dir = "rdoc" 37 | rdoc.title = "Heroku Rails #{gemspec.version}" 38 | rdoc.rdoc_files.include("README*") 39 | rdoc.rdoc_files.include("lib/**/*.rb") 40 | end 41 | 42 | 43 | task :default => :spec 44 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | √ refactor config/helpers out of tasks.rb into own file(s) 2 | √ add config setup tasks (DONE) 3 | √ add proper rails3 generator support 4 | √ add hooks to allow other precompilations before deploy 5 | - add sass/coffeescript precompile + jammit-s3 support 6 | -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | Autotest.add_discovery { "rspec2" } 2 | -------------------------------------------------------------------------------- /heroku-rails.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "heroku-rails" 3 | s.version = "0.4.4" 4 | 5 | s.authors = ["Elijah Miller", "Glenn Roberts", "Jacques Crocker"] 6 | s.summary = "Deployment and configuration tools for Heroku/Rails" 7 | s.description = "Manage multiple Heroku instances/apps for a single Rails app using Rake. It's the Capistrano for Heroku, without the suck." 8 | 9 | s.email = "railsjedi@gmail.com" 10 | s.homepage = "http://github.com/railsjedi/heroku-rails" 11 | s.rubyforge_project = "none" 12 | 13 | s.require_paths = ["lib"] 14 | s.files = Dir['lib/**/*', 15 | 'spec/**/*', 16 | 'heroku-rails.gemspec', 17 | 'Gemfile', 18 | 'Gemfile.lock', 19 | 'CHANGELOG', 20 | 'LICENSE', 21 | 'Rakefile', 22 | 'README.md', 23 | 'TODO'] 24 | 25 | s.test_files = Dir['spec/**/*'] 26 | s.rdoc_options = ["--charset=UTF-8"] 27 | s.extra_rdoc_files = [ 28 | "LICENSE", 29 | "README.md", 30 | "TODO", 31 | "CHANGELOG" 32 | ] 33 | 34 | s.add_runtime_dependency "heroku", ">= 2.15.0" 35 | s.add_development_dependency "rspec", "~> 2.0" 36 | s.add_development_dependency "rake", "~> 0.9.2" 37 | end 38 | -------------------------------------------------------------------------------- /lib/generators/heroku/config_generator.rb: -------------------------------------------------------------------------------- 1 | module Heroku 2 | module Generators 3 | class ConfigGenerator < ::Rails::Generators::Base 4 | desc "Generates a Heroku Config file at config/heroku.yml" 5 | 6 | def self.source_root 7 | @_heroku_gen_source_root ||= File.expand_path("../../templates", __FILE__) 8 | end 9 | 10 | def create_config_file 11 | template 'heroku.yml', File.join('config', "heroku.yml") 12 | end 13 | 14 | def create_rake_file 15 | template 'heroku.rake', File.join('lib', 'tasks', "heroku.rake") 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/generators/templates/heroku.rake: -------------------------------------------------------------------------------- 1 | # ### Shortcuts: uncomment these for easier to type deployments 2 | # ### e.g. rake deploy (instead of rake heroku:deploy) 3 | # ### 4 | # task :deploy => ["heroku:deploy"] 5 | # task :console => ["heroku:console"] 6 | # task :setup => ["heroku:setup"] 7 | # task :logs => ["heroku:logs"] 8 | # task :restart => ["heroku:restart"] 9 | 10 | # Heroku Deploy Callbacks 11 | namespace :heroku do 12 | 13 | # runs before all the deploys complete 14 | task :before_deploy do 15 | 16 | end 17 | 18 | # runs before each push to a particular heroku deploy environment 19 | task :before_each_deploy, [:app_name] do |t,args| 20 | 21 | end 22 | 23 | # runs after each push to a particular heroku deploy environment 24 | task :after_each_deploy, [:app_name] do |t,args| 25 | 26 | end 27 | 28 | # runs after all the deploys complete 29 | task :after_deploy do 30 | 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /lib/generators/templates/heroku.yml: -------------------------------------------------------------------------------- 1 | apps: 2 | # map the environments to your desired heroku app names 3 | # for example, 4 | # production: awesomeapp 5 | # would create the RACK_ENV=production 6 | # and deploy to http://awesomeapp.heroku.com 7 | production: awesomeapp 8 | staging: awesomeapp-staging 9 | legacy: awesomeapp-legacy 10 | 11 | stacks: 12 | # choose the stacks you want to use for each app. 13 | # the all: setting sets the default 14 | all: bamboo-mri-1.9.2 15 | 16 | # override on a per environment basis 17 | legacy: bamboo-ree-1.8.7 18 | 19 | config: 20 | # choose the configuration settings for all environments 21 | all: 22 | BUNDLE_WITHOUT: "test:development" 23 | CONFIG_VAR1: "config1" 24 | CONFIG_VAR2: "config2" 25 | 26 | # you can also override configuration settings for each environment 27 | production: 28 | CONFIG_VAR1: "config1-production" 29 | staging: 30 | CONFIG_VAR1: "config1-staging" 31 | 32 | collaborators: 33 | # Be sure to add yourself as a collaborator, otherwise your 34 | # access to the app will be revoked. 35 | all: 36 | - "my-heroku-email@somedomain.com" 37 | - "another-heroku-email@somedomain.com" 38 | 39 | domains: 40 | production: 41 | - "awesomeapp.com" 42 | - "www.awesomeapp.com" 43 | 44 | addons: 45 | all: 46 | - custom_domains:basic 47 | # add any other addons here 48 | 49 | production: 50 | - ssl:piggyback 51 | - cron:daily 52 | # - newrelic:bronze 53 | # production env specific addons here 54 | -------------------------------------------------------------------------------- /lib/heroku-rails.rb: -------------------------------------------------------------------------------- 1 | require 'heroku-rails/config' 2 | require 'heroku-rails/runner' 3 | require 'heroku-rails/railtie' if defined?(::Rails::Railtie) 4 | -------------------------------------------------------------------------------- /lib/heroku-rails/config.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module HerokuRails 4 | class Config 5 | 6 | class << self 7 | def root 8 | @heroku_rails_root || ENV["RAILS_ROOT"] || "." 9 | end 10 | def root=(root) 11 | @heroku_rails_root = root 12 | end 13 | end 14 | 15 | attr_accessor :settings 16 | 17 | def initialize(config_filepath) 18 | if File.exists?(config_filepath) 19 | self.settings = YAML.load(ERB.new(File.read(config_filepath)).result) || {} 20 | else 21 | self.settings = {} 22 | end 23 | end 24 | 25 | def apps 26 | self.settings['apps'] || {} 27 | end 28 | 29 | def app_names 30 | apps.values 31 | end 32 | 33 | def app_environments 34 | apps.keys 35 | end 36 | 37 | # return the stack setting for a particular app environment 38 | def stack(app_env) 39 | stacks = self.settings['stacks'] || {} 40 | stacks[app_env] || stacks['all'] 41 | end 42 | 43 | def rake_cmd(app_env) 44 | if self.stack(app_env) =~ /cedar/i 45 | 'heroku run rake' 46 | else 47 | 'heroku rake' 48 | end 49 | end 50 | 51 | # pull out the config setting hash for a particular app environment 52 | def config(app_env) 53 | config = self.settings['config'] || {} 54 | all = config['all'] || {} 55 | 56 | # overwrite all configs with the environment specific ones 57 | all.merge(config[app_env] || {}) 58 | end 59 | 60 | # return a list of domains for a particular app environment 61 | def domains(app_env) 62 | domains = self.settings['domains'] || {} 63 | domains[app_env] || [] 64 | end 65 | 66 | # return a list of collaborators for a particular app environment 67 | def collaborators(app_env) 68 | app_setting_list('collaborators', app_env) 69 | end 70 | 71 | # return a list of addons for a particular app environment 72 | def addons(app_env) 73 | app_setting_list('addons', app_env) 74 | end 75 | 76 | protected 77 | 78 | def app_setting_list(setting_key, app) 79 | setting = self.settings[setting_key] || {} 80 | all = setting['all'] || [] 81 | 82 | # add in collaborators from app environment to the ones defined in all 83 | (all + (setting[app] || [])).uniq 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/heroku-rails/railtie.rb: -------------------------------------------------------------------------------- 1 | module HerokuRails 2 | class Railtie < ::Rails::Railtie 3 | rake_tasks do 4 | HerokuRails::Config.root = ::Rails.root 5 | load 'heroku/rails/tasks.rb' 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/heroku-rails/runner.rb: -------------------------------------------------------------------------------- 1 | require 'heroku/client' 2 | 3 | module HerokuRails 4 | class Runner 5 | def initialize(config) 6 | @config = config 7 | @environments = [] 8 | end 9 | 10 | def authorize 11 | @heroku ||= Heroku::Auth.client 12 | end 13 | 14 | # add a specific environment to the run list 15 | def add_environment(env) 16 | @environments << env 17 | end 18 | 19 | # use all environments 20 | def all_environments 21 | @environments = @config.app_environments 22 | end 23 | 24 | # setup apps (create if necessary) 25 | def setup_apps 26 | authorize 27 | 28 | # get a list of all my current apps on Heroku (so we don't create dupes) 29 | @my_apps = @heroku.list.map{|a| a.first} 30 | 31 | each_heroku_app do |heroku_env, app_name, repo| 32 | next if @my_apps.include?(app_name) 33 | 34 | stack = @config.stack(heroku_env) 35 | stack_option = " --stack #{stack}" if stack.to_s.size > 0 36 | creation_command "heroku create #{app_name}#{stack_option} --remote #{app_name}" 37 | end 38 | end 39 | 40 | # setup the stacks for each app (migrating if necessary) 41 | def setup_stacks 42 | authorize 43 | each_heroku_app do |heroku_env, app_name, repo| 44 | # get the intended stack setting 45 | stack = @config.stack(heroku_env) 46 | 47 | # get the remote info about the app from heroku 48 | heroku_app_info = @heroku.info(app_name) || {} 49 | 50 | # if the stacks don't match, then perform a migration 51 | if stack != heroku_app_info[:stack] 52 | puts "Migrating the app: #{app_name} to the stack: #{stack}" 53 | creation_command "heroku stack:migrate #{stack} --app #{app_name}" 54 | end 55 | end 56 | end 57 | 58 | # setup the list of collaborators 59 | def setup_collaborators 60 | authorize 61 | each_heroku_app do |heroku_env, app_name, repo| 62 | # get the remote info about the app from heroku 63 | heroku_app_info = @heroku.info(app_name) || {} 64 | 65 | # get the intended list of collaborators to add 66 | collaborator_emails = @config.collaborators(heroku_env) 67 | 68 | # add current user to collaborator list (always) 69 | collaborator_emails << @heroku.user unless collaborator_emails.include?(@heroku.user) 70 | collaborator_emails << heroku_app_info[:owner] unless collaborator_emails.include?(heroku_app_info[:owner]) 71 | 72 | # get existing collaborators 73 | existing_emails = heroku_app_info[:collaborators].to_a.map{|c| c[:email]} 74 | 75 | # get the list of collaborators to delete 76 | existing_emails.each do |existing_email| 77 | # check to see if we need to delete this person 78 | unless collaborator_emails.include?(existing_email) 79 | # delete that collaborator if they arent on the approved list 80 | destroy_command "heroku sharing:remove #{existing_email} --app #{app_name}" 81 | end 82 | end 83 | 84 | # get the list of collaborators to add 85 | collaborator_emails.each do |collaborator_email| 86 | # check to see if we need to add this person 87 | unless existing_emails.include?(collaborator_email) 88 | # add the collaborator if they are not already on the server 89 | creation_command "heroku sharing:add #{collaborator_email} --app #{app_name}" 90 | end 91 | end 92 | 93 | # display the destructive commands 94 | output_destroy_commands(app_name) 95 | end 96 | end 97 | 98 | # setup configuration 99 | def setup_config 100 | authorize 101 | each_heroku_app do |heroku_env, app_name, repo| 102 | # get the configuration that we are aiming towards 103 | new_config = @config.config(heroku_env) 104 | 105 | # default RACK_ENV to the heroku_env (unless its manually set to something else) 106 | unless new_config["RACK_ENV"].to_s.length > 0 107 | new_config["RACK_ENV"] = heroku_env 108 | end 109 | 110 | # get the existing config from heroku's servers 111 | existing_config = @heroku.config_vars(app_name) || {} 112 | 113 | # find the config variables to add 114 | add_config = {} 115 | new_config.each do |new_key, new_val| 116 | add_config[new_key] = new_val unless existing_config[new_key] == new_val 117 | end 118 | 119 | # persist the changes onto heroku 120 | unless add_config.empty? 121 | # add the config 122 | set_config = "" 123 | add_config.each do |key, val| 124 | set_config << "#{key}='#{val}' " 125 | end 126 | 127 | creation_command "heroku config:add #{set_config} --app #{app_name}" 128 | end 129 | end 130 | end 131 | 132 | # setup the addons for heroku 133 | def setup_addons 134 | authorize 135 | each_heroku_app do |heroku_env, app_name, repo| 136 | # get the addons that we are aiming towards 137 | addons = @config.addons(heroku_env) 138 | 139 | # get the addons that are already on the servers 140 | existing_addons = (@heroku.installed_addons(app_name) || []).map{|a| a["name"]} 141 | 142 | # all apps need the shared database 143 | addons << "shared-database:5mb" unless addons.index("shared-database:5mb") || addons.index("shared-database:20gb") 144 | 145 | # add "custom_domains" if that addon doesnt already exist 146 | # and we have domains configured for this app 147 | addons << "custom_domains:basic" unless @config.domains(heroku_env).empty? or 148 | addons.any?{|a| a =~ /custom_domains/} or 149 | existing_addons.any?{|a| a =~ /custom_domains/} 150 | 151 | # remove the addons that need to be removed 152 | existing_addons.each do |existing_addon| 153 | # check to see if we need to delete this addon 154 | unless addons.include?(existing_addon) 155 | # delete this addon if they arent on the approved list 156 | destroy_command "heroku addons:remove #{existing_addon} --app #{app_name}" 157 | end 158 | end 159 | 160 | # add the addons that dont exist already 161 | addons.each do |addon| 162 | # check to see if we need to add this addon 163 | unless existing_addons.include?(addon) 164 | # add this addon if they are not already added 165 | creation_command "heroku addons:add #{addon} --app #{app_name}" 166 | end 167 | end 168 | 169 | # display the destructive commands 170 | output_destroy_commands(app_name) 171 | end 172 | end 173 | 174 | # setup the domains for heroku 175 | def setup_domains 176 | authorize 177 | each_heroku_app do |heroku_env, app_name, repo| 178 | # get the domains that we are aiming towards 179 | domains = @config.domains(heroku_env) 180 | 181 | # get the domains that are already on the servers 182 | existing_domains = (@heroku.list_domains(app_name) || []).map{|a| a[:domain]} 183 | 184 | # remove the domains that need to be removed 185 | existing_domains.each do |existing_domain| 186 | # check to see if we need to delete this domain 187 | unless domains.include?(existing_domain) 188 | # delete this domain if they arent on the approved list 189 | destroy_command "heroku domains:remove #{existing_domain} --app #{app_name}" 190 | end 191 | end 192 | 193 | # add the domains that dont exist already 194 | domains.each do |domain| 195 | # check to see if we need to add this domain 196 | unless existing_domains.include?(domain) 197 | # add this domain if they are not already added 198 | creation_command "heroku domains:add #{domain} --app #{app_name}" 199 | end 200 | end 201 | 202 | # display the destructive commands 203 | output_destroy_commands(app_name) 204 | end 205 | end 206 | 207 | # cycles through each configured heroku app 208 | # yields the environment name, the app name, and the repo url 209 | def each_heroku_app 210 | 211 | if @config.apps.size == 0 212 | puts "\nNo heroku apps are configured. Run: 213 | rails generate heroku:config\n\n" 214 | puts "this will generate a default config/heroku.yml that you should edit" 215 | puts "and then try running this command again" 216 | 217 | exit(1) 218 | end 219 | 220 | if @environments.blank? && @config.apps.size == 1 221 | @environments = [@config.app_environments.first] 222 | end 223 | 224 | if @environments.present? 225 | @environments.each do |heroku_env| 226 | app_name = @config.apps[heroku_env] 227 | yield(heroku_env, app_name, "git@heroku.com:#{app_name}.git") 228 | end 229 | else 230 | puts "\nYou must first specify at least one Heroku app: 231 | rake [] 232 | rake production restart 233 | rake demo staging deploy" 234 | 235 | puts "\n\nYou can use also command all Heroku apps for this project: 236 | rake all heroku:setup\n" 237 | 238 | exit(1) 239 | end 240 | end 241 | 242 | def system_with_echo(*args) 243 | puts args.join(' ') 244 | command(*args) 245 | end 246 | 247 | def creation_command(*args) 248 | system_with_echo(*args) 249 | end 250 | 251 | def destroy_command(*args) 252 | # puts args.join(' ') 253 | @destroy_commands ||= [] 254 | @destroy_commands << args.join(' ') 255 | end 256 | 257 | def output_destroy_commands(app) 258 | puts "The #{app} had a few things removed from the heroku.yml." 259 | puts "If they are no longer neccessary, then run the following commands:\n\n" 260 | (@destroy_commands || []).each do |destroy_command| 261 | puts destroy_command 262 | end 263 | puts "\n\nthese commands may cause data loss so make sure you know that these are necessary" 264 | # clear destroy commands 265 | @destroy_commands = [] 266 | end 267 | 268 | def command(*args) 269 | system(*args) 270 | end 271 | 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /lib/heroku/rails/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'heroku-rails' 2 | 3 | HEROKU_CONFIG_FILE = File.join(HerokuRails::Config.root, 'config', 'heroku.yml') 4 | HEROKU_CONFIG = HerokuRails::Config.new(HEROKU_CONFIG_FILE) 5 | HEROKU_RUNNER = HerokuRails::Runner.new(HEROKU_CONFIG) 6 | 7 | # create all the the environment specific tasks 8 | (HEROKU_CONFIG.apps).each do |heroku_env, app_name| 9 | desc "Select #{heroku_env} Heroku app for later commands" 10 | task heroku_env do 11 | 12 | # callback switch_environment 13 | @heroku_app = {:env => heroku_env, :app_name => app_name} 14 | Rake::Task["heroku:switch_environment"].reenable 15 | Rake::Task["heroku:switch_environment"].invoke 16 | 17 | HEROKU_RUNNER.add_environment(heroku_env) 18 | end 19 | end 20 | 21 | desc 'Select all Heroku apps for later command' 22 | task :all do 23 | HEROKU_RUNNER.all_environments 24 | end 25 | 26 | namespace :heroku do 27 | def system_with_echo(*args) 28 | HEROKU_RUNNER.system_with_echo(*args) 29 | end 30 | 31 | desc 'Add git remotes for all apps in this project' 32 | task :remotes => :all do 33 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 34 | system_with_echo("git remote add #{heroku_env} #{repo}") 35 | end 36 | end 37 | 38 | desc 'Lists configured apps' 39 | task :apps => :all do 40 | puts "\n" 41 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 42 | puts "#{heroku_env} maps to the Heroku app #{app_name} located at:" 43 | puts " #{repo}" 44 | puts 45 | end 46 | end 47 | 48 | desc "Get remote server information on the heroku app" 49 | task :info do 50 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 51 | system_with_echo "heroku info --app #{app_name}" 52 | puts "\n" 53 | end 54 | end 55 | 56 | desc "Deploys, migrates and restarts latest code" 57 | task :deploy => "heroku:before_deploy" do 58 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 59 | puts "\n\nDeploying to #{app_name}..." 60 | # set the current heroku_app so that callbacks can read the data 61 | @heroku_app = {:env => heroku_env, :app_name => app_name, :repo => repo} 62 | Rake::Task["heroku:before_each_deploy"].reenable 63 | Rake::Task["heroku:before_each_deploy"].invoke(app_name) 64 | 65 | rake_cmd = HEROKU_CONFIG.rake_cmd(heroku_env) 66 | 67 | branch = `git branch`.scan(/^\* (.*)\n/).flatten.first.to_s 68 | if branch.present? 69 | @git_push_arguments ||= [] 70 | system_with_echo "git push #{repo} #{@git_push_arguments.join(' ')} #{branch}:master && #{rake_cmd} --app #{app_name} db:migrate && heroku restart --app #{app_name}" 71 | else 72 | puts "Unable to determine the current git branch, please checkout the branch you'd like to deploy" 73 | exit(1) 74 | end 75 | Rake::Task["heroku:after_each_deploy"].reenable 76 | Rake::Task["heroku:after_each_deploy"].invoke(app_name) 77 | puts "\n" 78 | end 79 | Rake::Task["heroku:after_deploy"].invoke 80 | end 81 | 82 | # Callback before all deploys 83 | task :before_deploy do 84 | end 85 | 86 | # Callback after all deploys 87 | task :after_deploy do 88 | end 89 | 90 | # Callback before each deploy 91 | task :before_each_deploy, [:app_name] do |t,args| 92 | end 93 | 94 | # Callback after each deploy 95 | task :after_each_deploy, [:app_name] do |t,args| 96 | end 97 | 98 | # Callback for when we switch environment 99 | task :switch_environment do 100 | end 101 | 102 | desc "Force deploys, migrates and restarts latest code" 103 | task :force_deploy do 104 | @git_push_arguments ||= [] 105 | @git_push_arguments << '--force' 106 | Rake::Task["heroku:deploy"].execute 107 | end 108 | 109 | desc "Captures a bundle on Heroku" 110 | task :capture do 111 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 112 | system_with_echo "heroku bundles:capture --app #{app_name}" 113 | end 114 | end 115 | 116 | desc "Opens a remote console" 117 | task :console do 118 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 119 | system_with_echo "heroku console --app #{app_name}" 120 | end 121 | end 122 | 123 | desc "Shows the Heroku logs" 124 | task :logs do 125 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 126 | system_with_echo "heroku logs --app #{app_name}" 127 | end 128 | end 129 | 130 | desc "Restarts remote servers" 131 | task :restart do 132 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 133 | system_with_echo "heroku restart --app #{app_name}" 134 | end 135 | end 136 | 137 | namespace :setup do 138 | 139 | desc "Creates the apps on Heroku" 140 | task :apps do 141 | HEROKU_RUNNER.setup_apps 142 | end 143 | 144 | desc "Setup the Heroku stacks from heroku.yml config" 145 | task :stacks do 146 | HEROKU_RUNNER.setup_stacks 147 | end 148 | 149 | desc "Setup the Heroku collaborators from heroku.yml config" 150 | task :collaborators do 151 | HEROKU_RUNNER.setup_collaborators 152 | end 153 | 154 | desc "Setup the Heroku environment config variables from heroku.yml config" 155 | task :config do 156 | HEROKU_RUNNER.setup_config 157 | end 158 | 159 | desc "Setup the Heroku addons from heroku.yml config" 160 | task :addons do 161 | HEROKU_RUNNER.setup_addons 162 | end 163 | 164 | desc "Setup the Heroku domains from heroku.yml config" 165 | task :domains do 166 | HEROKU_RUNNER.setup_domains 167 | end 168 | end 169 | 170 | desc "Setup Heroku deploy environment from heroku.yml config" 171 | task :setup => [ 172 | "heroku:setup:apps", 173 | "heroku:setup:stacks", 174 | "heroku:setup:collaborators", 175 | "heroku:setup:config", 176 | "heroku:setup:addons", 177 | "heroku:setup:domains", 178 | ] 179 | 180 | namespace :db do 181 | desc "Migrates and restarts remote servers" 182 | task :migrate do 183 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 184 | rake_cmd = HEROKU_CONFIG.rake_cmd(heroku_env) 185 | system_with_echo "#{rake_cmd} --app #{app_name} db:migrate && heroku restart --app #{app_name}" 186 | end 187 | end 188 | 189 | desc "Pulls the database from heroku and stores it into db/dumps/" 190 | task :pull do 191 | HEROKU_RUNNER.each_heroku_app do |heroku_env, app_name, repo| 192 | system_with_echo "heroku pgdumps:capture --app #{app_name}" 193 | dump = `heroku pgdumps --app #{app_name}`.split("\n").last.split(" ").first 194 | system_with_echo "mkdir -p #{HerokuRails::Config.root}/db/dumps" 195 | file = "#{HerokuRails::Config.root}/db/dumps/#{dump}.sql.gz" 196 | url = `heroku pgdumps:url --app #{app_name} #{dump}`.chomp 197 | system_with_echo "wget", url, "-O", file 198 | 199 | # TODO: these are a bit distructive... 200 | # system_with_echo "rake db:drop db:create" 201 | # system_with_echo "gunzip -c #{file} | #{HerokuRails::Config.root}/script/dbconsole" 202 | # system_with_echo "rake jobs:clear" 203 | end 204 | end 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /spec/fixtures/heroku-config.yml: -------------------------------------------------------------------------------- 1 | apps: 2 | production: awesomeapp 3 | staging: awesomeapp-staging 4 | 5 | stacks: 6 | all: bamboo-mri-1.9.2 7 | staging: bamboo-ree-1.8.7 8 | 9 | config: 10 | all: 11 | BUNDLE_WITHOUT: "test:development" 12 | CONFIG_VAR1: "config1" 13 | CONFIG_VAR2: "config2" 14 | 15 | production: 16 | CONFIG_VAR1: "config1-production" 17 | 18 | staging: 19 | CONFIG_VAR1: "config1-staging" 20 | STAGING_CONFIG: "special-staging" 21 | 22 | collaborators: 23 | all: 24 | - "all-user1@somedomain.com" 25 | - "all-user2@somedomain.com" 26 | - "all-user2@somedomain.com" 27 | staging: 28 | - "staging-user@somedomain.com" 29 | production: 30 | - "production-user@somedomain.com" 31 | 32 | domains: 33 | staging: 34 | - "staging.awesomeapp.com" 35 | production: 36 | - "awesomeapp.com" 37 | - "www.awesomeapp.com" 38 | 39 | addons: 40 | all: 41 | # add any other addons here 42 | - custom_domains:basic 43 | - newrelic:bronze 44 | 45 | production: 46 | # list production env specific addons here 47 | - ssl:piggyback 48 | - cron:daily -------------------------------------------------------------------------------- /spec/heroku/rails/heroku_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module HerokuRails 4 | describe Config do 5 | before(:each) do 6 | @config = Config.new(config_path("heroku-config.yml")) 7 | end 8 | 9 | it "should read the configuration file" do 10 | @config.settings.should_not be_empty 11 | end 12 | 13 | describe "#apps" do 14 | it "should return the list of apps defined" do 15 | @config.apps.should have(2).apps 16 | @config.apps.should include("production" => "awesomeapp") 17 | @config.apps.should include("staging" => "awesomeapp-staging") 18 | end 19 | end 20 | 21 | describe "#app_names" do 22 | it "should return the list of apps defined" do 23 | @config.app_names.should have(2).names 24 | @config.app_names.should include("awesomeapp") 25 | @config.app_names.should include("awesomeapp-staging") 26 | end 27 | end 28 | 29 | describe "#app_environments" do 30 | it "should return a list of the environments defined" do 31 | @config.app_environments.should have(2).environments 32 | @config.app_environments.should include("production") 33 | @config.app_environments.should include("staging") 34 | end 35 | end 36 | 37 | describe "#stack" do 38 | it "should return the associated stack for an environment" do 39 | @config.stack("staging").should == "bamboo-ree-1.8.7" 40 | end 41 | 42 | it "should default to the all setting if not explicitly defined" do 43 | @config.stack("production").should == "bamboo-mri-1.9.2" 44 | end 45 | end 46 | 47 | describe "#config" do 48 | context "staging environment" do 49 | before(:each) do 50 | @config = @config.config("staging") 51 | end 52 | it "should include configs defined in 'staging'" do 53 | @config["STAGING_CONFIG"].should == "special-staging" 54 | end 55 | 56 | it "should include configs defined in 'all'" do 57 | @config["BUNDLE_WITHOUT"].should == "test:development" 58 | end 59 | 60 | it "should use configs defined in 'staging' ahead of configs defined in 'all'" do 61 | @config["CONFIG_VAR1"].should == "config1-staging" 62 | end 63 | end 64 | end 65 | 66 | describe "#collaborators" do 67 | context "staging environment" do 68 | before(:each) do 69 | @collaborators = @config.collaborators('staging') 70 | end 71 | 72 | it "should include the collaborators defined in 'all'" do 73 | @collaborators.should include('all-user1@somedomain.com') 74 | @collaborators.should include('all-user2@somedomain.com') 75 | @collaborators.should have(3).collaborators 76 | end 77 | 78 | it "should include collaborators defined in 'staging'" do 79 | @collaborators.should include('staging-user@somedomain.com') 80 | end 81 | 82 | it "should not include collaborators defined in 'production'" do 83 | @collaborators.should_not include('production-user@somedomain.com') 84 | end 85 | end 86 | end 87 | 88 | describe "#domains" do 89 | context "staging environment" do 90 | before(:each) do 91 | @domains = @config.domains('staging') 92 | end 93 | 94 | it "should include the domains defined in 'staging'" do 95 | @domains.should include('staging.awesomeapp.com') 96 | end 97 | 98 | it "should not include the domains defined in 'production'" do 99 | @domains.should_not include('awesomeapp.com') 100 | @domains.should_not include('www.awesomeapp.com') 101 | end 102 | end 103 | 104 | context "production environment" do 105 | it "should include the domains defined in 'production'" do 106 | @domains = @config.domains('production') 107 | @domains.should include('awesomeapp.com') 108 | @domains.should include('www.awesomeapp.com') 109 | end 110 | end 111 | end 112 | 113 | describe "#addons" do 114 | context "staging environment" do 115 | before(:each) do 116 | @addons = @config.addons('staging') 117 | end 118 | 119 | it "should include addons defined in 'all'" do 120 | @addons.should include('custom_domains:basic') 121 | @addons.should include('newrelic:bronze') 122 | end 123 | 124 | it "should not include addons defined in 'production'" do 125 | @addons.should_not include('ssl:piggyback') 126 | end 127 | end 128 | end 129 | 130 | end 131 | end -------------------------------------------------------------------------------- /spec/heroku/rails/heroku_runner_spec.rb: -------------------------------------------------------------------------------- 1 | # needz moar test! -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'heroku-rails' 2 | require 'bundler/setup' 3 | 4 | 5 | RSpec.configure do |c| 6 | # setup fixtures path 7 | c.before(:all) do 8 | @fixture_path = Pathname.new(File.join(File.dirname(__FILE__), "/fixtures")) 9 | raise "Fixture folder not found: #{@fixture_path}" unless @fixture_path.directory? 10 | end 11 | 12 | # returns the file path of a fixture setting file 13 | def config_path(filename) 14 | @fixture_path.join(filename) 15 | end 16 | 17 | end 18 | 19 | --------------------------------------------------------------------------------