heroku-rails-saas 0.1.7 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.bundle/config +2 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rvmrc +21 -0
- data/CHANGELOG +27 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +94 -42
- data/README.md +49 -17
- data/data/cacert.pem +3988 -0
- data/heroku-rails-saas.gemspec +25 -0
- data/lib/generators/templates/heroku.rake +13 -12
- data/lib/heroku-rails-saas.rb +1 -4
- data/lib/heroku-rails-saas/config.rb +28 -25
- data/lib/heroku-rails-saas/displayer.rb +26 -0
- data/lib/heroku-rails-saas/helper.rb +67 -0
- data/lib/heroku-rails-saas/heroku_client.rb +54 -0
- data/lib/heroku-rails-saas/railtie.rb +8 -2
- data/lib/heroku-rails-saas/runner.rb +471 -204
- data/lib/heroku-rails-saas/version.rb +3 -0
- data/lib/heroku/rails/tasks.rb +112 -193
- data/spec/fixtures/awesomeapp.yml +7 -0
- data/spec/fixtures/example.netrc +3 -0
- data/spec/fixtures/heroku-config.yml +1 -0
- data/spec/heroku-rails-saas/config_spec.rb +214 -0
- data/spec/heroku-rails-saas/displayer_spec.rb +35 -0
- data/spec/heroku-rails-saas/helper_spec.rb +19 -0
- data/spec/heroku-rails-saas/heorku_client_spec.rb +58 -0
- data/spec/heroku-rails-saas/runner_spec.rb +64 -0
- data/spec/spec_helper.rb +4 -1
- metadata +125 -35
- data/heroku-rails.gemspec +0 -39
- data/lib/heroku-rails-saas/hash_recursive_merge.rb +0 -11
- data/spec/heroku/rails/saas/heroku_config_spec.rb +0 -189
- data/spec/heroku/rails/saas/heroku_runner_spec.rb +0 -23
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../lib/heroku-rails-saas/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "heroku-rails-saas"
|
5
|
+
gem.version = HerokuRailsSaas::VERSION
|
6
|
+
gem.authors = ["Elijah Miller", "Glenn Roberts", "Jacques Crocker", "Lance Sanchez", "Chris Trinh"]
|
7
|
+
gem.summary = "Deployment and configuration tools for Heroku/Rails"
|
8
|
+
gem.description = "Manage multiple Heroku instances/apps for a single Rails app using Rake."
|
9
|
+
gem.email = "lance.sanchez@gmail.com"
|
10
|
+
gem.homepage = "http://github.com/darkbushido/heroku-rails-saas"
|
11
|
+
gem.rubyforge_project = "none"
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.files = `git ls-files | grep -Ev '^(myapp|examples)'`.split("\n")
|
14
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
15
|
+
gem.rdoc_options = ["--charset=UTF-8"]
|
16
|
+
gem.extra_rdoc_files = ["LICENSE", "README.md", "TODO", "CHANGELOG"]
|
17
|
+
|
18
|
+
gem.add_runtime_dependency "rails"
|
19
|
+
gem.add_runtime_dependency "heroku-api", "~> 0.3.8"
|
20
|
+
gem.add_runtime_dependency "netrc", "~> 0.7.7"
|
21
|
+
gem.add_runtime_dependency "parallel", "~> 0.6.2"
|
22
|
+
gem.add_runtime_dependency "rendezvous", "~> 0.0.2"
|
23
|
+
gem.add_development_dependency "rspec", "~> 2.0"
|
24
|
+
gem.add_development_dependency "webmock", "~> 1.11.0"
|
25
|
+
end
|
@@ -2,32 +2,33 @@
|
|
2
2
|
# ### e.g. rake deploy (instead of rake heroku:deploy)
|
3
3
|
# ###
|
4
4
|
# task :deploy => ["heroku:deploy"]
|
5
|
-
# task :console => ["heroku:console"]
|
6
5
|
# task :setup => ["heroku:setup"]
|
7
6
|
# task :logs => ["heroku:logs"]
|
8
7
|
# task :restart => ["heroku:restart"]
|
9
8
|
|
10
9
|
# Heroku Deploy Callbacks
|
11
10
|
namespace :heroku do
|
12
|
-
|
13
|
-
# runs before all the deploys complete
|
11
|
+
# Runs before all the deploys complete.
|
14
12
|
task :before_deploy do
|
15
|
-
|
16
13
|
end
|
17
14
|
|
18
|
-
#
|
19
|
-
task :before_each_deploy, [:
|
20
|
-
|
15
|
+
# Runs before each push to a particular heroku deploy environment.
|
16
|
+
task :before_each_deploy, [:local_name, :remote_name, :configs] do |t, args|
|
21
17
|
end
|
22
18
|
|
23
|
-
#
|
24
|
-
task :
|
19
|
+
# Runs every time there is heroku deploy regardless of exceptions/failures.
|
20
|
+
task :ensure_each_deploy, [:local_name, :remote_name, :configs] do |t, args|
|
21
|
+
end
|
25
22
|
|
23
|
+
# Runs after each push to a particular heroku deploy environment
|
24
|
+
task :after_each_deploy, [:local_name, :remote_name, :configs] do |t, args|
|
26
25
|
end
|
27
26
|
|
28
|
-
#
|
27
|
+
# Runs after all the deploys complete
|
29
28
|
task :after_deploy do
|
30
|
-
|
31
29
|
end
|
32
30
|
|
33
|
-
|
31
|
+
# Callback for when we switch environment
|
32
|
+
task :switch_environment do
|
33
|
+
end
|
34
|
+
end
|
data/lib/heroku-rails-saas.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require 'active_support/core_ext/object/try'
|
2
|
+
require 'active_support/core_ext/hash/deep_merge'
|
2
3
|
require 'erb'
|
3
4
|
|
4
5
|
module HerokuRailsSaas
|
5
6
|
class Config
|
6
|
-
|
7
7
|
SEPERATOR = ":"
|
8
8
|
|
9
9
|
class << self
|
@@ -15,7 +15,7 @@ module HerokuRailsSaas
|
|
15
15
|
@heroku_rails_root = root
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
18
|
+
def local_name(app, env)
|
19
19
|
"#{app}#{SEPERATOR}#{env}"
|
20
20
|
end
|
21
21
|
|
@@ -27,8 +27,7 @@ module HerokuRailsSaas
|
|
27
27
|
def extract_name_from(app_env)
|
28
28
|
name, env = app_env.split(SEPERATOR)
|
29
29
|
name
|
30
|
-
end
|
31
|
-
|
30
|
+
end
|
32
31
|
end
|
33
32
|
|
34
33
|
attr_accessor :settings
|
@@ -44,22 +43,10 @@ module HerokuRailsSaas
|
|
44
43
|
def app_names
|
45
44
|
apps.keys
|
46
45
|
end
|
47
|
-
|
48
|
-
def cmd(app_env)
|
49
|
-
if self.stack(app_env) =~ /cedar/i
|
50
|
-
'heroku run '
|
51
|
-
else
|
52
|
-
'heroku '
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def rails_cli script
|
57
|
-
Rails::VERSION::MAJOR < 3 ? ".script/#{script}" : "rails #{script}"
|
58
|
-
end
|
59
46
|
|
60
|
-
# Returns the app name on heroku
|
47
|
+
# Returns the app name on heroku from a string format like so: `app:env`
|
61
48
|
# Allows for `rake <app:env> [<app:env>] <command>`
|
62
|
-
def
|
49
|
+
def heroku_app_name(string)
|
63
50
|
app_name, env = string.split(SEPERATOR)
|
64
51
|
apps[app_name][env]
|
65
52
|
end
|
@@ -67,7 +54,9 @@ module HerokuRailsSaas
|
|
67
54
|
# return all enviromnets in this format app:env
|
68
55
|
def app_environments(env_filter="")
|
69
56
|
apps.each_with_object([]) do |(app, hsh), arr|
|
70
|
-
hsh.each
|
57
|
+
hsh.each do |env, app_name|
|
58
|
+
arr << self.class.local_name(app, env) if(env_filter.nil? || env_filter.empty? || env == env_filter)
|
59
|
+
end
|
71
60
|
end
|
72
61
|
end
|
73
62
|
|
@@ -103,7 +92,6 @@ module HerokuRailsSaas
|
|
103
92
|
app_setting_hash("scale", app_env)
|
104
93
|
end
|
105
94
|
|
106
|
-
|
107
95
|
# return a list of collaborators for a particular app environment
|
108
96
|
def collaborators(app_env)
|
109
97
|
app_setting_array('collaborators', app_env)
|
@@ -111,18 +99,33 @@ module HerokuRailsSaas
|
|
111
99
|
|
112
100
|
# return a list of addons for a particular app environment
|
113
101
|
def addons(app_env)
|
114
|
-
app_setting_array('addons', app_env)
|
102
|
+
all_addons = app_setting_array('addons', app_env)
|
103
|
+
|
104
|
+
# Replace default addons tier with app specific ones.
|
105
|
+
addons = all_addons.each_with_object({}) do |addon, hash|
|
106
|
+
name, tier = addon.split(":")
|
107
|
+
hash[name] = tier
|
108
|
+
end
|
109
|
+
|
110
|
+
addons.to_a.map { |key_value| key_value.join(":") }
|
115
111
|
end
|
116
112
|
|
117
|
-
|
113
|
+
# return the region for a particular app environment
|
114
|
+
def region(app_env)
|
115
|
+
name, env = app_env.split(SEPERATOR)
|
116
|
+
stacks = self.settings['region'] || {}
|
117
|
+
stacks[name].try("[]", env) || stacks['all']
|
118
|
+
end
|
118
119
|
|
120
|
+
private
|
119
121
|
# Add app specific settings to the default ones defined in all for an array listing
|
120
122
|
def app_setting_array(setting_key, app_env)
|
121
123
|
name, env = app_env.split(SEPERATOR)
|
122
124
|
setting = self.settings[setting_key] || {}
|
123
125
|
default = setting['all'] || []
|
124
126
|
|
125
|
-
app_settings = setting[name].try("[]", env)
|
127
|
+
app_settings = Array.wrap(setting[name].try("[]", env))
|
128
|
+
|
126
129
|
(default + app_settings).uniq
|
127
130
|
end
|
128
131
|
|
@@ -159,9 +162,9 @@ module HerokuRailsSaas
|
|
159
162
|
end
|
160
163
|
|
161
164
|
def aggregate_heroku_configs(config_files)
|
162
|
-
|
165
|
+
configs = config_files[:apps].each_with_object({}) { |file, h| h.deep_merge!(parse_yml(file, :apps)) }
|
163
166
|
# overwrite all configs with the environment specific ones
|
164
|
-
|
167
|
+
configs.deep_merge!(parse_yml(config_files[:default], :default))
|
165
168
|
end
|
166
169
|
end
|
167
170
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
module HerokuRailsSaas
|
4
|
+
class Displayer
|
5
|
+
class << self
|
6
|
+
# Prepends a string output with a label consisting of the app name and a color code.
|
7
|
+
def labelize(message="", new_line=true, remote_name, color)
|
8
|
+
message = "[ #{Helper.send(color, remote_name)} ] #{message}"
|
9
|
+
message = message + "\n" if new_line && message[-1] != "\n"
|
10
|
+
$stdout.print(message)
|
11
|
+
$stdout.flush
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(remote_name, color)
|
16
|
+
@remote_name = remote_name
|
17
|
+
@color = color
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :color, :remote_name
|
21
|
+
|
22
|
+
def labelize(message="", new_line=true)
|
23
|
+
self.class.labelize(message, new_line, @remote_name, @color)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative 'displayer'
|
2
|
+
|
3
|
+
module HerokuRailsSaas
|
4
|
+
module Helper
|
5
|
+
COLORS = %w(cyan yellow green magenta red)
|
6
|
+
COLOR_CODES = {
|
7
|
+
"red" => 31,
|
8
|
+
"green" => 32,
|
9
|
+
"yellow" => 33,
|
10
|
+
"magenta" => 35,
|
11
|
+
"cyan" => 36,
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
COLORS.each do |color|
|
16
|
+
define_method(color.to_sym) { |string| colorize(string, COLOR_CODES[color]) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Implementation from https://github.com/heroku/heroku/blob/master/lib/heroku/helpers.rb for consistency.
|
20
|
+
@@kb = 1024
|
21
|
+
@@mb = 1024 * @@kb
|
22
|
+
@@gb = 1024 * @@mb
|
23
|
+
def format_bytes(amount)
|
24
|
+
amount = amount.to_i
|
25
|
+
return '(empty)' if amount == 0
|
26
|
+
return amount if amount < @@kb
|
27
|
+
return "#{(amount / @@kb).round}k" if amount < @@mb
|
28
|
+
return "#{(amount / @@mb).round}M" if amount < @@gb
|
29
|
+
return "#{(amount / @@gb).round}G"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Implementation from https://github.com/heroku/heroku/blob/master/lib/heroku/helpers.rb for consistency.
|
33
|
+
def styled_hash(hash, displayer)
|
34
|
+
max_key_length = hash.keys.map {|key| key.to_s.length}.max + 2
|
35
|
+
keys ||= hash.keys.sort {|x,y| x.to_s <=> y.to_s}
|
36
|
+
keys.each do |key|
|
37
|
+
case value = hash[key]
|
38
|
+
when Array
|
39
|
+
if value.empty?
|
40
|
+
next
|
41
|
+
else
|
42
|
+
elements = value.sort {|x,y| x.to_s <=> y.to_s}
|
43
|
+
displayer.labelize("#{key}: ".ljust(max_key_length), false)
|
44
|
+
puts elements[0]
|
45
|
+
elements[1..-1].each do |element|
|
46
|
+
displayer.labelize("#{' ' * max_key_length}#{element}")
|
47
|
+
end
|
48
|
+
if elements.length > 1
|
49
|
+
displayer.labelize
|
50
|
+
end
|
51
|
+
end
|
52
|
+
when nil
|
53
|
+
next
|
54
|
+
else
|
55
|
+
displayer.labelize("#{key}: ".ljust(max_key_length), false)
|
56
|
+
puts value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
def colorize(string, color_code)
|
63
|
+
"\e[#{color_code}m#{string}\e[0m"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'heroku-api'
|
2
|
+
require 'netrc'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative 'helper'
|
6
|
+
|
7
|
+
module HerokuRailsSaas
|
8
|
+
class HerokuClient
|
9
|
+
HEROKU_API_HOST = "api.heroku.com"
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@user, @api_token = netrc[HEROKU_API_HOST]
|
13
|
+
@heroku = Heroku::API.new(:api_key => @api_token)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :user, :heroku, :api_token
|
17
|
+
|
18
|
+
private
|
19
|
+
# Redirects method calls to the Heroku::API client, parse the JSON and returns the body.
|
20
|
+
def method_missing(method_name, *args, &block)
|
21
|
+
begin
|
22
|
+
response = @heroku.__send__(method_name.to_sym, *args, &block)
|
23
|
+
response.body
|
24
|
+
rescue Heroku::API::Errors::ErrorWithResponse => error
|
25
|
+
message = error.response.status == 404 ? "#{Helper.yellow(args[0])} does not exists" : JSON.parse(error.response.body)["error"]
|
26
|
+
status = error.response.headers["Status"]
|
27
|
+
|
28
|
+
raise <<-OUTPUT
|
29
|
+
#{Helper.red(error.class)}:
|
30
|
+
Status: #{status}
|
31
|
+
Message: #{message}
|
32
|
+
OUTPUT
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def netrc # :nodoc:
|
37
|
+
@netrc ||= begin
|
38
|
+
File.exists?(netrc_path) ? Netrc.read(netrc_path) : raise(StandardError)
|
39
|
+
rescue => error
|
40
|
+
raise ".netrc missing or no entry found. Try `heroku auth:login`"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def netrc_path # :nodoc:
|
45
|
+
default = Netrc.default_path
|
46
|
+
encrypted = default + ".gpg"
|
47
|
+
if File.exists?(encrypted)
|
48
|
+
encrypted
|
49
|
+
else
|
50
|
+
default
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -1,8 +1,14 @@
|
|
1
|
+
require_relative 'config'
|
2
|
+
require_relative 'runner'
|
3
|
+
|
1
4
|
module HerokuRailsSaas
|
2
5
|
class Railtie < ::Rails::Railtie
|
3
6
|
rake_tasks do
|
4
7
|
HerokuRailsSaas::Config.root = ::Rails.root
|
5
|
-
|
8
|
+
if ::Rails.env.development?
|
9
|
+
puts "Load heroku-rails-saas rake task"
|
10
|
+
load 'heroku/rails/tasks.rb'
|
11
|
+
end
|
6
12
|
end
|
7
13
|
end
|
8
|
-
end
|
14
|
+
end
|
@@ -1,247 +1,531 @@
|
|
1
1
|
require 'active_support/core_ext/object/blank'
|
2
|
-
require '
|
2
|
+
require 'parallel'
|
3
|
+
|
4
|
+
require_relative 'heroku_client'
|
5
|
+
require_relative 'helper'
|
6
|
+
require_relative 'displayer'
|
3
7
|
|
4
8
|
module HerokuRailsSaas
|
5
9
|
class Runner
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
DATABASE_REGEX = /heroku-postgresql|shared-database|heroku-shared-postgresql|amazon_rds/
|
13
|
+
DEFAULT_HEROKU_DOMAIN = /\.herokuapp\.com\Z/
|
14
|
+
SHARED_DATABASE_ADDON = "shared-database:5mb"
|
15
|
+
CONFIG_DELETE_MARKER = "DELETE"
|
16
|
+
LOCAL_CA_FILE = File.expand_path('../../data/cacert.pem', __FILE__)
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Returns an array of :add and :delete deltas respectively.
|
20
|
+
def deltas(local, remote)
|
21
|
+
[local - remote, remote - local]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
6
25
|
def initialize(config)
|
7
26
|
@config = config
|
8
|
-
@
|
27
|
+
@local_names = []
|
28
|
+
@displayer = nil
|
29
|
+
@assigned_colors = {}
|
9
30
|
end
|
10
31
|
|
11
|
-
|
12
|
-
|
32
|
+
def_delegator :@displayer, :labelize
|
33
|
+
|
34
|
+
# App/Environment methods
|
35
|
+
#---------------------------------------------------------------------------------------------------------------------
|
36
|
+
#
|
37
|
+
def heroku
|
38
|
+
@heroku ||= HerokuClient.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_app(local_name)
|
42
|
+
@local_names << local_name
|
13
43
|
end
|
14
44
|
|
15
|
-
|
16
|
-
|
17
|
-
@environments << env
|
45
|
+
def add_environment(environment)
|
46
|
+
@local_names = @config.app_environments(environment)
|
18
47
|
end
|
19
48
|
|
20
|
-
#
|
49
|
+
# Set filter to true to filter out all production environments.
|
21
50
|
def all_environments(filter=false)
|
22
|
-
@
|
23
|
-
filter ? @
|
51
|
+
@local_names = @config.app_environments
|
52
|
+
filter ? @local_names.reject! { |local_name| local_name[regex_for(:production)] } : @local_names
|
24
53
|
end
|
25
54
|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
55
|
+
# Setup methods
|
56
|
+
#---------------------------------------------------------------------------------------------------------------------
|
57
|
+
#
|
58
|
+
def setup_app
|
59
|
+
each_heroku_app do |local_name, remote_name|
|
60
|
+
_setup_app(local_name, remote_name)
|
61
|
+
end
|
29
62
|
end
|
30
63
|
|
31
|
-
|
32
|
-
|
33
|
-
|
64
|
+
def setup_stack
|
65
|
+
each_heroku_app do |local_name, remote_name|
|
66
|
+
remote_stack = heroku.get_stack(remote_name).select { |stack| stack["current"] }.first["name"]
|
67
|
+
local_stack = @config.stack(local_name)
|
34
68
|
|
35
|
-
|
36
|
-
|
69
|
+
if local_stack != remote_stack
|
70
|
+
puts "Migrating the app: #{remote_name} to the stack: #{local_stack}"
|
71
|
+
heroku.put_stack(remote_name, local_stack)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def setup_collaborators
|
77
|
+
each_heroku_app do |local_name, remote_name|
|
78
|
+
_setup_collaborators(local_name, remote_name)
|
79
|
+
end
|
80
|
+
end
|
37
81
|
|
38
|
-
|
39
|
-
|
82
|
+
def setup_addons
|
83
|
+
each_heroku_app do |local_name, remote_name|
|
84
|
+
_setup_addons(local_name, remote_name)
|
85
|
+
end
|
86
|
+
end
|
40
87
|
|
41
|
-
|
88
|
+
def setup_config
|
89
|
+
each_heroku_app do |local_name, remote_name|
|
90
|
+
_setup_config(local_name, remote_name)
|
91
|
+
end
|
92
|
+
end
|
42
93
|
|
43
|
-
|
94
|
+
def setup_domains
|
95
|
+
each_heroku_app do |local_name, remote_name|
|
96
|
+
_setup_domains(local_name, remote_name)
|
44
97
|
end
|
45
98
|
end
|
46
99
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
100
|
+
# Action methods
|
101
|
+
#---------------------------------------------------------------------------------------------------------------------
|
102
|
+
#
|
103
|
+
def deploy
|
104
|
+
require 'pty'
|
105
|
+
|
106
|
+
Rake::Task["heroku:before_deploy"].invoke
|
107
|
+
|
108
|
+
deploy_branch = prompt_for_branch
|
53
109
|
|
54
|
-
|
55
|
-
|
110
|
+
each_heroku_app do |local_name, remote_name|
|
111
|
+
@displayer.labelize("Deploying to #{remote_name}...")
|
112
|
+
|
113
|
+
configs = @config.config(local_name)
|
114
|
+
|
115
|
+
Rake::Task["heroku:before_each_deploy"].reenable
|
116
|
+
Rake::Task["heroku:before_each_deploy"].invoke(local_name, remote_name, configs)
|
117
|
+
|
118
|
+
begin
|
119
|
+
raise "Server not setup run `rake #{local_name} heorku:setup`" if _setup_app?(remote_name)
|
120
|
+
|
121
|
+
repo = heroku.get_app(remote_name)["git_url"]
|
122
|
+
|
123
|
+
# The use of PTY here is because we can't depend on the external process 'git' to flush its
|
124
|
+
# buffered output to STDOUT, using PTY we can mimic a terminal and trick 'git' into periodically flushing
|
125
|
+
# it's output.
|
126
|
+
# NOTE: The process bar in 'git' doesn't render correct since it tries to re-render the same line while
|
127
|
+
# other proceess are trying to do the same.
|
128
|
+
# ^0 is required so git dereferences the tag into a commit SHA (else Heroku's git server will throw up)
|
129
|
+
# See https://github.com/TerraCycleUS/heroku-rails-saas/commit/25cbcd3d79fe74e4e54297df1022a39bdd104668
|
130
|
+
PTY.spawn("git push #{repo} --force #{deploy_branch}^0:refs/heads/master") do |read_io, _, pid|
|
131
|
+
begin
|
132
|
+
read_io.sync = true
|
133
|
+
read_io.each { |line| @displayer.labelize(line) }
|
134
|
+
Process.wait(pid)
|
135
|
+
rescue Errno::EIO
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
if continue = $?.exitstatus
|
140
|
+
_maintenance(true, remote_name)
|
141
|
+
_setup_collaborators(local_name, remote_name)
|
142
|
+
_setup_addons(local_name, remote_name)
|
143
|
+
_setup_domains(local_name, remote_name)
|
144
|
+
_setup_config(local_name, remote_name)
|
145
|
+
_migrate(remote_name)
|
146
|
+
_scale(local_name, remote_name)
|
147
|
+
_restart(remote_name)
|
148
|
+
_maintenance(false, remote_name)
|
149
|
+
end
|
150
|
+
rescue Interrupt
|
151
|
+
@displayer.labelize("!!! Interrupt issued stopping deployment")
|
152
|
+
rescue Exception => error
|
153
|
+
@displayer.labelize("!!! Error deploying: #{Helper.red(error.message)}")
|
154
|
+
continue = false
|
155
|
+
ensure
|
156
|
+
Rake::Task["heroku:ensure_each_deploy"].reenable
|
157
|
+
Rake::Task["heroku:ensure_each_deploy"].invoke(local_name, remote_name, configs)
|
158
|
+
end
|
56
159
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
creation_command "heroku stack:migrate #{stack} --app #{app_name}"
|
160
|
+
if continue
|
161
|
+
Rake::Task["heroku:after_each_deploy"].reenable
|
162
|
+
Rake::Task["heroku:after_each_deploy"].invoke(local_name, remote_name, configs)
|
61
163
|
end
|
62
164
|
end
|
165
|
+
|
166
|
+
Rake::Task["heroku:after_deploy"].invoke
|
63
167
|
end
|
64
168
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
169
|
+
def exec_on_all(command)
|
170
|
+
each_heroku_app do |_, remote_name|
|
171
|
+
async_exec(remote_name, command)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Triggers an one-off process.
|
176
|
+
def async_exec(remote_name, command)
|
177
|
+
result = heroku.post_ps(remote_name, command)
|
178
|
+
@displayer.labelize("[async] " + Helper.green(result["command"]))
|
179
|
+
end
|
180
|
+
|
181
|
+
# Triggers an one-off process and waits for the job to complete.
|
182
|
+
def sync_exec(remote_name, command)
|
183
|
+
result = heroku.post_ps(remote_name, command)
|
184
|
+
process_id = result["id"]
|
185
|
+
@displayer.labelize("[sync] " + Helper.green(result["command"]))
|
186
|
+
begin
|
187
|
+
sleep 1
|
188
|
+
running_process_ids = heroku.get_ps(remote_name).map { |ps| ps["id"] }
|
189
|
+
end while running_process_ids.include?(process_id)
|
190
|
+
end
|
191
|
+
|
192
|
+
def apps
|
193
|
+
each_heroku_app do |local_name, remote_name|
|
194
|
+
repo = heroku.get_app(remote_name)["git_url"]
|
195
|
+
puts "#{Helper.red(local_name)} maps to the Heroku app #{Helper.yellow(remote_name)}:"
|
196
|
+
puts " #{Helper.green(repo)}"
|
197
|
+
puts
|
198
|
+
end
|
199
|
+
end
|
90
200
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
201
|
+
# Implementation from https://raw.github.com/heroku/heroku/master/lib/heroku/command/apps.rb to be consistent with our output.
|
202
|
+
# See Helper#styled_hash for further implemention details.
|
203
|
+
def info
|
204
|
+
each_heroku_app do |_, remote_name|
|
205
|
+
app_data = heroku.get_app(remote_name)
|
206
|
+
|
207
|
+
addons = heroku.get_addons(remote_name).map { |addon| addon['name'] }.sort
|
208
|
+
collaborators = heroku.get_collaborators(remote_name).map { |collaborator| collaborator['email'] }.sort
|
209
|
+
collaborators.reject! { |email| email == app_data['owner_email'] }
|
210
|
+
|
211
|
+
data = {:name => remote_name}
|
212
|
+
data["Addons"] = addons if addons.present?
|
213
|
+
data["Collaborators"] = collaborators
|
214
|
+
data["Create Status"] = app_data["create_status"] if app_data["create_status"] && app_data["create_status"] != "complete"
|
215
|
+
data["Database Size"] = Helper.format_bytes(app_data["database_size"]) if app_data["database_size"]
|
216
|
+
data["Git URL"] = app_data["git_url"]
|
217
|
+
data["Database Size"].gsub!('(empty)', '0K') + " in #{quantify("table", app_data["database_tables"])}" if app_data["database_tables"]
|
218
|
+
|
219
|
+
if app_data["dyno_hours"].is_a?(Hash)
|
220
|
+
data["Dyno Hours"] = app_data["dyno_hours"].keys.map do |type|
|
221
|
+
"%s - %0.2f dyno-hours" % [ type.to_s.capitalize, app_data["dyno_hours"][type] ]
|
97
222
|
end
|
98
223
|
end
|
99
224
|
|
100
|
-
|
101
|
-
|
225
|
+
data["Owner Email"] = app_data["owner_email"]
|
226
|
+
data["Region"] = app_data["region"] if app_data["region"]
|
227
|
+
data["Repo Size"] = Helper.format_bytes(app_data["repo_size"]) if app_data["repo_size"]
|
228
|
+
data["Slug Size"] = Helper.format_bytes(app_data["slug_size"]) if app_data["slug_size"]
|
229
|
+
data["Stack"] = app_data["stack"]
|
230
|
+
data.merge!("Dynos" => app_data["dynos"], "Workers" => app_data["workers"]) if data["Stack"] != "cedar"
|
231
|
+
data["Web URL"] = app_data["web_url"]
|
232
|
+
data["Tier"] = app_data["tier"].capitalize if app_data["tier"]
|
233
|
+
|
234
|
+
Helper.styled_hash(data, @displayer)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def maintenance(toggle)
|
239
|
+
each_heroku_app do |_, remote_name|
|
240
|
+
_maintenance(toggle, remote_name)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def restart
|
245
|
+
each_heroku_app do |_, remote_name|
|
246
|
+
_restart(remote_name)
|
102
247
|
end
|
103
248
|
end
|
104
249
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
new_config = @config.config(app_env)
|
111
|
-
|
112
|
-
# default RACK_ENV and RAILS_ENV to the heroku_env (unless its manually set to something else)
|
113
|
-
new_config["RACK_ENV"] = HerokuRailsSaas::Config.extract_environment_from(app_env) unless new_config["RACK_ENV"]
|
114
|
-
new_config["RAILS_ENV"] = HerokuRailsSaas::Config.extract_environment_from(app_env) unless new_config["RAILS_ENV"]
|
115
|
-
# get the existing config from heroku's servers
|
116
|
-
existing_config = @heroku.config_vars(app_name) || {}
|
117
|
-
|
118
|
-
# find the config variables to add
|
119
|
-
add_config = {}
|
120
|
-
new_config.each do |new_key, new_val|
|
121
|
-
add_config[new_key] = new_val unless existing_config[new_key] == new_val
|
122
|
-
end
|
250
|
+
def scale
|
251
|
+
each_heroku_app do |local_name, remote_name|
|
252
|
+
_scale(local_name, remote_name)
|
253
|
+
end
|
254
|
+
end
|
123
255
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
256
|
+
def logs
|
257
|
+
each_heroku_app do |_, remote_name|
|
258
|
+
_logs(remote_name)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# NOTE: This doesn't work with more than one environment. My guess is that each process triggers STDIN to flush
|
263
|
+
# its buffer causing it to act very strange. A possible solution is to have a master process (or the current rake
|
264
|
+
# process) to control the flow of input data via an IO pipe.
|
265
|
+
def console
|
266
|
+
require 'rendezvous'
|
267
|
+
|
268
|
+
each_heroku_app do |_, remote_name|
|
269
|
+
_console(remote_name)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Helper methods
|
274
|
+
#---------------------------------------------------------------------------------------------------------------------
|
275
|
+
#
|
276
|
+
# Cycles through each heroku app and yield the local app name and the heroku app name. This will fork and create another
|
277
|
+
# child process for each heorku app.
|
278
|
+
def each_heroku_app
|
279
|
+
process_heroku_command do |local_names|
|
280
|
+
$stdout.sync = true # Sync up the bufferred output.
|
132
281
|
|
133
|
-
|
134
|
-
|
282
|
+
# Preload the colors before we parallelize any commands.
|
283
|
+
local_names.each do |local_name|
|
284
|
+
@assigned_colors[local_name] ||= Helper::COLORS[@assigned_colors.size % Helper::COLORS.size]
|
285
|
+
end
|
286
|
+
|
287
|
+
# Performs work in 4 processes, each process is tasked the same command but for a different
|
288
|
+
# heroku environment.
|
289
|
+
# We use processes to get around the GIL/GVL issues.
|
290
|
+
Parallel.each(local_names, :in_processes => 4) do |local_name|
|
291
|
+
remote_name = @config.heroku_app_name(local_name)
|
292
|
+
@displayer = Displayer.new(remote_name, @assigned_colors[local_name])
|
293
|
+
yield(local_name, remote_name)
|
135
294
|
end
|
136
295
|
end
|
137
296
|
end
|
138
297
|
|
139
|
-
#
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
existing_addons.each do |existing_addon|
|
154
|
-
# check to see if we need to delete this addon
|
155
|
-
unless addons.include?(existing_addon)
|
156
|
-
# delete this addon if they arent on the approved list
|
157
|
-
destroy_command "heroku addons:remove #{existing_addon} --app #{app_name} --confirm #{app_name}"
|
158
|
-
end
|
298
|
+
# Internal methods
|
299
|
+
#---------------------------------------------------------------------------------------------------------------------
|
300
|
+
#
|
301
|
+
private
|
302
|
+
def _setup_app(local_name, remote_name)
|
303
|
+
if _setup_app?(remote_name)
|
304
|
+
params = {'name' => remote_name}
|
305
|
+
region = @config.region(local_name)
|
306
|
+
|
307
|
+
@displayer.labelize("Creating Heroku app: #{Helper.green(remote_name)}")
|
308
|
+
|
309
|
+
if region.present?
|
310
|
+
params.merge!('region' => region)
|
311
|
+
@displayer.labelize("\t Region: #{Helper.green(region)}")
|
159
312
|
end
|
160
313
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
314
|
+
heroku.post_app(params)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def _setup_app?(remote_name)
|
319
|
+
!heroku.get_apps.any? { |apps| apps["name"] == remote_name }
|
320
|
+
end
|
321
|
+
|
322
|
+
def _setup_collaborators(local_name, remote_name)
|
323
|
+
@displayer.labelize("Setting collaborators... ")
|
324
|
+
|
325
|
+
remote_collaborators = heroku.get_collaborators(remote_name).map { |collaborator| collaborator["email"] }
|
326
|
+
local_collaborators = @config.collaborators(local_name)
|
327
|
+
|
328
|
+
add_collaborators, delete_collaborators = self.class.deltas(local_collaborators, remote_collaborators)
|
329
|
+
apply(remote_name, add_collaborators, "post_collaborator", "Adding collaborator(s):")
|
330
|
+
apply(remote_name, delete_collaborators, "delete_collaborator", "Deleting collaborator(s):")
|
331
|
+
end
|
332
|
+
|
333
|
+
def _setup_addons(local_name, remote_name)
|
334
|
+
@displayer.labelize("Setting addons... ")
|
335
|
+
|
336
|
+
remote_addons = heroku.get_addons(remote_name).map { |addon| addon["name"] }
|
337
|
+
local_addons = @config.addons(local_name)
|
338
|
+
|
339
|
+
# Requires at the minimum a shared database.
|
340
|
+
local_addons << SHARED_DATABASE_ADDON unless local_addons.any? {|x| x[DATABASE_REGEX] }
|
341
|
+
|
342
|
+
add_addons, delete_addons = self.class.deltas(local_addons, remote_addons)
|
343
|
+
apply(remote_name, add_addons, "post_addon", "Adding addon(s):")
|
344
|
+
apply(remote_name, delete_addons, "delete_addon", "Deleting addon(s):")
|
345
|
+
end
|
346
|
+
|
347
|
+
def _setup_config(local_name, remote_name)
|
348
|
+
@displayer.labelize("Setting config... ")
|
349
|
+
|
350
|
+
remote_configs = heroku.get_config_vars(remote_name)
|
351
|
+
local_configs = @config.config(local_name)
|
352
|
+
|
353
|
+
delete_config_keys = []
|
354
|
+
add_configs = local_configs.delete_if do |key, value|
|
355
|
+
if value == CONFIG_DELETE_MARKER
|
356
|
+
delete_config_keys << key
|
357
|
+
elsif remote_configs.has_key?(key) && remote_configs[key] == value.to_s
|
358
|
+
true
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
perform_delete = delete_config_keys.present? &&
|
363
|
+
remote_configs.keys.any? { |key| delete_config_keys.include?(key) }
|
364
|
+
|
365
|
+
if add_configs.present?
|
366
|
+
@displayer.labelize("Adding config(s):")
|
367
|
+
add_configs.each do |key, value|
|
368
|
+
if value.is_a?(String) && value.include?("\n")
|
369
|
+
configs_values = value.split("\n")
|
370
|
+
@displayer.labelize("#{key.rjust(25)}: #{configs_values.shift}")
|
371
|
+
configs_values.each { |v| @displayer.labelize("#{''.rjust(25)} #{v}") }
|
372
|
+
else
|
373
|
+
@displayer.labelize("#{key.rjust(25)}: #{value}")
|
167
374
|
end
|
168
375
|
end
|
376
|
+
heroku.put_config_vars(remote_name, add_configs)
|
377
|
+
end
|
169
378
|
|
170
|
-
|
171
|
-
|
379
|
+
if perform_delete
|
380
|
+
@displayer.labelize("Deleting config(s):")
|
381
|
+
delete_config_keys.each do |key|
|
382
|
+
@displayer.labelize("#{key.rjust(25)}: #{remote_configs[key]}")
|
383
|
+
heroku.delete_config_var(remote_name, key)
|
384
|
+
end
|
172
385
|
end
|
173
386
|
end
|
174
387
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
388
|
+
def _setup_domains(local_name, remote_name)
|
389
|
+
@displayer.labelize("Setting domains... ")
|
390
|
+
|
391
|
+
remote_domains = heroku.get_domains(remote_name).map { |domain| domain["domain"] }
|
392
|
+
local_domains = @config.domains(local_name)
|
393
|
+
add_domains, delete_domains = self.class.deltas(local_domains, remote_domains)
|
394
|
+
|
395
|
+
delete_domains.delete_if { |domain| domain =~ DEFAULT_HEROKU_DOMAIN }
|
396
|
+
|
397
|
+
apply(remote_name, add_domains, "post_domain", "Adding domain(s):")
|
398
|
+
apply(remote_name, delete_domains, "delete_domain", "Deleting domain(s):")
|
399
|
+
end
|
400
|
+
|
401
|
+
def _maintenance(toggle, remote_name)
|
402
|
+
value = toggle ? '1' : 0
|
403
|
+
display = toggle ? Helper.green("ON") : Helper.red("OFF")
|
404
|
+
@displayer.labelize("Maintenance mode #{display}")
|
405
|
+
heroku.post_app_maintenance(remote_name, value)
|
406
|
+
end
|
407
|
+
|
408
|
+
def _restart(remote_name)
|
409
|
+
heroku.post_ps_restart(remote_name)
|
410
|
+
@displayer.labelize("Restarting... #{Helper.green('OK')}")
|
411
|
+
end
|
412
|
+
|
413
|
+
def _migrate(remote_name)
|
414
|
+
sync_exec(remote_name, "rake db:migrate")
|
415
|
+
end
|
416
|
+
|
417
|
+
def _scale(local_name, remote_name)
|
418
|
+
scaling = @config.scale(local_name)
|
419
|
+
types = scaling.keys
|
420
|
+
|
421
|
+
# Clock must be the last process to scale because it could require a worker dyno to be present
|
422
|
+
# since it can trigger a scheduling of a background job immediately after its state is up.
|
423
|
+
types << types.delete("clock")
|
424
|
+
types.each { |type| heroku.post_ps_scale(remote_name, type, scaling[type]) }
|
425
|
+
@displayer.labelize("Scaling ... #{Helper.green('OK')}")
|
426
|
+
end
|
427
|
+
|
428
|
+
def _logs(remote_name)
|
429
|
+
url = heroku.get_logs(remote_name, {:tail => 1})
|
430
|
+
uri = URI.parse(url)
|
431
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
432
|
+
|
433
|
+
if uri.scheme == 'https'
|
434
|
+
http.use_ssl = true
|
435
|
+
if ENV["HEROKU_SSL_VERIFY"] == "disable"
|
436
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
437
|
+
else
|
438
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
439
|
+
http.ca_file = LOCAL_CA_FILE
|
440
|
+
http.verify_callback = lambda do |preverify_ok, ssl_context|
|
441
|
+
if (!preverify_ok) || ssl_context.error != 0
|
442
|
+
@displayer.labelize("WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with HEROKU_SSL_VERIFY=disable")
|
443
|
+
end
|
444
|
+
true
|
191
445
|
end
|
192
446
|
end
|
447
|
+
end
|
193
448
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
449
|
+
http.read_timeout = 60 * 60 * 24
|
450
|
+
|
451
|
+
begin
|
452
|
+
http.start do
|
453
|
+
http.request_get(uri.path + (uri.query ? "?" + uri.query : "")) do |request|
|
454
|
+
request.read_body do |chunk|
|
455
|
+
chunk.split("\n").each { |line| @displayer.labelize(line) }
|
456
|
+
end
|
200
457
|
end
|
201
458
|
end
|
202
|
-
|
203
|
-
|
204
|
-
|
459
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
|
460
|
+
@displayer.labelize("Could not connect to logging service")
|
461
|
+
rescue Timeout::Error, EOFError
|
462
|
+
@displayer.labelize("\nRequest timed out")
|
205
463
|
end
|
206
464
|
end
|
207
465
|
|
208
|
-
def
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
466
|
+
def _console(remote_name)
|
467
|
+
data = heroku.post_ps(remote_name, 'console', {:attach => true})
|
468
|
+
|
469
|
+
Rendezvous.start(:url => data['rendezvous_url'])
|
470
|
+
end
|
471
|
+
|
472
|
+
# Prompt for a branch tag if any of the environments being deploy to is production grade.
|
473
|
+
# This serves as a sanity check against deploy directly to production environments.
|
474
|
+
def prompt_for_branch
|
475
|
+
deploy_branch = nil
|
476
|
+
|
477
|
+
if @local_names.any? {|local_name| local_name[regex_for(:production)] }
|
478
|
+
all_tags = `git tag`
|
479
|
+
target_tag = `git describe --tags --abbrev=0`.chomp # Set latest tag as default
|
480
|
+
|
481
|
+
begin
|
482
|
+
puts "\nGit tags:"
|
483
|
+
puts all_tags
|
484
|
+
print "\nPlease enter a tag to deploy (or hit Enter for \"#{target_tag}\"): "
|
485
|
+
input_tag = STDIN.gets.chomp
|
486
|
+
if input_tag.present?
|
487
|
+
if all_tags[/^#{input_tag}\n/].present?
|
488
|
+
target_tag = input_tag
|
489
|
+
invalid = false
|
490
|
+
else
|
491
|
+
puts "\n\nInvalid git tag!"
|
492
|
+
invalid = true
|
493
|
+
end
|
219
494
|
end
|
495
|
+
end while invalid
|
496
|
+
|
497
|
+
if target_tag.empty?
|
498
|
+
puts "Unable to determine the tag to deploy."
|
499
|
+
exit(1)
|
500
|
+
end
|
501
|
+
|
502
|
+
deploy_branch = target_tag
|
503
|
+
else
|
504
|
+
deploy_branch = `git branch`.scan(/^\* (.*)\n/).flatten.first.to_s
|
505
|
+
|
506
|
+
if deploy_branch.empty?
|
507
|
+
puts "Unable to determine the current git branch, please checkout the branch you'd like to deploy."
|
508
|
+
exit(1)
|
220
509
|
end
|
221
510
|
end
|
511
|
+
|
512
|
+
deploy_branch
|
222
513
|
end
|
223
514
|
|
224
|
-
#
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
puts "\nNo heroku apps are configured. Run:
|
229
|
-
rails generate heroku:config\n\n"
|
515
|
+
# Checks to see if there is at least one environment indicated to run commands against.
|
516
|
+
def process_heroku_command
|
517
|
+
if @config.apps.blank?
|
518
|
+
puts "\nNo heroku apps are configured. Run: rails generate heroku:config\n\n"
|
230
519
|
puts "this will generate a default config/heroku.yml that you should edit"
|
231
520
|
puts "and then try running this command again"
|
232
521
|
|
233
522
|
exit(1)
|
234
523
|
end
|
235
524
|
|
236
|
-
|
237
|
-
@environments = [all_environments(true).try(:first)].compact
|
238
|
-
end
|
525
|
+
@local_names = [all_environments(true).try(:first)].compact if @local_names.empty?
|
239
526
|
|
240
|
-
if @
|
241
|
-
@
|
242
|
-
app_name = @config.app_name_on_heroku(env)
|
243
|
-
yield(env, app_name, "git@heroku.com:#{app_name}.git")
|
244
|
-
end
|
527
|
+
if @local_names.present?
|
528
|
+
yield(@local_names)
|
245
529
|
else
|
246
530
|
puts "\nYou must first specify at least one Heroku app:
|
247
531
|
rake <app>:<environment> [<app>:<environment>] <command>
|
@@ -255,45 +539,28 @@ module HerokuRailsSaas
|
|
255
539
|
end
|
256
540
|
end
|
257
541
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
def
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
@destroy_commands ||= []
|
270
|
-
@destroy_commands << args.join(' ')
|
271
|
-
end
|
272
|
-
|
273
|
-
def output_destroy_commands(app)
|
274
|
-
if @destroy_commands.try(:any?)
|
275
|
-
puts "The #{app} had a few things removed from the heroku.yml."
|
276
|
-
puts "If they are no longer neccessary, then run the following commands:\n\n"
|
277
|
-
@destroy_commands.each do |destroy_command|
|
278
|
-
puts destroy_command
|
542
|
+
# Apply heroku configurations changes.
|
543
|
+
# app - name of the remote heroku app.
|
544
|
+
# settings - list to apply method to.
|
545
|
+
# method - name of the api method to call.
|
546
|
+
# message - display of what actions are to be performed.
|
547
|
+
def apply(app, settings, method, message)
|
548
|
+
if settings.present?
|
549
|
+
@displayer.labelize(message)
|
550
|
+
settings.each do |setting|
|
551
|
+
@displayer.labelize("\t#{setting}")
|
552
|
+
heroku.__send__(method.to_sym, app, setting)
|
279
553
|
end
|
280
|
-
puts "\n\nthese commands may cause data loss so make sure you know that these are necessary"
|
281
554
|
end
|
282
|
-
# clear destroy commands
|
283
|
-
@destroy_commands = []
|
284
555
|
end
|
285
556
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
def regex_for env
|
291
|
-
match = case env
|
557
|
+
# Returns a regex to look for a specific type of an environment.
|
558
|
+
def regex_for(environment)
|
559
|
+
match = case environment
|
292
560
|
when :production then "production|prod|live"
|
293
561
|
when :staging then "staging|stage"
|
294
562
|
end
|
295
563
|
Regexp.new("#{@config.class::SEPERATOR}(#{match})")
|
296
564
|
end
|
297
|
-
|
298
565
|
end
|
299
566
|
end
|