├── .document ├── .gitignore ├── README.rdoc ├── Rakefile ├── VERSION ├── capistrano-gitflow.gemspec ├── lib └── capistrano │ ├── gitflow.rb │ └── gitflow │ └── natcmp.rb ├── recipes └── gitflow_recipes.rb └── spec ├── gitflow_spec.rb ├── spec.opts └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rdoc 2 | pkg 3 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = gitflow: a Capistrano recipe for git deployment using tags in a multistage environment. 2 | 3 | The best thing about this recipe is that there is almost nothing to learn -- your cap deploy process barely changes. 4 | Gitflow simply adds some tagging/logging/workflow magic. 5 | 6 | # BEFORE 7 | $ cap deploy # 'master' goes to staging 8 | $ cap production deploy # 'master' goes to production 9 | 10 | # AFTER 11 | $ cap deploy 12 | # 'master' goes to staging; tag staging-YYYY-MM-DD.X created 13 | 14 | $ cap production deploy 15 | # deploys latest staging tag, or if last tag is a production tag then that, to production 16 | # displays a commit log of what will be pushed to production, requests confirmation before deploying 17 | # tag 'staging-YYYY-MM-DD-X' goes to production 18 | # tag 'production-YYYY-MM-DD-X' created; points to staging-YYYY-MM-DD-X 19 | 20 | # BONUS 21 | $ cap gitflow:commit_log 22 | # displays a commit log pushed to staging 23 | 24 | $ cap production gitflow:commit_log 25 | # displays a commit log of what will be pushed to production 26 | 27 | == INSTALLATION 28 | 29 | First, install the gem: 30 | 31 | gem install capistrano-gitflow 32 | 33 | Then update config/deploy.rb 34 | 35 | require 'capistrano/ext/multistage' 36 | require 'capistrano/gitflow' # needs to come after multistage 37 | 38 | More info at: http://rubygems.org/gems/capistrano-gitflow 39 | 40 | == DETAILS 41 | 42 | After experimenting with several workflows for deployment in git, I've finally found one I really like. 43 | 44 | * You can push to staging at any time; every staging push is automatically tagged with a unique tag. 45 | * You can only push a staging tag to production. This helps to enforce QA of all pushes to production. 46 | 47 | === PUSH TO STAGING 48 | 49 | Whenever you want to push the currently checked-out code to staging, just do: 50 | 51 | cap staging deploy 52 | 53 | gitflow will automatically: 54 | 55 | * create a unique tag in the format of 'staging-YYYY-MM-DD.X' 56 | * configure multistage to use that tag for the deploy 57 | * push the code and tags to the remote "origin" 58 | * and run the normal deploy task for the staging stage. 59 | 60 | === PUSH TO PRODUCTION: 61 | 62 | Whenever you want to push code to production, just do: 63 | 64 | cap production deploy 65 | 66 | gitflow will automatically: 67 | 68 | * determine the last staging tag created, show a commit log of last-production-tag..last-staging-tag 69 | * (alternatively, specify the tag to push to production via `-s tag=staging-YYYY-MM-DD-X-user-description` 70 | * prompt for confirmation of deploy 71 | * alias the staging tag to a production tag like: production-2008-09-08.2 72 | * configure multistage to use that tag for the deploy 73 | * push the code and tags to the remote "origin" 74 | * and run the normal deploy task for the production stage. 75 | 76 | === NOTES: 77 | 78 | * you may need to wipe out the cached-copy on the remote server that cap uses when switching to this workflow; I have seen situations where the cached copy cannot cleanly checkout to the new branch/tag. it's safe to try without wiping it out first, it will fail gracefully. 79 | * if your stages already have a "set :branch, 'my-staging-branch'" call in your configs, remove it. This workflow configures it automatically. 80 | 81 | == CREDIT 82 | 83 | Originally created by Alan Pinstein. 84 | 85 | Gemified and hacked by Josh Nichols. 86 | 87 | == LICENSE 88 | 89 | MIT licensed. 90 | 91 | Copyright (c) 2009-2011 Alan Pinstein 92 | 93 | Copyright (c) 2010-2011 Josh Nichols 94 | 95 | Permission is hereby granted, free of charge, to any person obtaining a copy 96 | of this software and associated documentation files (the "Software"), to deal 97 | in the Software without restriction, including without limitation the rights 98 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 99 | copies of the Software, and to permit persons to whom the Software is 100 | furnished to do so, subject to the following conditions: 101 | 102 | The above copyright notice and this permission notice shall be included in 103 | all copies or substantial portions of the Software. 104 | 105 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 106 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 107 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 108 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 109 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 110 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 111 | THE SOFTWARE. 112 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "capistrano-gitflow" 8 | gem.summary = %Q{Capistrano recipe for a deployment workflow based on git tags } 9 | gem.description = %Q{Capistrano recipe for a deployment workflow based on git tags} 10 | gem.email = "josh@technicalpickles.com" 11 | gem.homepage = "http://github.com/technicalpickles/capistrano-gitflow" 12 | gem.authors = ["Joshua Nichols"] 13 | gem.add_dependency "capistrano" 14 | gem.add_dependency "stringex" 15 | gem.add_development_dependency "rspec", ">= 1.2.9" 16 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 17 | end 18 | Jeweler::GemcutterTasks.new 19 | rescue LoadError 20 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 21 | end 22 | 23 | require 'spec/rake/spectask' 24 | Spec::Rake::SpecTask.new(:spec) do |spec| 25 | spec.libs << 'lib' << 'spec' 26 | spec.spec_files = FileList['spec/**/*_spec.rb'] 27 | end 28 | 29 | Spec::Rake::SpecTask.new(:rcov) do |spec| 30 | spec.libs << 'lib' << 'spec' 31 | spec.pattern = 'spec/**/*_spec.rb' 32 | spec.rcov = true 33 | end 34 | 35 | task :spec => :check_dependencies 36 | 37 | task :default => :spec 38 | 39 | require 'rake/rdoctask' 40 | Rake::RDocTask.new do |rdoc| 41 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 42 | 43 | rdoc.rdoc_dir = 'rdoc' 44 | rdoc.title = "gitflow #{version}" 45 | rdoc.rdoc_files.include('README*') 46 | rdoc.rdoc_files.include('lib/**/*.rb') 47 | end 48 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.4.3 2 | -------------------------------------------------------------------------------- /capistrano-gitflow.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{capistrano-gitflow} 8 | s.version = "1.4.3" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Joshua Nichols"] 12 | s.date = %q{2011-04-07} 13 | s.description = %q{Capistrano recipe for a deployment workflow based on git tags} 14 | s.email = %q{josh@technicalpickles.com} 15 | s.extra_rdoc_files = [ 16 | "README.rdoc" 17 | ] 18 | s.files = [ 19 | ".document", 20 | "README.rdoc", 21 | "Rakefile", 22 | "VERSION", 23 | "capistrano-gitflow.gemspec", 24 | "lib/capistrano/gitflow.rb", 25 | "lib/capistrano/gitflow/natcmp.rb", 26 | "recipes/gitflow_recipes.rb", 27 | "spec/gitflow_spec.rb", 28 | "spec/spec.opts", 29 | "spec/spec_helper.rb" 30 | ] 31 | s.homepage = %q{http://github.com/technicalpickles/capistrano-gitflow} 32 | s.require_paths = ["lib"] 33 | s.rubygems_version = %q{1.3.7} 34 | s.summary = %q{Capistrano recipe for a deployment workflow based on git tags} 35 | s.test_files = [ 36 | "spec/gitflow_spec.rb", 37 | "spec/spec_helper.rb" 38 | ] 39 | 40 | if s.respond_to? :specification_version then 41 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 42 | s.specification_version = 3 43 | 44 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 45 | s.add_runtime_dependency(%q, [">= 0"]) 46 | s.add_runtime_dependency(%q, [">= 0"]) 47 | s.add_development_dependency(%q, [">= 1.2.9"]) 48 | else 49 | s.add_dependency(%q, [">= 0"]) 50 | s.add_dependency(%q, [">= 0"]) 51 | s.add_dependency(%q, [">= 1.2.9"]) 52 | end 53 | else 54 | s.add_dependency(%q, [">= 0"]) 55 | s.add_dependency(%q, [">= 0"]) 56 | s.add_dependency(%q, [">= 1.2.9"]) 57 | end 58 | end 59 | 60 | -------------------------------------------------------------------------------- /lib/capistrano/gitflow.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano' 2 | require File.join(File.dirname(__FILE__), 'gitflow', 'natcmp') 3 | require 'stringex' 4 | 5 | module Capistrano 6 | class Gitflow 7 | def self.load_into(capistrano_configuration) 8 | capistrano_configuration.load do 9 | before "deploy:update_code", "gitflow:calculate_tag" 10 | before "gitflow:calculate_tag", "gitflow:verify_up_to_date" 11 | 12 | namespace :gitflow do 13 | def last_tag_matching(pattern) 14 | # search for most recent (chronologically) tag matching the passed pattern, then get the name of that tag. 15 | last_tag = `git describe --exact-match --match '#{pattern}' \`git log --tags='#{pattern}*' --format="%H" -1\``.chomp 16 | return nil if last_tag == '' 17 | return last_tag 18 | end 19 | 20 | def last_staging_tag() 21 | last_tag_matching('staging-*') 22 | end 23 | 24 | def next_staging_tag 25 | hwhen = Date.today.to_s 26 | who = `whoami`.chomp.to_url 27 | what = Capistrano::CLI.ui.ask("What does this release introduce? (this will be normalized and used in the tag for this release) ").to_url 28 | 29 | last_staging_tag = last_tag_matching("staging-#{hwhen}-*") 30 | new_tag_serial = if last_staging_tag && last_staging_tag =~ /staging-[0-9]{4}-[0-9]{2}-[0-9]{2}\-([0-9]*)/ 31 | $1.to_i + 1 32 | else 33 | 1 34 | end 35 | 36 | "#{stage}-#{hwhen}-#{new_tag_serial}-#{who}-#{what}" 37 | end 38 | 39 | def last_production_tag() 40 | last_tag_matching('production-*') 41 | end 42 | 43 | def using_git? 44 | fetch(:scm, :git).to_sym == :git 45 | end 46 | 47 | task :verify_up_to_date do 48 | if using_git? 49 | set :local_branch, `git branch --no-color 2> /dev/null | sed -e '/^[^*]/d'`.gsub(/\* /, '').chomp 50 | set :local_sha, `git log --pretty=format:%H HEAD -1`.chomp 51 | set :origin_sha, `git log --pretty=format:%H #{local_branch} -1` 52 | unless local_sha == origin_sha 53 | abort """ 54 | Your #{local_branch} branch is not up to date with origin/#{local_branch}. 55 | Please make sure you have pulled and pushed all code before deploying: 56 | 57 | git pull origin #{local_branch} 58 | # run tests, etc 59 | git push origin #{local_branch} 60 | 61 | """ 62 | end 63 | end 64 | end 65 | 66 | desc "Calculate the tag to deploy" 67 | task :calculate_tag do 68 | if using_git? 69 | # make sure we have any other deployment tags that have been pushed by others so our auto-increment code doesn't create conflicting tags 70 | `git fetch` 71 | 72 | if respond_to?("tag_#{stage}") 73 | send "tag_#{stage}" 74 | 75 | system "git push --tags origin #{local_branch}" 76 | if $? != 0 77 | abort "git push failed" 78 | end 79 | else 80 | puts "Will deploy tag: #{local_branch}" 81 | set :branch, local_branch 82 | end 83 | end 84 | end 85 | 86 | desc "Show log between most recent staging tag (or given tag=XXX) and last production release." 87 | task :commit_log do 88 | from_tag = if stage == :production 89 | last_production_tag 90 | elsif stage == :staging 91 | last_staging_tag 92 | else 93 | abort "Unsupported stage #{stage}" 94 | end 95 | 96 | # no idea how to properly test for an optional cap argument a la '-s tag=x' 97 | to_tag = capistrano_configuration[:tag] 98 | to_tag ||= begin 99 | puts "Calculating 'end' tag for :commit_log for '#{stage}'" 100 | to_tag = if stage == :production 101 | last_staging_tag 102 | elsif stage == :staging 103 | 'master' 104 | else 105 | abort "Unsupported stage #{stage}" 106 | end 107 | end 108 | 109 | 110 | # use custom compare command if set 111 | if ENV['git_log_command'] && ENV['git_log_command'].strip != '' 112 | command = "git #{ENV['git_log_command']} #{from_tag}..#{to_tag}" 113 | else 114 | # default compare command 115 | # be awesome for github 116 | if `git config remote.origin.url` =~ /git@github.com:(.*)\/(.*).git/ 117 | command = "open https://github.com/#{$1}/#{$2}/compare/#{from_tag}...#{to_tag}" 118 | else 119 | command = "git log #{from_tag}..#{to_tag}" 120 | end 121 | end 122 | puts "Displaying commits from #{from_tag} to #{to_tag} via:\n#{command}" 123 | system command 124 | 125 | puts "" 126 | end 127 | 128 | desc "Mark the current code as a staging/qa release" 129 | task :tag_staging do 130 | current_sha = `git log --pretty=format:%H HEAD -1` 131 | last_staging_tag_sha = if last_staging_tag 132 | `git log --pretty=format:%H #{last_staging_tag} -1` 133 | end 134 | 135 | if last_staging_tag_sha == current_sha 136 | puts "Not re-tagging staging because latest tag (#{last_staging_tag}) already points to HEAD" 137 | new_staging_tag = last_staging_tag 138 | else 139 | new_staging_tag = next_staging_tag 140 | puts "Tagging current branch for deployment to staging as '#{new_staging_tag}'" 141 | system "git tag -a -m 'tagging current code for deployment to staging' #{new_staging_tag}" 142 | end 143 | 144 | set :branch, new_staging_tag 145 | end 146 | 147 | desc "Push the approved tag to production. Pass in tag to deploy with '-s tag=staging-YYYY-MM-DD-X-feature'." 148 | task :tag_production do 149 | promote_to_production_tag = capistrano_configuration[:tag] || last_staging_tag 150 | 151 | unless promote_to_production_tag && promote_to_production_tag =~ /staging-.*/ 152 | abort "Couldn't find a staging tag to deploy; use '-s tag=staging-YYYY-MM-DD.X'" 153 | end 154 | unless last_tag_matching(promote_to_production_tag) 155 | abort "Staging tag #{promote_to_production_tag} does not exist." 156 | end 157 | 158 | promote_to_production_tag =~ /^staging-(.*)$/ 159 | new_production_tag = "production-#{$1}" 160 | 161 | if new_production_tag == last_production_tag 162 | puts "Not re-tagging #{last_production_tag} because it already exists" 163 | really_deploy = Capistrano::CLI.ui.ask("Do you really want to deploy #{last_production_tag}? [y/N]").to_url 164 | 165 | exit(1) unless really_deploy =~ /^[Yy]$/ 166 | else 167 | puts "Preparing to promote staging tag '#{promote_to_production_tag}' to '#{new_production_tag}'" 168 | gitflow.commit_log 169 | unless capistrano_configuration[:tag] 170 | really_deploy = Capistrano::CLI.ui.ask("Do you really want to deploy #{new_production_tag}? [y/N]").to_url 171 | 172 | exit(1) unless really_deploy =~ /^[Yy]$/ 173 | end 174 | puts "Promoting staging tag #{promote_to_production_tag} to production as '#{new_production_tag}'" 175 | system "git tag -a -m 'tagging current code for deployment to production' #{new_production_tag} #{promote_to_production_tag}" 176 | end 177 | 178 | set :branch, new_production_tag 179 | end 180 | end 181 | 182 | namespace :deploy do 183 | namespace :pending do 184 | task :compare do 185 | gitflow.commit_log 186 | end 187 | end 188 | end 189 | 190 | end 191 | 192 | end 193 | end 194 | end 195 | 196 | if Capistrano::Configuration.instance 197 | Capistrano::Gitflow.load_into(Capistrano::Configuration.instance) 198 | end 199 | -------------------------------------------------------------------------------- /lib/capistrano/gitflow/natcmp.rb: -------------------------------------------------------------------------------- 1 | # natcmp.rb 2 | # 3 | # Natural order comparison of two strings 4 | # e.g. "my_prog_v1.1.0" < "my_prog_v1.2.0" < "my_prog_v1.10.0" 5 | # which does not follow alphabetically 6 | # 7 | # Based on Martin Pool's "Natural Order String Comparison" originally written in C 8 | # http://sourcefrog.net/projects/natsort/ 9 | # 10 | # This implementation is Copyright (C) 2003 by Alan Davies 11 | # 12 | # 13 | # This software is provided 'as-is', without any express or implied 14 | # warranty. In no event will the authors be held liable for any damages 15 | # arising from the use of this software. 16 | # 17 | # Permission is granted to anyone to use this software for any purpose, 18 | # including commercial applications, and to alter it and redistribute it 19 | # freely, subject to the following restrictions: 20 | # 21 | # 1. The origin of this software must not be misrepresented; you must not 22 | # claim that you wrote the original software. If you use this software 23 | # in a product, an acknowledgment in the product documentation would be 24 | # appreciated but is not required. 25 | # 2. Altered source versions must be plainly marked as such, and must not be 26 | # misrepresented as being the original software. 27 | # 3. This notice may not be removed or altered from any source distribution. 28 | 29 | class String 30 | 31 | # 'Natural order' comparison of two strings 32 | def String.natcmp(str1, str2, caseInsensitive=false) 33 | str1, str2 = str1.dup, str2.dup 34 | compareExpression = /^(\D*)(\d*)(.*)$/ 35 | 36 | if caseInsensitive 37 | str1.downcase! 38 | str2.downcase! 39 | end 40 | 41 | # Remove all whitespace 42 | str1.gsub!(/\s*/, '') 43 | str2.gsub!(/\s*/, '') 44 | 45 | while (str1.length > 0) or (str2.length > 0) do 46 | # Extract non-digits, digits and rest of string 47 | str1 =~ compareExpression 48 | chars1, num1, str1 = $1.dup, $2.dup, $3.dup 49 | 50 | str2 =~ compareExpression 51 | chars2, num2, str2 = $1.dup, $2.dup, $3.dup 52 | 53 | # Compare the non-digits 54 | case (chars1 <=> chars2) 55 | when 0 # Non-digits are the same, compare the digits... 56 | # If either number begins with a zero, then compare alphabetically, 57 | # otherwise compare numerically 58 | if (num1[0] != 48) and (num2[0] != 48) 59 | num1, num2 = num1.to_i, num2.to_i 60 | end 61 | 62 | case (num1 <=> num2) 63 | when -1 then return -1 64 | when 1 then return 1 65 | end 66 | when -1 then return -1 67 | when 1 then return 1 68 | end # case 69 | 70 | end # while 71 | 72 | # Strings are naturally equal 73 | return 0 74 | end 75 | 76 | end # class String 77 | -------------------------------------------------------------------------------- /recipes/gitflow_recipes.rb: -------------------------------------------------------------------------------- 1 | # Just need to add to LOAD_PATH, so we can require 'gitflow' 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | -------------------------------------------------------------------------------- /spec/gitflow_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe "Gitflow" do 4 | it "fails" do 5 | fail "hey buddy, you should probably rename this file and start specing for real" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'gitflow' 4 | require 'spec' 5 | require 'spec/autorun' 6 | 7 | Spec::Runner.configure do |config| 8 | 9 | end 10 | --------------------------------------------------------------------------------