bb_deploy 0.0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d9cc6005046b0480f59f0ddd824b42ce3db5b4fc
4
+ data.tar.gz: ef97ccf9b0486fe4222cce699372fbb1aa291c96
5
+ SHA512:
6
+ metadata.gz: 201d6e780955bd86311528ee7ed3109f3319018b6fd61a903b92a1b6b65ab32e68bb121536d3ce7ad91ec6b02b31c19a827c20aca542a915161123b006d0e82a
7
+ data.tar.gz: 7892a5234d19ee8bfab1a20395ca22d046676ab073d2189b33ff071bc947b7e6a0619bbbe22f5a2ce2b8fb685d40dd68bb5e83f0d1cf96c79e3d69c246370d95
@@ -0,0 +1,44 @@
1
+ require 'yaml'
2
+ require_relative './heroku'
3
+
4
+ module BbDeploy
5
+ class Config
6
+ attr_accessor :application_name,
7
+ :application_urls,
8
+ :logentries_token,
9
+ :deployment_channel,
10
+ :engineering_channel,
11
+ :slack_webhook_key
12
+
13
+ class << self
14
+ def configuration
15
+ @configuration ||= BbDeploy::Config.new
16
+ end
17
+
18
+ def configure
19
+ yield(configuration)
20
+ configuration
21
+ end
22
+
23
+ def configure_from_yaml(file_path)
24
+ options = YAML.load(ERB.new(File.read(file_path)).result)
25
+ configure do |config|
26
+ %w(application_name application_urls deployment_channel engineering_channel).each do |key|
27
+ config.send("#{key}=", options[key])
28
+ end
29
+ end
30
+ end
31
+
32
+ def set_heroku_fields!(phase)
33
+ configure do |config|
34
+ config.logentries_token = BbDeploy::Heroku.get_variable(phase, 'DEPLOY_LOG_TOKEN').freeze
35
+ config.slack_webhook_key = BbDeploy::Heroku.get_variable(phase, 'SLACK_WEBHOOK').freeze
36
+ end
37
+ end
38
+
39
+ def method_missing(method, *args)
40
+ configuration.send(method, *args)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,181 @@
1
+ require 'pp'
2
+ require_relative './config'
3
+ require_relative './task'
4
+ require_relative './logger'
5
+ require_relative './git'
6
+ require_relative './heroku'
7
+
8
+ class BbDeploy::Deployer
9
+
10
+ include BbDeploy::Logger
11
+
12
+ attr_reader :phase
13
+
14
+ def initialize(phase)
15
+ BbDeploy::Config.configure_from_yaml('config/deploy.yml')
16
+ BbDeploy::Config.set_heroku_fields!(phase)
17
+ @phase = phase
18
+ end
19
+
20
+ # Environment inquiry methods
21
+ %w(qa staging production).each do |phase|
22
+ define_method("#{phase}?") { @phase == phase }
23
+ end
24
+
25
+ def deploy!
26
+ BbDeploy::Git.check_git_status!
27
+ check_access!
28
+ staging_checks if phase == 'staging'
29
+ prompt = "Do you want to deploy #{current_branch} to #{BbDeploy::Config.application_name} #{phase}?"
30
+ if phase == 'production'
31
+ do_deploy if BbDeploy::Task.ask(prompt, required_response: 'production', important: true)
32
+ else
33
+ do_deploy if BbDeploy::Task.ask(prompt)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def dputs(*msgs)
40
+ asterisks = "*" * 80
41
+ puts asterisks
42
+ puts(*msgs.map(&:to_s))
43
+ puts asterisks
44
+ end
45
+
46
+
47
+ def current_branch
48
+ BbDeploy::Git.current_branch(started_on_master?)
49
+ end
50
+
51
+ def do_deploy
52
+ branch = current_branch
53
+
54
+ chat_broadcast(":alert: '#{ENV['USER']}' is deploying the #{BbDeploy::Config.application_name} branch '#{branch}' to '#{phase}' :alert:", phase: phase, notify_eng_hub: true)
55
+
56
+ do_heroku_deploy!
57
+
58
+ open_in_browser
59
+
60
+ if phase != 'qa' && !started_on_master?
61
+ puts "Merging back to master ..."
62
+ puts `git checkout master && git pull && git merge -`
63
+ puts "You must now resolve any conflicts in the merge and then `git push origin master` ..."
64
+ end
65
+
66
+ chat_broadcast(":alertgreen: Deployment of the #{BbDeploy::Config.application_name} branch '#{branch}' to '#{phase}' completed! :alertgreen:", phase: phase, notify_eng_hub: true)
67
+ end
68
+
69
+ def logger
70
+ @logger ||= BbDeploy::Logger.logger(phase)
71
+ end
72
+
73
+ def check_access!
74
+ unless BbDeploy::Heroku.has_access?(phase)
75
+ exit_with_message("You do not have access to the #{BbDeploy::Config.application_name} #{phase} application!")
76
+ end
77
+ end
78
+
79
+ def exit_with_message(msg)
80
+ puts "\n#{msg}\n"
81
+ exit(0)
82
+ end
83
+
84
+ def do_heroku_deploy! # rubocop:disable all
85
+ run_migrations = run_migrations?
86
+
87
+ exit(0) if run_migrations && !BbDeploy::Task.ask("There are new migrations to run. If you proceed,\n\n*** YOU WILL INCUR SITE DOWNTIME!!! ***\n\nDo you wish to proceed?")
88
+
89
+ enter_maint_mode = run_migrations || BbDeploy::Task.ask("Would you like to put #{BbDeploy::Config.application_name} into maintenance mode even though you don't have any migrations to run?")
90
+ if enter_maint_mode
91
+ stay_in_maint_mode = BbDeploy::Task.ask('Would you like to stay in maintenance mode after deployment, e.g. to run some rake tasks?')
92
+ logger.with_consolidated_logging { BbDeploy::Heroku.toggle_maintenance(mode: :on, phase: phase) }
93
+ end
94
+
95
+ push_release!
96
+
97
+ run_migrations! if run_migrations
98
+
99
+ if enter_maint_mode
100
+ if stay_in_maint_mode
101
+ dputs "********** NOTE: YOU ARE STILL IN MAINTENANCE MODE, AND MUST MANUALLY DISABLE IT VIA `rake heroku:maint:off:#{phase}` **********"
102
+ else
103
+ logger.with_consolidated_logging { BbDeploy::Heroku.toggle_maintenance(mode: :off, phase: phase) }
104
+ end
105
+ end
106
+ end
107
+
108
+ def run_migrations!
109
+ started_at = Time.current
110
+
111
+ chat_broadcast("Running migrations from #{BbDeploy::Config.application_name} branch '#{current_branch}' in #{phase} ...", phase: phase)
112
+ logger.with_consolidated_logging { BbDeploy::Heroku.migrate_db!(phase) }
113
+ chat_broadcast("Successfully ran migrations from #{BbDeploy::Config.application_name} branch '#{current_branch}' in #{phase}.", phase: phase)
114
+
115
+ time_spent = Time.zone.at(Time.current - started_at).getutc.strftime("%H:%M:%S")
116
+ # The only time started_on_master is non-nil is when creating a new staging branch
117
+ if started_on_master?
118
+ chat_broadcast("Yo, @jake - the #{BbDeploy::Config.application_name} branch '#{current_branch}' contains migrations that take #{time_spent} to run", phase: phase, notify_eng_hub: true)
119
+ end
120
+ end
121
+
122
+ def push_release!
123
+ puts "Deploying ..."
124
+ chat_broadcast("Pushing #{BbDeploy::Config.application_name} branch '#{current_branch}' to #{phase} ...", phase: phase)
125
+ logger.with_consolidated_logging { puts(BbDeploy::Git.push_to_phase(phase)) }
126
+ end
127
+
128
+ def open_in_browser
129
+ sleep 2 # because removing maint mode takes a couple seconds to propagate
130
+ host_and_path =
131
+ case phase
132
+ when 'qa'; BbDeploy::Config.application_urls['qa']
133
+ when 'staging'; BbDeploy::Config.application_urls['staging']
134
+ when 'production'; BbDeploy::Config.application_urls['production']
135
+ end
136
+
137
+ puts "Opening the site for inspection ..."
138
+ BbDeploy::Task.run("open https://#{host_and_path}")
139
+ end
140
+
141
+ def run_migrations?
142
+ sha = BbDeploy::Heroku.last_release_sha(phase)
143
+ BbDeploy::Git.migrations_present?(sha)
144
+ end
145
+
146
+ def started_on_master!
147
+ @started_on_mater = true
148
+ end
149
+
150
+ def started_on_master?
151
+ !!@started_on_master
152
+ end
153
+
154
+ def staging_checks
155
+ if BbDeploy::Git.on_master?
156
+ do_it = BbDeploy::Task.ask("You are on master. Do you want to create a ***NEW*** release branch from the HEAD of master and deploy it to #{BbDeploy::Config.application_name} #{phase}?")
157
+
158
+ if do_it
159
+ date = Time.zone.today.to_s(:db).tr('-', '_')
160
+ new_branch = "release_#{date}"
161
+
162
+ if BbDeploy::Git.local_release(new_branch).present?
163
+ exit_with_message("There already exists a local release branch named #{new_branch}!")
164
+ end
165
+
166
+ if BbDeploy::Git.remote_release(new_branch).present?
167
+ exit_with_message("There already exists a remote release branch named #{new_branch}!")
168
+ end
169
+
170
+ BbDeploy::Git.push_release_branch!
171
+
172
+ started_on_master!
173
+ end
174
+ elsif BbDeploy::Git.on_a_release_branch?
175
+ do_it = BbDeploy::Task.ask("Do you want to ***REDEPLOY*** the HEAD of your local version of #{current_branch} to #{BbDeploy::Config.application_name} #{phase}?")
176
+ else
177
+ exit_with_message("You may only deploy master or a release branch to #{BbDeploy::Config.application_name} #{phase}!")
178
+ end
179
+ end
180
+
181
+ end
@@ -0,0 +1,61 @@
1
+ require_relative './task'
2
+
3
+ module BbDeploy::Git
4
+ class << self
5
+ # NOTE - THE DEFAULT AUTO-MEMOIZATION CAN CAUSE ISSUES IF YOU FORGET IT'S HAPPENING, SO NOTE THAT IT OCCURS!!!
6
+ def current_branch(reload = false)
7
+ if reload || !@current_branch
8
+ # I don't know sed at all, hence I don't know which backslashes need to be doubled and which don't :(
9
+ cmd = 'git branch 2> /dev/null | sed -e \'/^[^*]/d\' -e \'s/* \(.*\)/\1/\''
10
+ result = `#{cmd}`
11
+ raise "Unable to determine the current branch name" if result.blank?
12
+ @current_branch = result.strip
13
+ else
14
+ @current_branch
15
+ end
16
+ end
17
+
18
+ def push_to_phase(phase)
19
+ `git push #{phase} HEAD:master --force`
20
+ end
21
+
22
+ def on_master?
23
+ current_branch == 'master'
24
+ end
25
+
26
+ def push_release_branch!
27
+ puts `git checkout -lb #{new_branch} && git push origin #{new_branch} -u`
28
+ end
29
+
30
+ def on_a_release_branch?
31
+ current_branch.start_with?('release_')
32
+ end
33
+
34
+ def check_git_status! # rubocop:disable Metrics/CyclomaticComplexity
35
+ puts "Running `git fetch` ... "
36
+ quietly { `git fetch` }
37
+ if `git log ..origin/#{current_branch}`.present?
38
+ exit(0) unless BbDeploy::Task.ask("There are new commits on the remote branch 'origin/#{current_branch}'. Are you sure you want to proceed?")
39
+ end
40
+ status_msg_a = `git status`.split(/(?:\n+)/)
41
+ if status_msg_a[1] =~ /your.branch.is.ahead/i
42
+ exit(0) unless BbDeploy::Task.ask("You have local, committed changes that have not been pushed to the remote. Are you sure you want to proceed?")
43
+ end
44
+ unless status_msg_a.last =~ /nothing to commit/i
45
+ exit(0) unless BbDeploy::Task.ask("You have local, uncommitted changes. Are you sure you want to proceed?")
46
+ end
47
+ end
48
+
49
+ def migrations_present?(sha)
50
+ `git diff #{sha} db/migrate`.present?
51
+ end
52
+
53
+ def local_release(branch_name)
54
+ `git branch --list "#{branch_name}"`
55
+ end
56
+
57
+ def remote_release(branch_name)
58
+ `git branch -r --list "origin/#{branch_name}"`
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,42 @@
1
+ require_relative './task'
2
+
3
+ module BbDeploy::Heroku
4
+ class << self
5
+ def get_variable(phase, var_name)
6
+ @heroku_env_vars ||= {}
7
+ return @heroku_env_vars[var_name] if @heroku_env_vars[var_name]
8
+ token = heroku_run("heroku config:get #{var_name} --remote #{phase}")
9
+ if token.present?
10
+ token = token.chomp
11
+ @heroku_env_vars[var_name] = token.split.last
12
+ end
13
+ @heroku_env_vars[var_name]
14
+ end
15
+
16
+ def last_release_sha(phase)
17
+ heroku_run("heroku releases -n 25 --remote #{phase} | grep Deploy | head -n 1 | awk '{print $3}'")
18
+ end
19
+
20
+ def has_access?(phase)
21
+ heroku_info = heroku_run("heroku info --remote #{phase} 2>&1")
22
+ !(heroku_info =~ /You do not have access/i || heroku_info =~ /Enter your Heroku credentials./)
23
+ end
24
+
25
+ def migrate_db!(phase)
26
+ heroku_run(
27
+ "heroku run --size=PX rake db:migrate --remote #{phase}",
28
+ "heroku restart --remote #{phase}" # necessary to not have the web server gag
29
+ )
30
+ end
31
+
32
+ def toggle_maintenance(mode:, phase:)
33
+ Rake::Task["heroku:maint:#{mode}:#{phase}"].invoke
34
+ end
35
+
36
+ def heroku_run(*cmds)
37
+ # Heroku Toolbelt uses Ruby 1.9, which requires clearing the RUBYOPT var ...
38
+ puts "Running: #{cmds.map(&:inspect).join(", ")}"
39
+ BbDeploy::Task.run(*cmds.map { |cmd| cmd =~ /heroku/ ? "export RUBYOPT='' && #{cmd}" : cmd })
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # Helper file for Logging/Slack notifications
2
+ module BbDeploy::Logger
3
+ def self.logger(phase)
4
+ @logger ||= ConsolidatedLogger::Logentries.new(phase) # Phase-specific memoization
5
+ end
6
+
7
+ module ConsolidatedLogger
8
+ # For logging to our Logentries consolidated logging service
9
+ class Logentries
10
+ def initialize(phase)
11
+ @phase = phase
12
+ end
13
+
14
+ def with_consolidated_logging
15
+ @original_stdout = $stdout
16
+ # Logging something before assigning $stdout is necessary
17
+ # to avoid `SystemStackError: stack level too deep`?!
18
+ logger.send(:info, "logging #with_consolidated_logging")
19
+ $stdout = self
20
+ yield
21
+ ensure
22
+ $stdout = @original_stdout
23
+ end
24
+
25
+ # To replace $stdout we must #write
26
+ def write(msg)
27
+ # Apparently reassigning $stdout triggers a blank message.
28
+ @original_stdout.puts(msg)
29
+ logger.send(:info, "#{ENV['USER']} #{msg}") unless msg.blank?
30
+ end
31
+
32
+ def method_missing(method, *args)
33
+ # this is mostly a call to #flush. We do nothing.
34
+ end
35
+
36
+ # Logs originating here will be found in the BB
37
+ # Logentries account in the "Ruby Log" for each phase
38
+ def logger
39
+ @logger ||= Le.new(BbDeploy::Config.logentries_token)
40
+ end
41
+ end
42
+ end
43
+
44
+ def chat_broadcast(msg, phase:, notify_eng_hub: false)
45
+ attachment = [
46
+ {
47
+ fallback: "Deploying to #{phase}",
48
+ color: " ",
49
+ fields: [
50
+ {
51
+ title: "Details",
52
+ value: msg,
53
+ short: false
54
+ }
55
+ ]
56
+ }
57
+ ]
58
+
59
+ slack.ping("*Deploying to #{phase}*", attachments: attachment, channel: BbDeploy::Config.deployment_channel)
60
+ slack.ping("*Deploying to #{phase}*", attachments: attachment, channel: BbDeploy::Config.engineering_channel) if notify_eng_hub
61
+ @consolidated_logger ||= ConsolidatedLogger::Logentries.new(phase)
62
+ @consolidated_logger.logger.info("DEPLOY--#{msg}")
63
+ end
64
+
65
+ def slack
66
+ @slack ||= Slack::Notifier.new(BbDeploy::Config.slack_webhook_key)
67
+ end
68
+ end
@@ -0,0 +1,12 @@
1
+ require 'bb_deploy'
2
+ # require 'rails'
3
+
4
+ module BbDeploy
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :bb_deploy
7
+
8
+ rake_tasks do
9
+ load "tasks/deploy.rake"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ module BbDeploy::Task
2
+ class << self
3
+ def run(*cmds)
4
+ dry_run = !!ENV['DRY_RUN']
5
+ Dir.chdir(Rails.root)
6
+ result = cmds.map do |cmd|
7
+ cmd_s = "==> \`#{cmd}\`"
8
+ if dry_run
9
+ puts "DRY RUN ONLY"
10
+ puts cmd_s
11
+ else
12
+ with_timing(cmd_s) { `#{cmd}` || raise("System call failed: #{cmd.inspect}") }
13
+ end
14
+ end
15
+ result.last.try(:strip) unless dry_run
16
+ end
17
+
18
+ def ask(prompt, required_response: 'yes', important: false)
19
+ msg = prompt + "\n\nTyping anything other than '#{required_response}' will abort."
20
+ if important # Color important text RED and highlight the required response
21
+ msg = "\e[31m#{msg}\e[0m"
22
+ msg.sub!(/'#{required_response}'/, "\e[47m'#{required_response}'\e[49m")
23
+ end
24
+ puts
25
+ HighLine.new.ask(msg) =~ /\A#{required_response}\Z/i
26
+ end
27
+
28
+ private
29
+
30
+ def with_timing(what)
31
+ start = Time.current
32
+ puts
33
+ puts "Commencing #{what} ..."
34
+ result = yield
35
+ time = Time.zone.at(Time.current - start).getutc.strftime("%H:%M:%S")
36
+ puts "Finished #{what} in #{time}."
37
+ puts
38
+ result
39
+ end
40
+ end
41
+ end
data/lib/bb_deploy.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'pry'
2
+
3
+ module BbDeploy
4
+ require 'bb_deploy/railtie' if defined?(Rails)
5
+ require 'bb_deploy/deployer'
6
+ end
@@ -0,0 +1,17 @@
1
+ require_relative '../bb_deploy'
2
+
3
+ namespace :heroku do
4
+ namespace :deploy do
5
+ task qa: :environment do
6
+ BbDeploy::Deployer.new('qa').deploy!
7
+ end
8
+
9
+ task staging: :environment do
10
+ BbDeploy::Deployer.new('staging').deploy!
11
+ end
12
+
13
+ task production: :environment do
14
+ BbDeploy::Deployer.new('production').deploy!
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bb_deploy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Peter van Wesep
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: highline
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: le
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: slack-notifier
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email: peter@brightbytes.net
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - lib/bb_deploy.rb
90
+ - lib/bb_deploy/config.rb
91
+ - lib/bb_deploy/deployer.rb
92
+ - lib/bb_deploy/git.rb
93
+ - lib/bb_deploy/heroku.rb
94
+ - lib/bb_deploy/logger.rb
95
+ - lib/bb_deploy/railtie.rb
96
+ - lib/bb_deploy/task.rb
97
+ - lib/tasks/deploy.rake
98
+ homepage:
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.5.1
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Shared gem for deploying BrightBytes repositories
122
+ test_files: []