shred 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/shred/commands/base.rb +10 -3
- data/lib/shred/commands/deploy.rb +274 -0
- data/lib/shred/version.rb +1 -1
- data/lib/shred.rb +5 -0
- data/shred.gemspec +2 -0
- data/shred.yml +13 -0
- metadata +31 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc7d3395add485fc83816532c717b9e507d2a4ca
|
4
|
+
data.tar.gz: 2e51812cfbc8b84d64c69444cb88806600e6ee9f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b78a34732cba64175900fcc65bad96316222c35478dd3fefb30ab6cf7a52eed68acd56a4823a07ae11b268f6b15c6fa353bf54553f47ed5fcfaa823815d0471
|
7
|
+
data.tar.gz: d829b8587f15cac0c577387bd15a3a2790e304bb805b7a8909d6010a6e9dc921474d96f6f5e585837bb51671b3e45e472a81464e64e444bbfffee9a3912a60ef
|
data/README.md
CHANGED
data/lib/shred/commands/base.rb
CHANGED
@@ -64,7 +64,11 @@ module Shred
|
|
64
64
|
@success_msg = success_msg
|
65
65
|
@error_msg = error_msg
|
66
66
|
@output = output
|
67
|
-
@out =
|
67
|
+
@out = if output && output.respond_to?(:write)
|
68
|
+
output
|
69
|
+
elsif output
|
70
|
+
File.open(output, 'w')
|
71
|
+
end
|
68
72
|
end
|
69
73
|
|
70
74
|
def run(&block)
|
@@ -107,6 +111,7 @@ module Shred
|
|
107
111
|
console.say_err(exit_status)
|
108
112
|
end
|
109
113
|
end
|
114
|
+
exit_status
|
110
115
|
end
|
111
116
|
end
|
112
117
|
|
@@ -150,10 +155,12 @@ module Shred
|
|
150
155
|
sub_keys = key.to_s.split('.')
|
151
156
|
value = nil
|
152
157
|
sub_keys.each_with_index do |sub_key, i|
|
153
|
-
if base_cfg.key?(sub_key)
|
158
|
+
if base_cfg && base_cfg.key?(sub_key)
|
154
159
|
value = base_cfg = base_cfg[sub_key]
|
155
160
|
elsif i < sub_keys.length - 1
|
156
|
-
raise "Missing '#{key}' config for '#{
|
161
|
+
raise "Missing '#{key}' config for '#{command_name}' command"
|
162
|
+
else
|
163
|
+
value = nil
|
157
164
|
end
|
158
165
|
end
|
159
166
|
raise "Missing '#{key}' config for '#{command_name}' command" if required && !value
|
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'shred/commands/base'
|
2
|
+
require 'dotenv'
|
3
|
+
require 'platform-api'
|
4
|
+
|
5
|
+
module Shred
|
6
|
+
module Commands
|
7
|
+
class Deploy < Base
|
8
|
+
class_option :environment
|
9
|
+
class_option :branch
|
10
|
+
|
11
|
+
desc 'all', 'Fully deploy the application by performing all deploy steps'
|
12
|
+
long_desc <<-LONGDESC
|
13
|
+
Fully deploy the application by performing all deploy steps in this order:
|
14
|
+
|
15
|
+
1. update_code_from_heroku
|
16
|
+
2. detect_pending_migrations
|
17
|
+
3. if migrations were detected,
|
18
|
+
a. maintenance_on
|
19
|
+
b. scale_down
|
20
|
+
4. push_code_to_heroku
|
21
|
+
5. if migrations were detected,
|
22
|
+
a. snapshot_db
|
23
|
+
b. migrate_db
|
24
|
+
c. scale_up
|
25
|
+
d. restart_app
|
26
|
+
e. maintenance_off
|
27
|
+
6. send_notifications
|
28
|
+
LONGDESC
|
29
|
+
def all
|
30
|
+
invoke(:update_code_from_heroku)
|
31
|
+
invoke(:detect_pending_migrations)
|
32
|
+
if migration_count > 0
|
33
|
+
maintenance_on
|
34
|
+
scale_down
|
35
|
+
end
|
36
|
+
push_code_to_heroku
|
37
|
+
if migration_count > 0
|
38
|
+
snapshot_db
|
39
|
+
migrate_db
|
40
|
+
scale_up
|
41
|
+
restart_app
|
42
|
+
maintenance_off
|
43
|
+
end
|
44
|
+
send_notifications
|
45
|
+
end
|
46
|
+
|
47
|
+
desc 'update_code_from_heroku', 'Update local copy of Heroku git remote'
|
48
|
+
def update_code_from_heroku
|
49
|
+
exit_status = run_shell_command(ShellCommand.new(
|
50
|
+
command_lines: "git remote | grep #{heroku_remote_name} > /dev/null"
|
51
|
+
))
|
52
|
+
unless exit_status.success?
|
53
|
+
run_shell_command(ShellCommand.new(
|
54
|
+
command_lines: "git remote add #{heroku_remote_name} #{heroku_info['git_url']}"
|
55
|
+
))
|
56
|
+
end
|
57
|
+
run_shell_command(ShellCommand.new(
|
58
|
+
command_lines: "git fetch #{heroku_remote_name}"
|
59
|
+
))
|
60
|
+
console.say_ok("Updated code from #{heroku_app_name} Heroku app")
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'detect_pending_migrations', 'Detect whether or not the local branch has pending migrations to apply'
|
64
|
+
def detect_pending_migrations
|
65
|
+
if migration_count > 1
|
66
|
+
console.say_ok("#{migration_count} pending database migrations detected")
|
67
|
+
elsif migration_count == 1
|
68
|
+
console.say_ok("#{migration_count} pending database migration detected")
|
69
|
+
else
|
70
|
+
console.say_ok("No pending database migrations detected")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'maintenance_on', 'Enable maintenance mode for the Heroku app'
|
75
|
+
def maintenance_on
|
76
|
+
heroku.app.update(heroku_app_name, maintenance: true)
|
77
|
+
console.say_ok("Maintenance mode enabled")
|
78
|
+
end
|
79
|
+
|
80
|
+
desc 'scale_down', 'Scale down all non-web processes'
|
81
|
+
def scale_down
|
82
|
+
updates = process_counts.each_with_object([]) do |(process_type, count), m|
|
83
|
+
m << {'process' => process_type.to_s, 'quantity' => 0} if count > 0
|
84
|
+
end
|
85
|
+
heroku.formation.batch_update(heroku_app_name, 'updates' => updates)
|
86
|
+
updated = process_counts.map do |(process_type, count)|
|
87
|
+
if count > 1
|
88
|
+
"#{count} #{process_type} processes"
|
89
|
+
elsif count == 1
|
90
|
+
"#{count} #{process_type} process"
|
91
|
+
else
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end.compact
|
95
|
+
if updated.any?
|
96
|
+
console.say_ok("Scaled down #{updated.join(', ')}")
|
97
|
+
else
|
98
|
+
console.say_ok("No non-web processes to scale down")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
desc 'push_code_to_heroku', 'Push local git branch to Heroku remote'
|
103
|
+
def push_code_to_heroku
|
104
|
+
run_shell_command(ShellCommand.new(
|
105
|
+
command_lines: "git push -f #{heroku_remote_name} #{branch}:master"
|
106
|
+
))
|
107
|
+
console.say_ok("Pushed code to Heroku")
|
108
|
+
end
|
109
|
+
|
110
|
+
desc 'snapshot_db', 'Capture a snapshot of the Heroku database'
|
111
|
+
def snapshot_db
|
112
|
+
run_shell_command(ShellCommand.new(
|
113
|
+
command_lines: "heroku pgbackups:capture --expire --app #{heroku_app_name}"
|
114
|
+
))
|
115
|
+
console.say_ok("Database snapshot captured")
|
116
|
+
end
|
117
|
+
|
118
|
+
desc 'migrate_db', 'Apply pending migrations to the database'
|
119
|
+
def migrate_db
|
120
|
+
if migration_count > 0
|
121
|
+
dyno = heroku.dyno.create(heroku_app_name, command: 'rake db:migrate db:seed')
|
122
|
+
poll_one_off_dyno_until_done(dyno)
|
123
|
+
console.say_ok("Pending database migrations applied")
|
124
|
+
else
|
125
|
+
console.say_ok("No pending database migrations to apply")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
desc 'scale_up [--worker=NUM] [--clock=NUM]', 'Scale up all non-web processes'
|
130
|
+
option :worker, type: :numeric
|
131
|
+
option :clock, type: :numeric
|
132
|
+
def scale_up
|
133
|
+
updates = if options[:worker] || options[:clock]
|
134
|
+
[:worker, :clock].each_with_object([]) do |process_type, m|
|
135
|
+
m << {'process' => process_type.to_s, 'quantity' => options[process_type]}
|
136
|
+
end
|
137
|
+
else
|
138
|
+
[:worker, :clock].each_with_object([]) do |process_type, m|
|
139
|
+
count = process_counts[process_type] || 0
|
140
|
+
m << {'process' => process_type.to_s, 'quantity' => count} if count > 0
|
141
|
+
end
|
142
|
+
end
|
143
|
+
heroku.formation.batch_update(heroku_app_name, 'updates' => updates)
|
144
|
+
updated = process_counts.map do |(process_type, count)|
|
145
|
+
if count > 1
|
146
|
+
"#{count} #{process_type} processes"
|
147
|
+
elsif count == 1
|
148
|
+
"#{count} #{process_type} process"
|
149
|
+
else
|
150
|
+
nil
|
151
|
+
end
|
152
|
+
end.compact
|
153
|
+
if updated.any?
|
154
|
+
console.say_ok("Scaled up #{updated.join(', ')}")
|
155
|
+
else
|
156
|
+
console.say_ok("No non-web processes to scale up")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
desc 'restart_app', 'Restart the Heroku app'
|
161
|
+
def restart_app
|
162
|
+
dynos = heroku.dyno.list(heroku_app_name).find_all { |d| d['type'] == 'web' }
|
163
|
+
dynos.each do |dyno|
|
164
|
+
heroku.dyno.restart(heroku_app_name, dyno['id'])
|
165
|
+
end
|
166
|
+
if dynos.count > 1
|
167
|
+
console.say_ok("Restarted #{dynos.count} web dynos")
|
168
|
+
elsif dynos.count == 1
|
169
|
+
console.say_ok("Restarted #{dynos.count} web dyno")
|
170
|
+
else
|
171
|
+
console.say_ok("No web dynos to restart")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
desc 'maintenance_off', 'Disable maintenance mode for the Heroku app'
|
176
|
+
def maintenance_off
|
177
|
+
heroku.app.update(heroku_app_name, maintenance:false)
|
178
|
+
console.say_ok("Maintenance mode disabled")
|
179
|
+
end
|
180
|
+
|
181
|
+
desc 'send_notifications', 'Send deploy notifications to external services'
|
182
|
+
def send_notifications
|
183
|
+
Array(cfg('notifications', required: false)).each do |(service, command)|
|
184
|
+
command.
|
185
|
+
gsub!(%r[{environment}], environment).
|
186
|
+
gsub!(%r[{revision}], revision)
|
187
|
+
dyno = heroku.dyno.create(heroku_app_name, command: command)
|
188
|
+
poll_one_off_dyno_until_done(dyno)
|
189
|
+
console.say_ok("Notification sent to #{service}")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
no_commands do
|
194
|
+
def environment
|
195
|
+
@environment ||= begin
|
196
|
+
env = options[:environment] || cfg('default_environment', required: false)
|
197
|
+
return env if env
|
198
|
+
console.say_err("Deployment environment must be specified, either with --environment or with 'default_environment' config for '#{command_name}' command")
|
199
|
+
exit(1)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def branch
|
204
|
+
@branch ||= begin
|
205
|
+
br = options[:branch] || cfg("#{environment}.branch", required: false)
|
206
|
+
return br if br
|
207
|
+
console.say_err("Local branch name must be specified, either with --branch or with '#{environment}.branch' config for '#{command_name}' command")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def revision
|
212
|
+
`git rev-parse #{branch}`
|
213
|
+
end
|
214
|
+
|
215
|
+
def heroku
|
216
|
+
@heroku ||= begin
|
217
|
+
::Dotenv.load
|
218
|
+
begin
|
219
|
+
PlatformAPI.connect_oauth(ENV['HEROKU_DEPLOY_TOKEN'])
|
220
|
+
rescue Excon::Errors::Unauthorized
|
221
|
+
console.say_err("Access to Heroku is not authorized. Did you set the HEROKU_DEPLOY_TOKEN environment variable?")
|
222
|
+
exit(1)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def heroku_info
|
228
|
+
@heroku_info ||= heroku.app.info(heroku_app_name)
|
229
|
+
end
|
230
|
+
|
231
|
+
def migration_count
|
232
|
+
@migration_count ||= `git diff #{branch} #{heroku_remote_name}/master --name-only -- db | wc -l`.strip!.to_i
|
233
|
+
end
|
234
|
+
|
235
|
+
def process_counts
|
236
|
+
@process_counts ||= begin
|
237
|
+
formations = heroku.formation.list(heroku_app_name).each_with_object({}) { |f, m| m[f['type'].to_sym] = f }
|
238
|
+
[:worker, :clock].each_with_object({}) do |process_type, m|
|
239
|
+
quantity = formations.fetch(process_type, {}).fetch('quantity', 0)
|
240
|
+
m[process_type] = quantity if quantity > 0
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def heroku_app_name
|
246
|
+
@heroku_app_name ||= cfg("#{environment}.heroku.app_name")
|
247
|
+
end
|
248
|
+
|
249
|
+
def heroku_remote_name
|
250
|
+
@heroku_remote_name ||= cfg("#{environment}.heroku.remote_name", required: false) || heroku_app_name
|
251
|
+
end
|
252
|
+
|
253
|
+
def poll_one_off_dyno_until_done(dyno)
|
254
|
+
done = false
|
255
|
+
state = 'starting'
|
256
|
+
console.say_trace("Starting process with command `#{dyno['command']}`")
|
257
|
+
while !done do
|
258
|
+
begin
|
259
|
+
dyno = heroku.dyno.info(heroku_app_name, dyno['id'])
|
260
|
+
if dyno['state'] != state
|
261
|
+
console.say_trace("State changed from #{state} to #{dyno['state']}")
|
262
|
+
state = dyno['state']
|
263
|
+
end
|
264
|
+
sleep 2
|
265
|
+
rescue Excon::Errors::NotFound
|
266
|
+
done = true
|
267
|
+
console.say_trace("State changed from #{state} to complete")
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
data/lib/shred/version.rb
CHANGED
data/lib/shred.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'shred/commands/app'
|
2
2
|
require 'shred/commands/db'
|
3
|
+
require 'shred/commands/deploy'
|
3
4
|
require 'shred/commands/dotenv'
|
4
5
|
require 'shred/commands/js_deps'
|
5
6
|
require 'shred/commands/platform_deps'
|
@@ -57,6 +58,10 @@ module Shred
|
|
57
58
|
desc 'app SUBCOMMAND ...ARGS', 'Control the application'
|
58
59
|
subcommand 'app', Commands::App
|
59
60
|
end
|
61
|
+
if commands.key?('deploy')
|
62
|
+
desc 'deploy SUBCOMMAND ...ARGS', 'Deploy the application'
|
63
|
+
subcommand 'deploy', Commands::Deploy
|
64
|
+
end
|
60
65
|
if commands.key?('setup')
|
61
66
|
desc 'setup', 'First-time application setup'
|
62
67
|
def setup
|
data/shred.gemspec
CHANGED
data/shred.yml
CHANGED
@@ -3,6 +3,19 @@ commands:
|
|
3
3
|
start:
|
4
4
|
- foreman start
|
5
5
|
db:
|
6
|
+
deploy:
|
7
|
+
default_environment: staging
|
8
|
+
staging:
|
9
|
+
branch: master
|
10
|
+
heroku:
|
11
|
+
app_name: my-app-staging
|
12
|
+
production:
|
13
|
+
branch: production
|
14
|
+
heroku:
|
15
|
+
app_name: my-app-production
|
16
|
+
notifications:
|
17
|
+
airbrake: bin/rake airbrake:deploy RAILS_ENV={environment} TO={environment}
|
18
|
+
new_relic: bin/newrelic deployments -e {environment} -r {revision}
|
6
19
|
dotenv:
|
7
20
|
heroku:
|
8
21
|
app_name: my-app
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shred
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brian
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-10-
|
12
|
+
date: 2014-10-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -39,6 +39,34 @@ dependencies:
|
|
39
39
|
- - ~>
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '10.0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: dotenv
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :runtime
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: platform-api
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
42
70
|
- !ruby/object:Gem::Dependency
|
43
71
|
name: thor
|
44
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,6 +100,7 @@ files:
|
|
72
100
|
- lib/shred/commands/app.rb
|
73
101
|
- lib/shred/commands/base.rb
|
74
102
|
- lib/shred/commands/db.rb
|
103
|
+
- lib/shred/commands/deploy.rb
|
75
104
|
- lib/shred/commands/dotenv.rb
|
76
105
|
- lib/shred/commands/js_deps.rb
|
77
106
|
- lib/shred/commands/platform_deps.rb
|