Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first commit

  • Loading branch information...
commit 9cf095eb1cbcd409602543fd48fe5362ec6e9487 0 parents
@zilkey authored
8 .gemified
@@ -0,0 +1,8 @@
+---
+:author: Jeff Dean
+:summary: Helps you automatically create tags for each stage in a multi-stage deploment and deploy from the latest tag from the previous environment
+:name: autotagger
+:dependencies:
+- capistrano
+:homepage: http://github.com/zilkey/git_tagger/tree/master
+:version: 0.0.1
4 .gitignore
@@ -0,0 +1,4 @@
+pkg/*
+rdoc/*
+*.gem
+.DS_Store
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 [Jeff Dean]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
79 README.md
@@ -0,0 +1,79 @@
+# AutoTagger
+
+AutoTagger allows you to create a date-stamped tag for each stage of your deployment, and deploy from the last tag from the previous environment.
+
+Let's say you have the following workflow:
+
+ * Run all test on a Continuous Integration (CI) server
+ * Deploy to a staging server
+ * Deploy to a production server
+
+You can use the `autotag` command to tag releases on your CI box, then use the capistrano tasks to auto-tag each release.
+
+## Capistrano Integration
+
+Example deploy file:
+
+ require 'release_tagger'
+
+ # The :stages variable is required
+ set :stages, [:ci, :staging, :production]
+
+ # The :working_directory variable is optional, and defaults to Dir.pwd
+ # :working_directory can be an absolute or relative path
+ set :working_directory, "../../"
+
+ task :production do
+ # In each of your environments that need auto-branch setting, you need to set :current_stage
+ set :current_stage, :production
+ end
+
+ task :staging do
+ # If you do not set current_stage, it will not auto-set your branch
+ # set :current_stage, :staging
+ end
+
+ # You need to add the before/ater callbacks yourself
+ before "deploy:update_code", "release_tagger:set_branch"
+ after "deploy:update_code", "release_tagger:create_tag"
+
+Assume you have the following tags in your git repository:
+
+ * ci/01
+ * staging/01
+ * production/01
+
+The deployments would look like this:
+
+ cap staging deploy # => ci/01
+ cap production deploy # => staging/01
+
+You can override with with the -Shead and -Stag options
+
+ cap staging deploy -Shead=true # => master
+ cap staging deploy -Stag=staging/01 # => staging/01
+
+## The autotag executable
+
+ autotag -h
+ autotag demo
+ autotag demo .
+ autotag demo /Users/me/foo
+
+## Known Issues
+
+ * DOES NOT work with capistrano ext multi-stage
+ * It will accept invalid tag names (if you specify a tag name with a space, it will blow up when you try to create the tag)
+
+## Things that might be useful
+
+ * Make it possible to define a different remote other than "origin"
+ * Make it possible to define a different default branch other than "master"
+ * Make it work with either cap-ext multistage or single-file deploy.rb files
+ * Make it possible to provide your own tag naming convention (like the PaperClip string interpolation), instead of relying on <prefix>/<timestamp>
+
+## Links
+
+ * http://codeintensity.blogspot.com/2008/06/changelogs-and-deployment-notification.html
+
+Copyright (c) 2009 [Jeff Dean], released under the MIT license
45 auto_tagger.gemspec
@@ -0,0 +1,45 @@
+# -*- encoding: utf-8 -*-
+
+Gem::Specification.new do |s|
+ s.name = %q{auto_tagger}
+ s.version = "0.0.1"
+
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.authors = ["Jeff Dean"]
+ s.date = %q{2009-03-28}
+ s.default_executable = %q{autotag}
+ s.executables = ["autotag"]
+
+ files = []
+
+ ['lib', 'recipes', 'bin', 'spec'].each do |dir|
+ files += Dir.glob(File.join(File.dirname(__FILE__), dir, "**", "*"))
+ end
+
+ ['MIT-LICENSE','README.md'].each do |file|
+ files << File.join(File.dirname(__FILE__), file)
+ end
+
+ s.files = files
+
+ puts s.files
+
+ s.email = "jeff at zilkey dot com"
+ s.homepage = %q{http://github.com/zilkey/git_tagger/tree/master}
+ s.require_paths = ["lib", "recipes"]
+ s.rubygems_version = %q{1.3.1}
+ s.summary = %q{Helps you automatically create tags for each stage in a multi-stage deploment and deploy from the latest tag from the previous environment}
+
+ if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
+ s.specification_version = 2
+
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
+ s.add_runtime_dependency(%q<capistrano>, [">= 2.5.3"])
+ else
+ s.add_dependency(%q<capistrano>, [">= 2.5.3"])
+ end
+ else
+ s.add_dependency(%q<capistrano>, [">= 2.5.3"])
+ end
+end
20 bin/autotag
@@ -0,0 +1,20 @@
+#!/usr/bin/env ruby
+require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "auto_tagger"))
+
+def usage
+ puts
+ puts "USAGE: #{File.basename($0)} [-h] [<stage> <repository>]"
+ puts
+ puts ' where: -h displays this help message'
+ puts ' stage sets the tag prefix'
+ puts ' repository sets the repository to act on'
+ puts
+ exit 1
+end
+
+if ARGV[0] && ARGV[0] == "-h"
+ usage
+elsif ARGV[0]
+ AutoTagger.new(ARGV[0], ARGV[1]).create_tag
+ exit 1
+end
9 lib/auto_tagger.rb
@@ -0,0 +1,9 @@
+[
+ 'commander',
+ 'repository',
+ 'tag',
+ 'auto_tagger',
+ 'capistrano_helper'
+].each do |file|
+ require File.expand_path(File.join(File.dirname(__FILE__), "auto_tagger", file))
+end
26 lib/auto_tagger/auto_tagger.rb
@@ -0,0 +1,26 @@
+class AutoTagger
+
+ class EnvironmentCannotBeBlankError < StandardError; end
+
+ attr_reader :stage, :repository, :working_directory
+
+ def initialize(stage, path = nil)
+ raise EnvironmentCannotBeBlankError if stage.to_s.strip == ""
+ @working_directory = File.expand_path(path ||= Dir.pwd)
+ @repository = Repository.new(@working_directory)
+ @stage = stage
+ end
+
+ def create_tag
+ repository.tags.fetch
+ new_tag = repository.tags.create(stage)
+ repository.tags.push
+ new_tag
+ end
+
+ def latest_tag
+ repository.tags.fetch
+ repository.tags.latest_from(stage)
+ end
+
+end
38 lib/auto_tagger/capistrano_helper.rb
@@ -0,0 +1,38 @@
+class CapistranoHelper
+
+ class NoStagesSpecifiedError < StandardError
+ def message
+ "You must set the :stages variable to an array, like set :stages, [:ci, :demo]"
+ end
+ end
+
+ attr_reader :variables, :stages, :current_stage, :working_directory
+
+ def initialize(variables)
+ raise NoStagesSpecifiedError unless variables[:stages]
+ @variables = variables
+ @stages = variables[:stages]
+ @current_stage = variables[:current_stage]
+ @working_directory = variables[:working_directory] || Dir.pwd
+ end
+
+ def previous_stage
+ if current_stage
+ index = stages.index(current_stage) - 1
+ stages[index] if index > -1
+ end
+ end
+
+ def branch
+ if variables.has_key?(:head)
+ variables[:branch]
+ elsif variables.has_key?(:tag)
+ variables[:tag]
+ elsif previous_stage && (latest = AutoTagger.new(previous_stage, working_directory).latest_tag)
+ latest
+ else
+ variables[:branch]
+ end
+ end
+
+end
16 lib/auto_tagger/commander.rb
@@ -0,0 +1,16 @@
+class Commander
+ class << self
+ def execute(path, cmd)
+ `#{command_in_context(path, cmd)}`
+ end
+
+ def execute!(path, cmd)
+ system command_in_context(path, cmd)
+ end
+
+ def command_in_context(path, cmd)
+ "cd #{path} && #{cmd}"
+ end
+ end
+end
+
38 lib/auto_tagger/repository.rb
@@ -0,0 +1,38 @@
+class Repository
+
+ class NoPathProvidedError < StandardError; end
+ class NoSuchPathError < StandardError; end
+ class InvalidGitRepositoryError < StandardError; end
+ class GitCommandFailedError < StandardError; end
+
+ attr_reader :path
+
+ def initialize(path)
+ if path.to_s.strip == ""
+ raise NoPathProvidedError
+ elsif ! File.exists?(path)
+ raise NoSuchPathError
+ elsif ! File.exists?(File.join(path, ".git"))
+ raise InvalidGitRepositoryError
+ else
+ @path = path
+ end
+ end
+
+ def ==(other)
+ other.is_a?(Repository) && other.path == path
+ end
+
+ def tags
+ @tags ||= Tag.new(self)
+ end
+
+ def run(cmd)
+ Commander.execute(path, cmd)
+ end
+
+ def run!(cmd)
+ Commander.execute!(path, cmd) || raise(GitCommandFailedError)
+ end
+
+end
38 lib/auto_tagger/tag.rb
@@ -0,0 +1,38 @@
+class Tag
+
+ attr_reader :repository
+
+ def initialize(repository)
+ @repository = repository
+ end
+
+ def find_all
+ repository.run("git tag").split("\n")
+ end
+
+ def fetch
+ repository.run! "git fetch origin --tags"
+ end
+
+ def latest_from(stage)
+ find_all.select{|tag| tag =~ /^#{stage}/}.sort.last
+ end
+
+ def push
+ repository.run! "git push origin --tags"
+ end
+
+ def create(stage)
+ # git tag -a -m 'Successful continuous integration build on #{timestamp}' #{tag_name}"
+ tag_name = name_for(stage)
+ repository.run! "git tag #{tag_name}"
+ tag_name
+ end
+
+ private
+
+ def name_for(stage)
+ "%s/%s" % [stage, Time.now.utc.strftime('%Y%m%d%H%M%S')]
+ end
+
+end
26 recipes/release_tagger.rb
@@ -0,0 +1,26 @@
+require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "auto_tagger"))
+
+Capistrano::Configuration.instance(:must_exist).load do
+ namespace :release_tagger do
+ desc %Q{
+ Sets the branch to the latest tag from the previous stage.
+ Use -Shead=true to set the branch to master, -Stag=<tag> to specify the tag explicitly.
+ }
+ task :set_branch do
+ branch_name = CapistranoHelper.new(variables).branch
+ set :branch, branch_name
+ logger.info "setting branch to #{branch_name.inspect}"
+ end
+
+ desc %Q{Creates a tag using the current_stage variable}
+ task :create_tag do
+ if variables[:current_stage]
+ tag_name = AutoTagger.new(variables[:current_stage], variables[:working_directory]).create_tag
+ logger.info "created and pushed tag #{tag_name}"
+ else
+ logger.info "AUTO TAGGER WARNING: skipping auto-creation of tag. Please specify :current_stage to enable auto-creation of tags (like set :current_stage, :ci)."
+ end
+ end
+ end
+end
+
69 spec/auto_tagger/auto_tagger_spec.rb
@@ -0,0 +1,69 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe AutoTagger do
+
+ describe ".new" do
+ it "blows up if you don't pass an stage" do
+ proc do
+ AutoTagger.new(nil)
+ end.should raise_error(AutoTagger::EnvironmentCannotBeBlankError)
+ end
+
+ it "sets the stage when it's passed" do
+ AutoTagger.new("ci").stage.should == "ci"
+ end
+
+ it "sets the path to Dir.pwd when nil" do
+ mock(Dir).pwd { "/foo" }
+ mock(Repository).new("/foo")
+ AutoTagger.new("ci")
+ end
+
+ it "expands the path when the path is passed" do
+ mock(Repository).new(File.expand_path("."))
+ AutoTagger.new("ci", ".")
+ end
+
+ it "exposes the working directory" do
+ mock(Repository).new(File.expand_path("."))
+ AutoTagger.new("ci", ".").working_directory.should == File.expand_path(".")
+ end
+ end
+
+ describe "#create_tag" do
+ it "generates the correct commands" do
+ time = Time.local(2001,1,1)
+ mock(Time).now.once {time}
+ timestamp = time.utc.strftime('%Y%m%d%H%M%S')
+ mock(File).exists?(anything).twice { true }
+
+ mock(Commander).execute!("/foo", "git fetch origin --tags") {true}
+ mock(Commander).execute!("/foo", "git tag ci/#{timestamp}") {true}
+ mock(Commander).execute!("/foo", "git push origin --tags") {true}
+
+ AutoTagger.new("ci", "/foo").create_tag
+ end
+
+ it "returns the tag that was created" do
+ time = Time.local(2001,1,1)
+ mock(Time).now.once {time}
+ timestamp = time.utc.strftime('%Y%m%d%H%M%S')
+ mock(File).exists?(anything).twice { true }
+ mock(Commander).execute!(anything, anything).times(any_times) {true}
+
+ AutoTagger.new("ci", "/foo").create_tag.should == "ci/#{timestamp}"
+ end
+ end
+
+ describe "#latest_tag" do
+ it "generates the correct commands" do
+ mock(File).exists?(anything).twice { true }
+
+ mock(Commander).execute!("/foo", "git fetch origin --tags") {true}
+ mock(Commander).execute("/foo", "git tag") { "ci_01" }
+
+ AutoTagger.new("ci", "/foo").latest_tag
+ end
+ end
+
+end
130 spec/auto_tagger/capistrano_helper_spec.rb
@@ -0,0 +1,130 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe CapistranoHelper do
+
+ describe ".new" do
+ it "blows up if there are no stages" do
+ proc do
+ CapistranoHelper.new({})
+ end.should raise_error(CapistranoHelper::NoStagesSpecifiedError)
+ end
+ end
+
+ describe "#variables" do
+ it "returns all variables" do
+ CapistranoHelper.new({:stages => [:bar]}).variables.should == {:stages => [:bar]}
+ end
+ end
+
+ describe "#stages" do
+ it "returns the hashes' stages value" do
+ CapistranoHelper.new({:stages => [:bar]}).stages.should == [:bar]
+ end
+ end
+
+ describe "#working_directory" do
+ it "returns the hashes' working directory value" do
+ CapistranoHelper.new({:stages => [:bar], :working_directory => "/foo"}).working_directory.should == "/foo"
+ end
+
+ it "defaults to Dir.pwd if it's not set, or it's nil" do
+ mock(Dir).pwd { "/bar" }
+ CapistranoHelper.new({:stages => [:bar]}).working_directory.should == "/bar"
+ end
+ end
+
+ describe "#current_stage" do
+ it "returns the hashes' current stage value" do
+ CapistranoHelper.new({:stages => [:bar], :current_stage => :bar}).current_stage.should == :bar
+ CapistranoHelper.new({:stages => [:bar]}).current_stage.should be_nil
+ end
+ end
+
+ describe "#previous_stage" do
+ it "returns the previous stage if there is more than one stage, and there is a current stage" do
+ CapistranoHelper.new({:stages => [:foo, :bar], :current_stage => :bar}).previous_stage.should == :foo
+ end
+
+ it "returns nil if there is no previous stage" do
+ CapistranoHelper.new({:stages => [:bar], :current_stage => :bar}).previous_stage.should be_nil
+ end
+
+ it "returns nil if there is no current stage" do
+ CapistranoHelper.new({:stages => [:bar]}).previous_stage.should be_nil
+ end
+ end
+
+ describe "#branch" do
+ describe "with :head and :branch specified" do
+ it "returns master" do
+ variables = {
+ :stages => [:bar],
+ :head => nil,
+ :branch => "foo"
+ }
+ CapistranoHelper.new(variables).branch.should == "foo"
+ end
+ end
+
+ describe "with :head specified, but no branch specified" do
+ it "returns master" do
+ variables = {
+ :stages => [:bar],
+ :head => nil
+ }
+ CapistranoHelper.new(variables).branch.should == nil
+ end
+ end
+
+ describe "with :branch specified" do
+ it "returns the value of branch" do
+ variables = {
+ :stages => [:bar],
+ :branch => "foo"
+ }
+ CapistranoHelper.new(variables).branch.should == "foo"
+ end
+ end
+
+ describe "with a previous stage with a tag" do
+ it "returns the latest tag for the previous stage" do
+ variables = {
+ :stages => [:foo, :bar],
+ :current_stage => :bar,
+ :branch => "master",
+ :working_directory => "/foo"
+ }
+ tagger = Object.new
+ mock(tagger).latest_tag { "foo_01" }
+ mock(AutoTagger).new(:foo, "/foo") { tagger }
+ CapistranoHelper.new(variables).branch.should == "foo_01"
+ end
+ end
+
+ describe "with no branch and a previous stage with no tag" do
+ it "returns nil" do
+ variables = {
+ :stages => [:foo, :bar],
+ :current_stage => :bar,
+ :working_directory => "/foo"
+ }
+ tagger = Object.new
+ mock(tagger).latest_tag { nil }
+ mock(AutoTagger).new(:foo, "/foo") { tagger }
+ CapistranoHelper.new(variables).branch.should == nil
+ end
+ end
+
+ describe "with no branch and previous stage" do
+ it "returns nil" do
+ variables = {
+ :stages => [:bar],
+ :current_stage => :bar
+ }
+ CapistranoHelper.new(variables).previous_stage.should be_nil
+ CapistranoHelper.new(variables).branch.should == nil
+ end
+ end
+ end
+
+end
17 spec/auto_tagger/commander_spec.rb
@@ -0,0 +1,17 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Commander do
+ describe ".execute" do
+ it "execute the command and returns the results" do
+ mock(Commander).`("cd /foo && ls") { "" } #`
+ Commander.execute("/foo", "ls")
+ end
+ end
+
+ describe "system" do
+ it "executes and doesn't return anything" do
+ mock(Commander).system("cd /foo && ls")
+ Commander.execute!("/foo", "ls")
+ end
+ end
+end
72 spec/auto_tagger/repository_spec.rb
@@ -0,0 +1,72 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Repository do
+ describe ".new" do
+ it "sets the repo" do
+ mock(File).exists?(anything).twice { true }
+ repo = Repository.new("/foo")
+ repo.path.should == "/foo"
+ end
+
+ it "raises an error when the path is blank" do
+ proc do
+ Repository.new(" ")
+ end.should raise_error(Repository::NoPathProvidedError)
+ end
+
+ it "raises an error when the path is nil" do
+ proc do
+ Repository.new(nil)
+ end.should raise_error(Repository::NoPathProvidedError)
+ end
+
+ it "raises an error with a file that doesn't exist" do
+ mock(File).exists?("/foo") { false }
+ proc do
+ Repository.new("/foo")
+ end.should raise_error(Repository::NoSuchPathError)
+ end
+
+ it "raises an error with a non-git repository" do
+ mock(File).exists?("/foo") { true }
+ mock(File).exists?("/foo/.git") { false }
+ proc do
+ Repository.new("/foo")
+ end.should raise_error(Repository::InvalidGitRepositoryError)
+ end
+ end
+
+ describe "#==" do
+ it "compares paths" do
+ mock(File).exists?(anything).times(any_times) { true }
+ Repository.new("/foo").should_not == "/foo"
+ Repository.new("/foo").should_not == Repository.new("/bar")
+ Repository.new("/foo").should == Repository.new("/foo")
+ end
+ end
+
+ describe "#run" do
+ it "sends the correct command" do
+ mock(File).exists?(anything).twice { true }
+ mock(Commander).execute("/foo", "bar")
+ Repository.new("/foo").run("bar")
+ end
+ end
+
+ describe "run!" do
+ it "sends the correct command" do
+ mock(File).exists?(anything).twice { true }
+ mock(Commander).execute!("/foo", "bar") { true }
+ Repository.new("/foo").run!("bar")
+ end
+
+ it "raises an exception if it the command returns false" do
+ mock(File).exists?(anything).twice { true }
+ mock(Commander).execute!("/foo", "bar") { false }
+ proc do
+ Repository.new("/foo").run!("bar")
+ end.should raise_error(Repository::GitCommandFailedError)
+ end
+ end
+
+end
66 spec/auto_tagger/tag_spec.rb
@@ -0,0 +1,66 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Tag do
+
+ before(:each) do
+ @repository = Object.new
+ end
+
+ describe ".new" do
+ it "sets the repository" do
+ Tag.new(@repository).repository.should == @repository
+ end
+ end
+
+ describe "#find_all" do
+ it "returns an array of tags" do
+ mock(@repository).run("git tag") { "ci_01\nci_02" }
+ Tag.new(@repository).find_all.should == ["ci_01", "ci_02"]
+ end
+
+ it "returns an empty array if there are none" do
+ mock(@repository).run("git tag") { "" }
+ Tag.new(@repository).find_all.should be_empty
+ end
+ end
+
+ describe "#latest_from" do
+ before do
+ @tag = Tag.new(@repository)
+ mock(@tag).find_all { ["ci/01", "ci/02"] }
+ end
+
+ it "returns the latest tag that starts with the specified stage" do
+ @tag.latest_from(:ci).should == "ci/02"
+ end
+
+ it "returns nil if none match" do
+ @tag.latest_from(:staging).should be_nil
+ end
+ end
+
+ describe "#fetch_tags" do
+ it "sends the correct command" do
+ mock(@repository).run!("git fetch origin --tags")
+ Tag.new(@repository).fetch
+ end
+ end
+
+ describe "#push" do
+ it "sends the correct command" do
+ mock(@repository).run!("git push origin --tags")
+ Tag.new(@repository).push
+ end
+ end
+
+ describe "#create" do
+ it "creates the right command and returns the name" do
+ time = Time.local(2001,1,1)
+ mock(Time).now.once {time}
+ tag_name = "ci/#{time.utc.strftime('%Y%m%d%H%M%S')}"
+ mock(@repository).run!("git tag #{tag_name}")
+ Tag.new(@repository).create("ci").should == tag_name
+ end
+ end
+
+end
7 spec/spec_helper.rb
@@ -0,0 +1,7 @@
+require 'spec'
+require 'rr'
+require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "auto_tagger"))
+
+Spec::Runner.configure do |config|
+ config.mock_with :rr
+end
Please sign in to comment.
Something went wrong with that request. Please try again.