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 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