Skip to content

Instantly share code, notes, and snippets.

@rdh
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rdh/d6cb9304f587a931283b to your computer and use it in GitHub Desktop.
Save rdh/d6cb9304f587a931283b to your computer and use it in GitHub Desktop.
Heroku release with auto-generated release notes
rake release:cut - tags, updates relnotes, and copies master to release
rake release:staging - pushes release to staging on Heroku
rake release:production - pushes release to production on Heroku
rake release:all - use at your own risk
This code expects config/version.txt to exist and contain a single integer.
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
</head>
<body>
<%= status_table( @checkers ) %>
<br>
<pre><%= @notes %></pre>
</body>
</html>
namespace :release do
desc 'Cut a release and push it to staging and production'
task :all => :environment do
Releasing.cut
Releasing.deploy( :staging )
Releasing.deploy( :production )
end
desc 'Cut a release'
task :cut => :environment do
Releasing.cut
end
desc 'Release to staging'
task :staging => :environment do
Releasing.deploy( :staging )
end
desc 'Release to production'
task :production => :environment do
Releasing.deploy( :production )
end
desc 'Email a release notice'
task :mail => :environment do
SystemMailer.release.deliver
end
end
require 'spec_helper'
require 'rake'
describe 'release rake tasks' do
before( :each ) do
@rake = Rake::Application.new
Rake.application = @rake
Rake::Task.define_task(:environment)
load 'lib/tasks/release.rake'
Releasing.stub( :shell )
end
describe 'release:all' do
it 'cuts a release and pushes it to staging and production' do
Releasing.should_receive( :cut ).ordered
Releasing.should_receive( :deploy ).with( :staging ).ordered
Releasing.should_receive( :deploy ).with( :production ).ordered
@rake[ 'release:all' ].invoke
end
end
describe 'release:cut' do
it 'merges master into the release branch' do
Releasing.should_receive( :cut )
@rake[ 'release:cut' ].invoke
end
end
describe 'release:staging' do
it 'deploys a release to staging' do
Releasing.should_receive( :deploy ).with( :staging )
@rake[ 'release:staging' ].invoke
end
end
describe 'release:production' do
it 'deploys a release to production' do
Releasing.should_receive( :deploy ).with( :production )
@rake[ 'release:production' ].invoke
end
end
describe 'release:mail' do
it 'sends the system release email' do
message = double( Mail::Message )
message.should_receive( :deliver )
SystemMailer.should_receive( :release ).and_return( message )
@rake[ 'release:mail' ].invoke
end
end
end
class Releasing
SEPARATOR = '###############################################################################'
def self.cut
# Drop a tag and update the release notes
current_version = System::Version.new
shell 'git pull'
shell "git tag #{current_version} && git push --tags"
notes
shell %Q(git commit -a -m "- Updating release notes for #{current_version}")
# Merge master into the release branch
shell 'git checkout release'
shell 'git pull origin release'
shell 'git merge master' # TODO just merge up to tag to prevent surprises
# TODO run specs and bail if they don't pass
shell 'git push origin release'
shell 'git checkout master'
# Bump the version number and push it
System::Version.bump
next_version = System::Version.new
shell %Q(git commit -a -m "- Bumping version to #{next_version}")
shell 'git push origin master'
end
def self.deploy( env )
shell "git push #{env} release:master"
shell "heroku maintenance:on --remote #{env}"
shell "heroku run --size=PX rake db:migrate --remote #{env}"
shell "heroku restart --remote #{env}"
shell "heroku maintenance:off --remote #{env}"
shell "heroku run rake release:mail --remote #{env}"
end
def self.notes
current_version = System::Version.new
last_version = System::Version.new.decrement
command = %Q(git log --pretty=format:"\t %s" "#{last_version}..#{current_version}" | grep -v -e "^\\s*-")
lines = [] << "#{current_version} #{Time.now}" << shell( command )
current_version.notes = lines.join( "\n" ) + "\n\n"
end
# TODO refactor shell out to a more generic concern or util module
def self.shell( command )
result = `#{command}`
puts SEPARATOR
puts command
puts result
return result
end
end
require 'spec_helper'
describe Releasing do
let( :releaser ) { Class.new { include Releasing } }
let( :version ) { System::Version.new }
let( :other_version ) { System::Version.new }
let( :code ) { 727 }
before( :each ) do
Releasing.stub( :` )
Releasing.stub( :puts )
version.code = code
other_version.code = code
System::Version.stub( :new ).and_return( version, other_version )
System::Version.stub( :bump )
System::Version.any_instance.stub( :notes= )
System::Version.any_instance.stub( :save )
end
describe 'constants' do
it 'defines a separator for output' do
expected = '###############################################################################'
expect( Releasing::SEPARATOR ).to eql( expected )
end
end
describe '::cut' do
it 'runs a sequence of shell commands' do
Releasing.should_receive( :shell ).with( 'git pull' ).ordered
Releasing.should_receive( :shell ).with( "git tag #{version} && git push --tags" ).ordered
Releasing.should_receive( :notes ).ordered
Releasing.should_receive( :shell ).with( "git commit -a -m \"- Updating release notes for #{version}\"" ).ordered
Releasing.should_receive( :shell ).with( 'git checkout release' ).ordered
Releasing.should_receive( :shell ).with( 'git pull origin release' ).ordered
Releasing.should_receive( :shell ).with( 'git merge master' ).ordered
Releasing.should_receive( :shell ).with( 'git push origin release' ).ordered
Releasing.should_receive( :shell ).with( 'git checkout master' ).ordered
System::Version.should_receive( :bump ).ordered
Releasing.should_receive( :shell ).with( "git commit -a -m \"- Bumping version to #{other_version}\"" ).ordered
Releasing.should_receive( :shell ).with( 'git push origin master' ).ordered
Releasing.cut
end
end
describe '::deploy' do
it 'runs a sequence of shell commands' do
env = :staging
Releasing.should_receive( :shell ).with( "git push #{env} release:master" ).ordered
Releasing.should_receive( :shell ).with( "heroku maintenance:on --remote #{env}" ).ordered
Releasing.should_receive( :shell ).with( "heroku run --size=PX rake db:migrate --remote #{env}" ).ordered
Releasing.should_receive( :shell ).with( "heroku restart --remote #{env}" ).ordered
Releasing.should_receive( :shell ).with( "heroku maintenance:off --remote #{env}" ).ordered
Releasing.should_receive( :shell ).with( "heroku run rake release:mail --remote #{env}" ).ordered
Releasing.deploy( env )
end
end
describe '::notes' do
it 'retrieves notes from git log' do
expected = "git log --pretty=format:\"\t %s\" \"7.2.6..7.2.7\" | grep -v -e \"^\\s*-\""
Releasing.should_receive( :shell ).with( expected )
Releasing.notes
end
it 'adds the notes to the release notes file' do
version.should_receive( :notes= )
Releasing.notes
end
end
describe '::shell' do
it 'executes the command and captures the result' do
command = 'echo echo'
result = 'silence'
Releasing.should_receive( :` ).with( command ).and_return( result )
expect( Releasing.shell( command ) ).to be( result )
end
end
end
class SystemMailer < ActionMailer::Base
def release
@checkers = StatusCat::Status.all
@notes = System::Version.new.notes
config = StatusCat.config
mail( :to => config.to, :from => config.from, :subject => "[#{Rails.env.to_s.upcase}] Release #{System::Version.new}" )
end
end
module System
class Version
FILE = 'config/version.txt'
NOTES = 'ReleaseNotes.txt'
attr_accessor :code
def code
return @code ||= File.read( FILE ).to_i
end
def decrement
@code = code - 1
return self
end
def increment
@code = code + 1
return self
end
def notes
File.open( NOTES ) { |io| io.read }
end
def notes=( new_notes )
new_notes << notes
File.open( NOTES, 'wb' ) { |io| io.write( new_notes ) }
end
def save
File.open( FILE, 'wb' ) { |io| io.write( @code ) }
end
def to_s
major = ( code / 100 ).to_i
minor = ( ( code % 100 ) / 10 ).to_i
build = code % 10
return "#{major}.#{minor}.#{build}"
end
def self.bump
return System::Version.new.increment.save
end
end
end
require 'spec_helper.rb'
describe System::Version do
let( :version ) { System::Version.new }
describe 'constants' do
it 'defines version file path' do
expect( System::Version::FILE ).to eql( 'config/version.txt' )
end
it 'defines the release notes file path' do
expect( System::Version::NOTES ).to eql( 'ReleaseNotes.txt' )
end
end
describe 'attributes' do
describe 'code' do
it 'defaults to the file value' do
expected = File.read( System::Version::FILE ).to_i
expect( version.code ).to eql( expected )
end
it 'memoizes the value' do
File.should_receive( :read ).with( System::Version::FILE ).once.and_return( '1' )
version.code
version.code
end
end
end
describe '#decrement' do
it 'decrements the version code' do
expected = version.code - 1
version.decrement
expect( version.code ).to eql( expected )
end
it 'returns the version instance' do
expect( version.increment ).to eql( version )
end
end
describe '#increment' do
it 'increments the version code' do
expected = version.code + 1
version.increment
expect( version.code ).to eql( expected )
end
it 'returns the version instance' do
expect( version.increment ).to eql( version )
end
end
describe '#notes' do
it 'reads the contents of the release notes' do
expected = File.open( System::Version::NOTES ) { |io| io.read }
expect( version.notes ).to eql( expected )
end
it 'prepends new content of the release notes' do
new_notes = 'This is only a test'
old_notes = version.notes
file = double( 'file' )
File.should_receive( :open ).with( System::Version::NOTES ).and_yield( file )
File.should_receive( :open ).with( System::Version::NOTES, 'wb' ).and_yield( file )
file.should_receive( :read ).and_return( old_notes )
file.should_receive( :write ).with( new_notes + old_notes )
version.notes = new_notes
end
end
describe '#save' do
it 'writes the code to the version file' do
file = double( 'file' )
File.should_receive( :open ).with( System::Version::FILE, 'wb' ).and_yield( file )
file.should_receive( :write ).with( version.code )
version.save
end
end
describe '#to_s' do
it 'converts the code to a version string' do
version.code = 0
expect( version.to_s ).to eql( '0.0.0' )
version.code = 10
expect( version.to_s ).to eql( '0.1.0' )
version.code = 100
expect( version.to_s ).to eql( '1.0.0' )
version.code = 1000
expect( version.to_s ).to eql( '10.0.0' )
end
end
describe '::bump' do
it 'loads, increments, and saves the version' do
expect( version ).to be_present
System::Version.should_receive( :new ).and_return( version )
version.should_receive( :increment ).and_return( version )
version.should_receive( :save ).and_return( true )
expect( System::Version.bump ).to be_true
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment