factorylabs-auto_tagger 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "auto_tagger"))
2
+
3
+ Capistrano::Configuration.instance(:must_exist).load do
4
+ namespace :release_tagger do
5
+ desc %Q{
6
+ Sets the branch to the latest tag from the previous stage.
7
+ Use -Shead=true to set the branch to master, -Stag=<tag> to specify the tag explicitly.
8
+ }
9
+ task :set_branch do
10
+ if branch_name = CapistranoHelper.new(variables).branch
11
+ set :branch, branch_name
12
+ logger.info "setting branch to #{branch_name}"
13
+ else
14
+ logger.info "AUTO TAGGER: skipping auto-assignment of branch. Branch will remain the default.}"
15
+ end
16
+ end
17
+
18
+ desc %Q{Prints the most current tags from all stages}
19
+ task :print_latest_tags, :roles => :app do
20
+ logger.info "AUTO TAGGER: release tag history is:"
21
+ entries = CapistranoHelper.new(variables).release_tag_entries
22
+ entries.each do |entry|
23
+ logger.info entry
24
+ end
25
+ end
26
+
27
+ desc %Q{Reads the text file with the latest tag from the shared directory}
28
+ task :read_tag_from_shared, :roles => :app do
29
+ logger.info "AUTO TAGGER: latest tag deployed to this environment was:"
30
+ run "cat #{shared_path}/released_git_tag.txt"
31
+ end
32
+
33
+ desc %Q{Writes the tag name to a file in the shared directory}
34
+ task :write_tag_to_shared, :roles => :app do
35
+ if exists?(:branch)
36
+ logger.info "AUTO TAGGER: writing tag to shared text file on remote server"
37
+ run "echo '#{branch}' > #{shared_path}/released_git_tag.txt"
38
+ else
39
+ logger.info "AUTO TAGGER: no branch available. Text file was not written to server"
40
+ end
41
+ end
42
+
43
+ desc %Q{Creates a tag using the stage variable}
44
+ task :create_tag, :roles => :app do
45
+ if variables[:stage]
46
+ tag_name = AutoTagger.new(variables[:stage], variables[:working_directory]).create_tag(real_revision)
47
+ logger.info "AUTO TAGGER created tag #{tag_name} from #{real_revision}"
48
+ else
49
+ tag_name = AutoTagger.new(:production, variables[:working_directory]).create_tag
50
+ logger.info "AUTO TAGGER created tag #{tag_name}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,42 @@
1
+ class Repository
2
+
3
+ class NoPathProvidedError < StandardError; end
4
+ class NoSuchPathError < StandardError; end
5
+ class InvalidGitRepositoryError < StandardError; end
6
+ class GitCommandFailedError < StandardError; end
7
+
8
+ attr_reader :path
9
+
10
+ def initialize(path)
11
+ if path.to_s.strip == ""
12
+ raise NoPathProvidedError
13
+ elsif ! File.exists?(path)
14
+ raise NoSuchPathError
15
+ elsif ! File.exists?(File.join(path, ".git"))
16
+ raise InvalidGitRepositoryError
17
+ else
18
+ @path = path
19
+ end
20
+ end
21
+
22
+ def ==(other)
23
+ other.is_a?(Repository) && other.path == path
24
+ end
25
+
26
+ def tags
27
+ @tags ||= Tag.new(self)
28
+ end
29
+
30
+ def commit_for(tag)
31
+ Commander.execute(path, "git --no-pager log #{tag} --pretty=oneline -1")
32
+ end
33
+
34
+ def run(cmd)
35
+ Commander.execute(path, cmd)
36
+ end
37
+
38
+ def run!(cmd)
39
+ Commander.execute!(path, cmd) || raise(GitCommandFailedError)
40
+ end
41
+
42
+ end
@@ -0,0 +1,23 @@
1
+ class StageManager
2
+
3
+ class NoStagesSpecifiedError < StandardError
4
+ def message
5
+ "You must set the :stages variable to an array, like set :stages, [:ci, :demo]"
6
+ end
7
+ end
8
+
9
+ attr_reader :stages
10
+
11
+ def initialize(stages)
12
+ raise NoStagesSpecifiedError unless stages && stages.is_a?(Array)
13
+ @stages = stages.map{|stage| stage.to_s }
14
+ end
15
+
16
+ def previous_stage(stage)
17
+ if stage
18
+ index = stages.index(stage.to_s) - 1
19
+ stages[index] if index > -1
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,41 @@
1
+ # git --no-pager log --pretty=oneline -1
2
+ # git tag -a -m 'Successful continuous integration build on #{timestamp}' #{tag_name}"
3
+ class Tag
4
+
5
+ attr_reader :repository
6
+
7
+ def initialize(repository)
8
+ @repository = repository
9
+ end
10
+
11
+ def find_all
12
+ repository.run("git tag").split("\n")
13
+ end
14
+
15
+ def fetch
16
+ repository.run! "git fetch origin --tags"
17
+ end
18
+
19
+ def latest_from(stage)
20
+ find_all.select{|tag| tag =~ /^#{stage}\//}.sort.last
21
+ end
22
+
23
+ def push
24
+ repository.run! "git push origin --tags"
25
+ end
26
+
27
+ def create(stage, commit = nil)
28
+ tag_name = name_for(stage)
29
+ cmd = "git tag #{tag_name}"
30
+ cmd += " #{commit}" if commit
31
+ repository.run! cmd
32
+ tag_name
33
+ end
34
+
35
+ private
36
+
37
+ def name_for(stage)
38
+ "%s/%s" % [stage, Time.now.utc.strftime('%Y%m%d%H%M%S')]
39
+ end
40
+
41
+ end
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe AutoTagger do
4
+ before(:each) do
5
+ stub(Dir).pwd { File.join(File.dirname(__FILE__), '..', '..') }
6
+ end
7
+
8
+ describe ".new" do
9
+ it "blows up if you don't pass an stage" do
10
+ proc do
11
+ AutoTagger.new(nil)
12
+ end.should raise_error(AutoTagger::EnvironmentCannotBeBlankError)
13
+ end
14
+
15
+ it "sets the stage when it's passed" do
16
+ AutoTagger.new("ci").stage.should == "ci"
17
+ end
18
+
19
+ it "sets the path to Dir.pwd when nil" do
20
+ mock(Dir).pwd { "/foo" }
21
+ mock(Repository).new("/foo")
22
+ AutoTagger.new("ci")
23
+ end
24
+
25
+ it "expands the path when the path is passed" do
26
+ mock(Repository).new(File.expand_path("."))
27
+ AutoTagger.new("ci", ".")
28
+ end
29
+
30
+ it "exposes the working directory" do
31
+ mock(Repository).new(File.expand_path("."))
32
+ AutoTagger.new("ci", ".").working_directory.should == File.expand_path(".")
33
+ end
34
+ end
35
+
36
+ describe "#create_tag" do
37
+ it "generates the correct commands" do
38
+ time = Time.local(2001,1,1)
39
+ mock(Time).now.once {time}
40
+ timestamp = time.utc.strftime('%Y%m%d%H%M%S')
41
+ mock(File).exists?(anything).twice { true }
42
+
43
+ mock(Commander).execute!("/foo", "git fetch origin --tags") {true}
44
+ mock(Commander).execute!("/foo", "git tag ci/#{timestamp}") {true}
45
+ mock(Commander).execute!("/foo", "git push origin --tags") {true}
46
+
47
+ AutoTagger.new("ci", "/foo").create_tag
48
+ end
49
+
50
+ it "allows you to base it off an existing tag or commit" do
51
+ time = Time.local(2001,1,1)
52
+ mock(Time).now.once {time}
53
+ timestamp = time.utc.strftime('%Y%m%d%H%M%S')
54
+ mock(File).exists?(anything).twice { true }
55
+
56
+ mock(Commander).execute!("/foo", "git fetch origin --tags") {true}
57
+ mock(Commander).execute!("/foo", "git tag ci/#{timestamp} guid") {true}
58
+ mock(Commander).execute!("/foo", "git push origin --tags") {true}
59
+
60
+ AutoTagger.new("ci", "/foo").create_tag("guid")
61
+ end
62
+
63
+ it "returns the tag that was created" do
64
+ time = Time.local(2001,1,1)
65
+ mock(Time).now.once {time}
66
+ timestamp = time.utc.strftime('%Y%m%d%H%M%S')
67
+ mock(File).exists?(anything).twice { true }
68
+ mock(Commander).execute!(anything, anything).times(any_times) {true}
69
+
70
+ AutoTagger.new("ci", "/foo").create_tag.should == "ci/#{timestamp}"
71
+ end
72
+ end
73
+
74
+ describe "#latest_tag" do
75
+ it "generates the correct commands" do
76
+ mock(File).exists?(anything).twice { true }
77
+
78
+ mock(Commander).execute!("/foo", "git fetch origin --tags") {true}
79
+ mock(Commander).execute("/foo", "git tag") { "ci_01" }
80
+
81
+ AutoTagger.new("ci", "/foo").latest_tag
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,146 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe CapistranoHelper do
4
+
5
+ describe ".new" do
6
+ it "blows up if there are no stages" do
7
+ proc do
8
+ CapistranoHelper.new({})
9
+ end.should raise_error(StageManager::NoStagesSpecifiedError)
10
+ end
11
+ end
12
+
13
+ describe "#variables" do
14
+ it "returns all variables" do
15
+ CapistranoHelper.new({:autotagger_stages => [:bar]}).variables.should == {:autotagger_stages => [:bar]}
16
+ end
17
+ end
18
+
19
+ describe "#working_directory" do
20
+ it "returns the hashes' working directory value" do
21
+ CapistranoHelper.new({:autotagger_stages => [:bar], :working_directory => "/foo"}).working_directory.should == "/foo"
22
+ end
23
+
24
+ it "defaults to Dir.pwd if it's not set, or it's nil" do
25
+ mock(Dir).pwd { "/bar" }
26
+ CapistranoHelper.new({:autotagger_stages => [:bar]}).working_directory.should == "/bar"
27
+ end
28
+ end
29
+
30
+ describe "#stage" do
31
+ it "returns the hashes' current stage value" do
32
+ CapistranoHelper.new({:autotagger_stages => [:bar], :stage => :bar}).stage.should == :bar
33
+ CapistranoHelper.new({:autotagger_stages => [:bar]}).stage.should be_nil
34
+ end
35
+ end
36
+
37
+ describe "#release_tag_entries" do
38
+ it "returns a column-justifed version of all the commits" do
39
+ mock(Commander).execute("/foo", "git tag").times(any_times) { "ci/01\nstaging/01\nproduction/01" }
40
+ mock(Commander).execute("/foo", "git --no-pager log ci/01 --pretty=oneline -1") { "guid1" }
41
+ mock(Commander).execute("/foo", "git --no-pager log staging/01 --pretty=oneline -1") { "guid2" }
42
+ mock(Commander).execute("/foo", "git --no-pager log production/01 --pretty=oneline -1") { "guid3" }
43
+ mock(Commander).execute!("/foo", "git fetch origin --tags").times(any_times) { true }
44
+ mock(File).exists?(anything).times(any_times) {true}
45
+
46
+ variables = {
47
+ :working_directory => "/foo",
48
+ :autotagger_stages => [:ci, :staging, :production]
49
+ }
50
+ histories = CapistranoHelper.new(variables).release_tag_entries
51
+ histories.length.should == 3
52
+ histories[0].should include("ci/01", "guid1")
53
+ histories[1].should include("staging/01", "guid2")
54
+ histories[2].should include("production/01", "guid3")
55
+ end
56
+
57
+ it "ignores tags delimited with '_'" do
58
+ mock(Commander).execute("/foo", "git tag").times(any_times) { "ci/01\nci_02" }
59
+ mock(Commander).execute("/foo", "git --no-pager log ci/01 --pretty=oneline -1") { "guid1" }
60
+ mock(Commander).execute!("/foo", "git fetch origin --tags").times(any_times) { true }
61
+ mock(File).exists?(anything).times(any_times) {true}
62
+
63
+ variables = {
64
+ :working_directory => "/foo",
65
+ :autotagger_stages => [:ci]
66
+ }
67
+ histories = CapistranoHelper.new(variables).release_tag_entries
68
+ histories.length.should == 1
69
+ histories[0].should include("ci/01", "guid1")
70
+ end
71
+ end
72
+
73
+ describe "#branch" do
74
+ describe "with :head and :branch specified" do
75
+ it "returns master" do
76
+ variables = {
77
+ :autotagger_stages => [:bar],
78
+ :head => nil,
79
+ :branch => "foo"
80
+ }
81
+ CapistranoHelper.new(variables).branch.should == "foo"
82
+ end
83
+ end
84
+
85
+ describe "with :head specified, but no branch specified" do
86
+ it "returns master" do
87
+ variables = {
88
+ :autotagger_stages => [:bar],
89
+ :head => nil
90
+ }
91
+ CapistranoHelper.new(variables).branch.should == nil
92
+ end
93
+ end
94
+
95
+ describe "with :branch specified" do
96
+ it "returns the value of branch" do
97
+ variables = {
98
+ :autotagger_stages => [:bar],
99
+ :branch => "foo"
100
+ }
101
+ CapistranoHelper.new(variables).branch.should == "foo"
102
+ end
103
+ end
104
+
105
+ describe "with a previous stage with a tag" do
106
+ it "returns the latest tag for the previous stage" do
107
+ variables = {
108
+ :autotagger_stages => [:foo, :bar],
109
+ :stage => :bar,
110
+ :branch => "master",
111
+ :working_directory => "/foo"
112
+ }
113
+ tagger = Object.new
114
+ mock(tagger).latest_tag { "foo_01" }
115
+ mock(AutoTagger).new("foo", "/foo") { tagger }
116
+ CapistranoHelper.new(variables).branch.should == "foo_01"
117
+ end
118
+ end
119
+
120
+ describe "with no branch and a previous stage with no tag" do
121
+ it "returns nil" do
122
+ variables = {
123
+ :autotagger_stages => [:foo, :bar],
124
+ :stage => :bar,
125
+ :working_directory => "/foo"
126
+ }
127
+ tagger = Object.new
128
+ mock(tagger).latest_tag { nil }
129
+ mock(AutoTagger).new("foo", "/foo") { tagger }
130
+ CapistranoHelper.new(variables).branch.should == nil
131
+ end
132
+ end
133
+
134
+ describe "with no branch and previous stage" do
135
+ it "returns nil" do
136
+ variables = {
137
+ :autotagger_stages => [:bar],
138
+ :stage => :bar
139
+ }
140
+ CapistranoHelper.new(variables).previous_stage.should be_nil
141
+ CapistranoHelper.new(variables).branch.should == nil
142
+ end
143
+ end
144
+ end
145
+
146
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Commander do
4
+ describe ".execute" do
5
+ it "execute the command and returns the results" do
6
+ mock(Commander).`("cd /foo && ls") { "" } #`
7
+ Commander.execute("/foo", "ls")
8
+ end
9
+ end
10
+
11
+ describe "system" do
12
+ it "executes and doesn't return anything" do
13
+ mock(Commander).system("cd /foo && ls")
14
+ Commander.execute!("/foo", "ls")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Repository do
4
+ describe ".new" do
5
+ it "sets the repo" do
6
+ mock(File).exists?(anything).twice { true }
7
+ repo = Repository.new("/foo")
8
+ repo.path.should == "/foo"
9
+ end
10
+
11
+ it "raises an error when the path is blank" do
12
+ proc do
13
+ Repository.new(" ")
14
+ end.should raise_error(Repository::NoPathProvidedError)
15
+ end
16
+
17
+ it "raises an error when the path is nil" do
18
+ proc do
19
+ Repository.new(nil)
20
+ end.should raise_error(Repository::NoPathProvidedError)
21
+ end
22
+
23
+ it "raises an error with a file that doesn't exist" do
24
+ mock(File).exists?("/foo") { false }
25
+ proc do
26
+ Repository.new("/foo")
27
+ end.should raise_error(Repository::NoSuchPathError)
28
+ end
29
+
30
+ it "raises an error with a non-git repository" do
31
+ mock(File).exists?("/foo") { true }
32
+ mock(File).exists?("/foo/.git") { false }
33
+ proc do
34
+ Repository.new("/foo")
35
+ end.should raise_error(Repository::InvalidGitRepositoryError)
36
+ end
37
+ end
38
+
39
+ describe "#==" do
40
+ it "compares paths" do
41
+ mock(File).exists?(anything).times(any_times) { true }
42
+ Repository.new("/foo").should_not == "/foo"
43
+ Repository.new("/foo").should_not == Repository.new("/bar")
44
+ Repository.new("/foo").should == Repository.new("/foo")
45
+ end
46
+ end
47
+
48
+ describe "#run" do
49
+ it "sends the correct command" do
50
+ mock(File).exists?(anything).twice { true }
51
+ mock(Commander).execute("/foo", "bar")
52
+ Repository.new("/foo").run("bar")
53
+ end
54
+ end
55
+
56
+ describe "run!" do
57
+ it "sends the correct command" do
58
+ mock(File).exists?(anything).twice { true }
59
+ mock(Commander).execute!("/foo", "bar") { true }
60
+ Repository.new("/foo").run!("bar")
61
+ end
62
+
63
+ it "raises an exception if it the command returns false" do
64
+ mock(File).exists?(anything).twice { true }
65
+ mock(Commander).execute!("/foo", "bar") { false }
66
+ proc do
67
+ Repository.new("/foo").run!("bar")
68
+ end.should raise_error(Repository::GitCommandFailedError)
69
+ end
70
+ end
71
+
72
+ end