Created
August 30, 2012 13:10
-
-
Save mech/3528207 to your computer and use it in GitHub Desktop.
Appboy's Continuous Integration and Deployment Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Build script for Jenkins. This builds the checked out code and will deploy it if the commit came from | |
# an environment defined by the DeployEnvironment class. | |
# | |
# Scroll down to the bottom of this script to trace how it works. | |
require 'open-uri' | |
class StandardOutLogger | |
# Logs a message to standard out in red | |
# | |
# @param [ String ] msg The message to log | |
def error(msg) | |
colorize(msg, 31) | |
end | |
# Logs a message to standard out in green | |
# | |
# @param [ String ] msg The message to log | |
def info(msg) | |
colorize(msg, 32) | |
end | |
# Logs a message to standard out in yellow | |
# | |
# @param [ String ] msg The message to log | |
def warn(msg) | |
colorize(msg, 33) | |
end | |
private | |
# Outputs a message of a given color code to standard out | |
# | |
# @param [ String ] msg The message to log | |
# @param [ Integer ] color_code The color code to use | |
def colorize(msg, color_code) | |
output("\e[#{color_code}m#{msg}\e[0m") | |
end | |
# Logs a message to standard out | |
# | |
# @param [ String ] msg The message to log | |
def output(msg) | |
puts(msg) | |
end | |
end | |
class DeployEnvironment | |
attr_reader :branch, :environment, :url | |
# Creates a new DeployEnvironment | |
# | |
# @param [ String ] branch The name of the branch to use as the deploy source | |
# @param [ String ] ey_environment The name of the Engine Yard environment to deploy to | |
# @param [ Logger ] logger Optional logger to use that responds to #info and #error | |
def initialize(branch, ey_environment, url, logger = StandardOutLogger.new) | |
@branch = branch | |
@environment = ey_environment | |
@url = url | |
@logger = logger | |
end | |
# Deploys this environment | |
# | |
# @return [ Boolean ] Success of the deploy | |
def deploy | |
output = `ey deploy --environment #{@environment} --branch #{@branch} --no-migrate` | |
@logger.info(output) | |
$?.success? | |
end | |
# Runs smoke tests for this environment | |
# | |
# @return [ Boolean ] Success of smoke tests | |
def run_smoke_tests | |
# Do something reasonable here | |
# We do something like this: | |
# ENV['SMOKE_TEST_URL'] = @url | |
# `bundle exec rspec deploy/smoke_tests_spec.rb` | |
# $?.success? | |
true | |
end | |
# Rolls back this environment | |
# | |
# @return [ String ] Output of the rollback deploy log | |
def rollback | |
`ey rollback --environment #{@environment}` | |
end | |
end | |
class HipchatNotifier | |
# Creates a new HipChat Notifier | |
# | |
# @param [ String ] auth_token HipChat auth token | |
# @param [ String ] room HipChat room to post to | |
# @param [ Logger ] logger Optional logger to use that responds to #info and #error | |
def initialize(auth_token, room, logger = StandardOutLogger.new) | |
@auth_token = auth_token | |
@room = room | |
@logger = logger | |
end | |
# Posts an info message to HipChat | |
# | |
# @param [ String ] msg The message to send to Hipchat | |
def info(msg) | |
message(msg) | |
end | |
# Posts an error message to HipChat | |
# | |
# @param [ String ] msg The message to send to Hipchat | |
def error(msg) | |
message(msg, :red, true) | |
end | |
private | |
# Posts a message to HipChat | |
# | |
# @param [ String ] msg The message to send to Hipchat | |
# @param [ Symbol ] color The color to send per https://www.hipchat.com/docs/api/method/rooms/message | |
# @param [ Boolean ] notify Whether or not to notify the room | |
def message(msg, color = :purple, notify = false) | |
@logger.info("Posting to HipChat: \"#{msg}\"") | |
msg = URI.encode("<strong>#{msg}</strong>") | |
notify = notify ? 1 : 0 | |
`curl -d "auth_token=#{@auth_token}&room_id=#{@room}&from=Jenkins&color=#{color}¬ify=#{notify}&message=#{msg}" https://api.hipchat.com/v1/rooms/message` | |
end | |
end | |
class CIRunner | |
# Creates a new CIRunner | |
# | |
# @param [ Array ] deploy_environments Collection of DeployEnvironments that we can deploy to | |
# @param [ HipchatNotifier ] hipchat HipChatNotifier to use to post messages to HipChat | |
# @param [ Logger ] logger Optional logger to use that responds to #info and #error | |
def initialize(deploy_environments, hipchat, logger = StandardOutLogger.new) | |
@environments = deploy_environments | |
@logger = logger | |
@hipchat = hipchat | |
@commit_of_build = most_recent_commit() | |
end | |
# Runs the build by running bundle install and bundle exec rake | |
# | |
# @return [ Boolean ] true on success, raises an Exception on failure | |
def build | |
@logger.info("Starting the build.") | |
@logger.info(bundle()) | |
output, success = run_build() | |
@logger.info(output) | |
# Rake doesn't return consistent exit codes AFAICT, so actually inspect the RSpec logs | |
if output.match(/Failed examples/) || !success | |
@logger.error("Build failed.") | |
raise Exception.new("Build failed.") | |
else | |
@logger.info("Build complete.") | |
true | |
end | |
end | |
# Iterates over the deployable @environments and deploys to them if the commit we just build occurs on that | |
# environment's branch and #nodeploy is not in the deploy message. Posts to HipChat if the deploy was successful | |
# or not. | |
def deploy | |
@environments.each do |env| | |
if deploy?(env.branch) | |
@logger.info("Deploying to #{env.environment}") | |
# --no-migrate will not restart Unicorn | |
success = env.deploy() | |
if success | |
@hipchat.info("Appboy successfully deployed to #{env.environment} (#{env.url})") | |
smoke_tests_passed = env.run_smoke_tests() | |
unless smoke_tests_passed | |
@hipchat.error("Smoke tests failed for #{env.environment} (#{env.url})! Rolling back.") | |
output = env.rollback() | |
@logger.info(output) | |
@hipchat.info("Rollback complete for Appboy environment #{env.environment} (#{env.url})") | |
end | |
else | |
@hipchat.error("Appboy did not deploy successfully to #{env.environment} (#{env.url})") | |
end | |
end | |
end | |
end | |
private | |
# Returns the SHA1 hash of the most recent git commit on +branch+ | |
# | |
# @param [ String ] branch | |
# | |
# @return [ String ] SHA1 hash of the most recent git comment on the branch | |
def last_commit_on_branch(branch) | |
git_checkout(branch) | |
most_recent_commit() | |
end | |
# Runs bundle install | |
# | |
# @return [ String ] output from bundle install | |
def bundle | |
@logger.info("Running bundle") | |
`bundle install --without production,staging` | |
end | |
# Runs rake | |
# | |
# @return [ Array ] containing the rake output (string) and success (bool) | |
def run_build | |
@logger.info("Starting the build") | |
output = `bundle exec rake` | |
success = $?.success? | |
[output, success] | |
end | |
# Returns the SHA1 hash of the most recent git commit. | |
# | |
# @return SHA1 hash of the most recent git commit | |
def most_recent_commit | |
`git log | head -n 1 | cut -d " " -f 2` | |
end | |
# Returns whether or not a +term+ was in the last commit message | |
# | |
# @param [ String ] term String to look for in the commit message | |
# | |
# @example | |
# in_commit_message?("#nodeploy") | |
# | |
# @return [ Boolean ] Whether or not the term was in the last commit message | |
def in_commit_message?(term) | |
!`git log -n 1 | grep "#{term}"`.strip().empty? | |
end | |
# Returns whether or not we can deploy to +branch+. It will be true if the commit we built was on that branch and | |
# the commit message does not include #nodeploy. | |
# | |
# @param [ String ] branch Name of branch to check if we can deploy to it | |
# | |
# @example | |
# deploy?("develop") | |
# | |
# @return [ Boolean ] Whether or not we can deploy to +branch+ | |
def deploy?(branch) | |
commit_occurred_on_branch = @commit_of_build == last_commit_on_branch(branch) | |
commit_occurred_on_branch && !in_commit_message?("#nodeploy") | |
end | |
# Check out a branch from git. This will reset before checking out the branch, and then rebase that branch to | |
# ensure everything is up to date. | |
# | |
# @param [ String ] branch The git branch to check out | |
def git_checkout(branch) | |
`git reset --hard` | |
`git checkout #{branch}` | |
`git fetch` | |
`git rebase origin/#{branch}` | |
end | |
end | |
############################## | |
##### BEGIN BUILD SCRIPT ##### | |
############################## | |
HIPCHAT_AUTH_TOKEN = "..." # FILL ME IN | |
HIPCHAT_ROOM = "..." # FILL ME IN | |
hipchat = HipchatNotifier.new(HIPCHAT_AUTH_TOKEN, HIPCHAT_ROOM) | |
staging = DeployEnvironment.new("develop", "appboydotcom_staging", "https://www.url-for-staging-environment.com") | |
production = DeployEnvironment.new("master", "appboydotcom_production", "https://www.appboy.com") | |
deploy_environments = [staging, production] | |
runner = CIRunner.new(deploy_environments, hipchat) | |
runner.build() | |
runner.deploy() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment