commit 694fba3ea35c7156148a17be3534b87add16a257 Author: Karl Matthias Date: Wed Apr 4 10:15:53 2012 +0100 Initial check-in. diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..a9d8e26 --- /dev/null +++ b/Gemfile @@ -0,0 +1,9 @@ +source "https://rubygems.org" + +group :test do + gem 'rake' + gem 'rspec' + gem 'capistrano' + gem 'capistrano-ext' + gem 'capistrano-spec', :git => 'git://github.com/mydrive/capistrano-spec.git' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..6459018 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,45 @@ +GIT + remote: git://github.com/mydrive/capistrano-spec.git + revision: dcc0908dc00872272b1d495bcce70e82240fa8f7 + specs: + capistrano-spec (0.1.0) + +GEM + remote: https://rubygems.org/ + specs: + capistrano (2.11.2) + highline + net-scp (>= 1.0.0) + net-sftp (>= 2.0.0) + net-ssh (>= 2.0.14) + net-ssh-gateway (>= 1.1.0) + capistrano-ext (1.2.1) + capistrano (>= 1.0.0) + diff-lcs (1.1.3) + highline (1.6.11) + net-scp (1.0.4) + net-ssh (>= 1.99.1) + net-sftp (2.0.5) + net-ssh (>= 2.0.9) + net-ssh (2.3.0) + net-ssh-gateway (1.1.0) + net-ssh (>= 1.99.1) + rake (0.9.2.2) + rspec (2.8.0) + rspec-core (~> 2.8.0) + rspec-expectations (~> 2.8.0) + rspec-mocks (~> 2.8.0) + rspec-core (2.8.0) + rspec-expectations (2.8.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.8.0) + +PLATFORMS + ruby + +DEPENDENCIES + capistrano + capistrano-ext + capistrano-spec! + rake + rspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9276fd8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2012 MyDrive Solutions Limited, All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb6b89f --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +Capistrano Deployment Tags +========================== +This plugin to Capistrano will add tags for each deployment that +you make to the project. It is intnded to be used with the multistage +plugin and will tag each release by environment. Each successful +deployment will result in a new tag as well as moving an existing +tag that points to the last deployment. + +What It Does +------------ +If I were to issue the command: + +`cap production deploy` + +This would result in one new git tag with the environment and +timestamp: + +`production-2012.04.02-203155` + +It would also result in moving or creating this tag: + +`production-latest` + +These tags can be used for any number of useful things including +generating statistics about deployments per day/week/year, tracking +code size over a period of time, detecting Rails migrations, and +probably a thousand other things I haven't thought of. + +Usage +----- +If you use Bundler, be sure to add the gem to your Gemfile. +In your Capistrano `config/deploy.rb` you should add: + +`require 'capistrano_deploytags'` + +This will create two tasks, one that runs before deployment and one +that runs after. + +NOTE: You will be creating and pushing tags from the version of the +code in the current checkout. This plugin needs to be run from a +clean checkout of your codebase. You should be deploying from a +clean checkout anyway, so in most cases this is not a restriction +on how you already do things. The plugin will check if your code +is clean and complain if it is not. + +Credits +------- +This software was written by [Karl Matthias](https://github.com/relistan) +with help from [Gavin Heavyside](https://github.com/hgavin) and the +support of [MyDrive Solutions Limited](http://mydrivesolutions.com). + +License +------- +This plugin is released under the BSD two clause license which is +available in both the Ruby Gem and the source repository. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9e9effd --- /dev/null +++ b/Rakefile @@ -0,0 +1,20 @@ +require 'rake' + +$:.unshift(File.expand_path("lib")) + +begin + require 'rspec/core/rake_task' + + RSpec::Core::RakeTask.new(:spec) do |t| + t.rspec_opts = %w[--color --format=documentation] + t.pattern = "spec/**/*_spec.rb" + end + + task :default => [:spec] +rescue LoadError + # don't generate Rspec tasks if we don't have it installed +end + +task :build do + system "gem build capistrano-deploytags.gemspec" +end diff --git a/capistrano-deploytags.gemspec b/capistrano-deploytags.gemspec new file mode 100644 index 0000000..c1bad72 --- /dev/null +++ b/capistrano-deploytags.gemspec @@ -0,0 +1,14 @@ +Gem::Specification.new do |s| + s.name = 'capistrano_deploytags' + s.version = '0.5' + s.date = '2012-04-02' + s.summary = "Add dated, environment-specific tags to your git repo at each deployment." + s.description = s.summary + s.authors = ["Karl Matthias"] + s.email = 'relistan@gmail.com' + s.files = Dir.glob("lib/**/*") + %w{ README.md LICENSE } + s.homepage = 'http://github.com/mydrive/capistrano_deploytags' + s.add_dependency 'capistrano' + s.add_dependency 'capistrano-ext' + s.require_path = 'lib' +end diff --git a/lib/capistrano/deploy_tags.rb b/lib/capistrano/deploy_tags.rb new file mode 100644 index 0000000..a2bf486 --- /dev/null +++ b/lib/capistrano/deploy_tags.rb @@ -0,0 +1,84 @@ +module Capistrano + module DeployTags + def pending_git_changes? + # Do we have any changes vs HEAD on deployment branch? + !(`git fetch && git diff #{branch} --shortstat`.strip.empty?) + end + + def git_tag_for(stage) + "#{stage}-#{Time.now.strftime("%Y.%m.%d-%H%M%S")}" + end + + def last_git_tag_for(stage) + "#{stage}-latest" + end + + def safe_run(*args) + raise "#{args.join(" ")} failed!" unless system(*args) + end + + def validate_git_vars + unless exists?(:branch) && exists?(:stage) + logger.log Capistrano::Logger::IMPORTANT, "Capistrano Deploytags requires that :branch and :stage be defined." + raise 'define :branch or :stage' + end + end + + def git_tag?(tag) + !`git tag -l #{tag}`.strip.empty? + end + + def has_remote? + !`git remote`.strip.empty? + end + + def self.load_into(configuration) + configuration.load do + before :deploy, 'git:prepare_tree' + after :deploy, 'git:deploytags' + + desc 'prepare git tree so we can tag on successful deployment' + namespace :git do + task :prepare_tree, :except => { :no_release => true } do + cdt.validate_git_vars + + logger.log Capistrano::Logger::IMPORTANT, "Preparing to deploy HEAD from branch '#{branch}' to '#{stage}'" + + if cdt.pending_git_changes? + logger.log Capistrano::Logger::IMPORTANT, "Whoa there, partner. Dirty trees can't deploy. Git yerself clean first." + raise 'Dirty git tree' + end + + cdt.safe_run "git", "checkout", branch + cdt.safe_run "git", "pull", "origin", branch if cdt.has_remote? + end + + desc 'add git tags for each successful deployment' + task :tagdeploy, :except => { :no_release => true } do + cdt.validate_git_vars + + current_sha = `git rev-parse #{branch} HEAD`.strip[0..8] + logger.log Capistrano::Logger::INFO, "Tagging #{current_sha} for deployment" + + tag_user = (ENV['USER'] || ENV['USERNAME']).strip + cdt.safe_run "git", "tag", "-a", cdt.git_tag_for(stage), "-m", "#{tag_user} deployed #{current_sha} to #{stage}" + if cdt.git_tag?(cdt.last_git_tag_for(stage)) + cdt.safe_run "git", "tag", "-d", cdt.last_git_tag_for(stage) + cdt.safe_run "git", "push", "--tags" if cdt.has_remote? + end + + cdt.safe_run "git", "tag", "-a", cdt.last_git_tag_for(stage), "-m", "#{tag_user} deployed #{current_sha} to #{stage}" + cdt.safe_run "git", "push", "--tags" if cdt.has_remote? + end + end + + end + end + end +end + +Capistrano.plugin :cdt, Capistrano::DeployTags + +if Capistrano::Configuration.instance + Capistrano::DeployTags.load_into(Capistrano::Configuration.instance(:must_exist)) +end diff --git a/spec/capistrano_deploy_tags_spec.rb b/spec/capistrano_deploy_tags_spec.rb new file mode 100644 index 0000000..2eddc43 --- /dev/null +++ b/spec/capistrano_deploy_tags_spec.rb @@ -0,0 +1,74 @@ +require 'capistrano' +require 'capistrano-spec' +require 'fileutils' +mypath = File.expand_path(File.dirname(__FILE__)) +require File.expand_path(File.join(mypath, '..', 'lib', 'capistrano', 'deploy_tags')) + +describe Capistrano::DeployTags do + let(:configuration) { Capistrano::Configuration.new } + let(:tmpdir) { "/tmp/#{$$}" } + let(:mypath) { mypath } + + before :each do + Capistrano::DeployTags.load_into(configuration) + end + + def with_clean_repo(&block) + FileUtils.rm_rf tmpdir + FileUtils.mkdir tmpdir + FileUtils.chdir tmpdir + raise unless system("/usr/bin/tar xzf #{File.join(mypath, 'fixtures', 'git-fixture.tar.gz')}") + FileUtils.chdir "#{tmpdir}/git-fixture" + yield + FileUtils.rm_rf tmpdir + end + + context "prepare_tree" do + it "raises an error when not in a git tree" do + FileUtils.chdir '/tmp' + configuration.set(:branch, 'master') + configuration.set(:stage, 'test') + lambda { configuration.find_and_execute_task('git:prepare_tree') }.should raise_error('git checkout master failed!') + end + + context "with a clean git tree" do + it "raises an error if :stage or :branch are undefined" do + with_clean_repo do + lambda { configuration.find_and_execute_task('git:prepare_tree') }.should raise_error('define :branch or :stage') + end + end + + it "does not raise an error when run from a clean tree" do + with_clean_repo do + configuration.set(:branch, 'master') + configuration.set(:stage, 'test') + lambda { configuration.find_and_execute_task('git:prepare_tree') }.should_not raise_error + end + end + end + end + + context "tagdeploy" do + before :each do + configuration.set(:branch, 'master') + configuration.set(:stage, 'test') + end + + it "does not raise an error when run from a clean tree" do + with_clean_repo do + lambda { configuration.find_and_execute_task('git:tagdeploy') }.should_not raise_error + end + end + + it "adds appropriate git tags" do + with_clean_repo do + configuration.find_and_execute_task('git:tagdeploy') + + tags = `git tag -l`.split(/\n/).sort + tags.should have(2).items + tags.first.should =~ /^test-\d{4}\.\d{2}\.\d{2}/ + tags.last.should == 'test-latest' + end + end + end +end diff --git a/spec/fixtures/git-fixture.tar.gz b/spec/fixtures/git-fixture.tar.gz new file mode 100644 index 0000000..728b04a Binary files /dev/null and b/spec/fixtures/git-fixture.tar.gz differ