Skip to content

Instantly share code, notes, and snippets.

@mech
Created August 30, 2012 13:10
Show Gist options
  • Save mech/3528207 to your computer and use it in GitHub Desktop.
Save mech/3528207 to your computer and use it in GitHub Desktop.
Appboy's Continuous Integration and Deployment Script
# 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}&notify=#{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