shred 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 87f02895b1c18d05766bf9e02790f0e98314fda3
4
- data.tar.gz: 0d966bf202d43cbcc82d2e3bb6a4c8333b1bd329
3
+ metadata.gz: dc7d3395add485fc83816532c717b9e507d2a4ca
4
+ data.tar.gz: 2e51812cfbc8b84d64c69444cb88806600e6ee9f
5
5
  SHA512:
6
- metadata.gz: f03a12b0426f62dc6131920cb9af95d0b4aa5ee605c54c75447bb22e4f129ff39640537d642c728dd9dcf430eb0d6ff424ae0f1d195b88fce5ca95230d158108
7
- data.tar.gz: 14fd3fd258cc4001ec313518d3ef0a316206f270e7aca49103d63c770079e34a5774e55d6b1b80c1cd329d2288154f09f36a571f475e63f2a44fd0948652d6f8
6
+ metadata.gz: 7b78a34732cba64175900fcc65bad96316222c35478dd3fefb30ab6cf7a52eed68acd56a4823a07ae11b268f6b15c6fa353bf54553f47ed5fcfaa823815d0471
7
+ data.tar.gz: d829b8587f15cac0c577387bd15a3a2790e304bb805b7a8909d6010a6e9dc921474d96f6f5e585837bb51671b3e45e472a81464e64e444bbfffee9a3912a60ef
data/README.md CHANGED
@@ -20,7 +20,7 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
- TODO: Write usage instructions here
23
+ TODO: Write usage instructions here. For now, see the wiki.
24
24
 
25
25
  ## Contributing
26
26
 
@@ -64,7 +64,11 @@ module Shred
64
64
  @success_msg = success_msg
65
65
  @error_msg = error_msg
66
66
  @output = output
67
- @out = File.open(output, 'w') if output
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 '#{command}'"
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
@@ -1,3 +1,3 @@
1
1
  module Shred
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
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
@@ -21,5 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency 'bundler', '~> 1.7'
22
22
  spec.add_development_dependency 'rake', '~> 10.0'
23
23
 
24
+ spec.add_dependency 'dotenv'
25
+ spec.add_dependency 'platform-api'
24
26
  spec.add_dependency 'thor'
25
27
  end
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.2
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-14 00:00:00.000000000 Z
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