auto_tagger 0.0.9
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.
- data/MIT-LICENSE +20 -0
- data/README.md +178 -0
- data/bin/autotag +38 -0
- data/lib/auto_tagger/auto_tagger.rb +26 -0
- data/lib/auto_tagger/capistrano_helper.rb +39 -0
- data/lib/auto_tagger/commander.rb +15 -0
- data/lib/auto_tagger/repository.rb +42 -0
- data/lib/auto_tagger/stage_manager.rb +23 -0
- data/lib/auto_tagger/tag.rb +41 -0
- data/lib/auto_tagger.rb +10 -0
- data/recipes/release_tagger.rb +55 -0
- data/spec/auto_tagger/auto_tagger_spec.rb +85 -0
- data/spec/auto_tagger/capistrano_helper_spec.rb +146 -0
- data/spec/auto_tagger/commander_spec.rb +17 -0
- data/spec/auto_tagger/repository_spec.rb +72 -0
- data/spec/auto_tagger/stage_manager_spec.rb +30 -0
- data/spec/auto_tagger/tag_spec.rb +66 -0
- data/spec/spec_helper.rb +7 -0
- metadata +82 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [Jeff Dean]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# IMPORTANT NOTE
|
2
|
+
|
3
|
+
This gem is still in it's infancy, and lots of things might change. Since this creates and pushes tags to your git repository, please use with caution.
|
4
|
+
|
5
|
+
# AutoTagger
|
6
|
+
|
7
|
+
AutoTagger is a gem that helps you automatically create a date-stamped tag for each stage of your deployment, and deploy from the last tag from the previous environment.
|
8
|
+
|
9
|
+
Let's say you have the following workflow:
|
10
|
+
|
11
|
+
* Run all test on a Continuous Integration (CI) server
|
12
|
+
* Deploy to a staging server
|
13
|
+
* Deploy to a production server
|
14
|
+
|
15
|
+
You can use the `autotag` command to tag releases on your CI box, then use the capistrano tasks to auto-tag each release.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
gem sources -a http://gems.github.com
|
20
|
+
sudo gem install zilkey-auto_tagger
|
21
|
+
|
22
|
+
## Contribute
|
23
|
+
|
24
|
+
* [Tracker Project](http://www.pivotaltracker.com/projects/11988)
|
25
|
+
* [GitHub Repository](http://github.com/zilkey/auto_tagger/tree/master)
|
26
|
+
|
27
|
+
## The autotag executable
|
28
|
+
|
29
|
+
Installing the gem creates an executable file named autotag, which takes two arguments: the stage, and optionally the path to the git repo:
|
30
|
+
|
31
|
+
$ autotag demo # => creates a tag like demo/200804041234 in the current directory
|
32
|
+
$ autotag demo . # => same as above
|
33
|
+
$ autotag demo /Users/me/foo # => cd's to /Users/me/foo before creating the tag
|
34
|
+
|
35
|
+
Running autotag does the following:
|
36
|
+
|
37
|
+
$ git fetch origin --tags
|
38
|
+
$ git tag <stage>/<timestamp>
|
39
|
+
$ git push origin --tags
|
40
|
+
|
41
|
+
## Capistrano Integration
|
42
|
+
|
43
|
+
AutoTagger comes with 2 capistrano tasks:
|
44
|
+
|
45
|
+
* `release_tagger:set_branch` tries to set the branch to the last tag from the previous environment.
|
46
|
+
* `release_tagger:create_tag` runs autotag for the current stage
|
47
|
+
|
48
|
+
Example `config/deploy.rb` file:
|
49
|
+
|
50
|
+
require 'release_tagger'
|
51
|
+
|
52
|
+
# The :autotagger_stages variable is required
|
53
|
+
set :autotagger_stages, [:ci, :staging, :production]
|
54
|
+
|
55
|
+
# The :working_directory variable is optional, and defaults to Dir.pwd
|
56
|
+
# :working_directory can be an absolute or relative path
|
57
|
+
set :working_directory, "../../"
|
58
|
+
|
59
|
+
task :production do
|
60
|
+
# In each of your environments that need auto-branch setting, you need to set :stage
|
61
|
+
set :stage, :production
|
62
|
+
end
|
63
|
+
|
64
|
+
task :staging do
|
65
|
+
# If you do not set stage, it will not auto-set your branch
|
66
|
+
# set :stage, :staging
|
67
|
+
end
|
68
|
+
|
69
|
+
# You need to add the before/ater callbacks yourself
|
70
|
+
before "deploy:update_code", "release_tagger:set_branch"
|
71
|
+
after "deploy", "release_tagger:create_tag"
|
72
|
+
after "deploy", "release_tagger:write_tag_to_shared"
|
73
|
+
after "deploy", "release_tagger:print_latest_tags"
|
74
|
+
|
75
|
+
### Cpistano-ext multistage support
|
76
|
+
|
77
|
+
If you use capistano-ext multistage, you can use auto_tagger.
|
78
|
+
|
79
|
+
set :autotagger_stages, [:ci, :staging, :production]
|
80
|
+
set :stages, [:staging, :production]
|
81
|
+
set :default_stage, :staging
|
82
|
+
require 'capistrano/ext/multistage'
|
83
|
+
|
84
|
+
When you deploy, autotagger will auto-detect your current stage.
|
85
|
+
|
86
|
+
### release_tagger:set_branch
|
87
|
+
|
88
|
+
This task sets the git branch to the latest tag from the previous stage. Assume you have the following tags in your git repository:
|
89
|
+
|
90
|
+
* ci/01
|
91
|
+
* staging/01
|
92
|
+
* production/01
|
93
|
+
|
94
|
+
And the following stages in your capistrano file:
|
95
|
+
|
96
|
+
set :autotagger_stages, [:ci, :staging, :production]
|
97
|
+
|
98
|
+
The deployments would look like this:
|
99
|
+
|
100
|
+
cap staging release_tagger:set_branch # => sets branch to ci/01
|
101
|
+
cap production release_tagger:set_branch # => sets branch to staging/01
|
102
|
+
|
103
|
+
You can override with with the -Shead and -Stag options
|
104
|
+
|
105
|
+
cap staging release_tagger:set_branch -Shead=true # => sets branch to master
|
106
|
+
cap staging release_tagger:set_branch -Stag=staging/01 # => sets branch to staging/01
|
107
|
+
|
108
|
+
If you add `before "deploy:update_code", "release_tagger:set_branch"`, you can just deploy with:
|
109
|
+
|
110
|
+
cap staging deploy
|
111
|
+
|
112
|
+
and the branch will be set for you automatically.
|
113
|
+
|
114
|
+
### release_tagger:create_tag
|
115
|
+
|
116
|
+
This cap task creates a new tag, based on the latest tag from the previous environment.
|
117
|
+
|
118
|
+
If there is no tag from the previous stage, it creates a new tag from the latest commit in your _working directory_.
|
119
|
+
|
120
|
+
If you don't specify any `autotagger_stages`, autotagger will create a tag that starts with "production".
|
121
|
+
|
122
|
+
### release_tagger:print_latest_tags
|
123
|
+
|
124
|
+
This task reads the git version from the text file in shared:
|
125
|
+
|
126
|
+
cap staging release_tagger:read_tag_from_shared
|
127
|
+
|
128
|
+
### release_tagger:print_latest_tags
|
129
|
+
|
130
|
+
This task takes the latest tag from each environment and prints it to the screen. You can add it to your deploy.rb like so:
|
131
|
+
|
132
|
+
after "deploy", "release_tagger:print_latest_tags"
|
133
|
+
|
134
|
+
Or call it directly, like:
|
135
|
+
|
136
|
+
cap production release_tagger:print_latest_tags
|
137
|
+
|
138
|
+
This will produce output like:
|
139
|
+
|
140
|
+
** AUTO TAGGER: release tag history is:
|
141
|
+
** ci ci/20090331045345 8031807feb5f4f99dd83257cdc07081fa6080cba some commit message
|
142
|
+
** staging staging/20090331050908 8031807feb5f4f99dd83257cdc07081fa6080cba some commit message
|
143
|
+
** production production/20090331050917 8031807feb5f4f99dd83257cdc07081fa6080cba some commit message
|
144
|
+
|
145
|
+
## Running tests:
|
146
|
+
|
147
|
+
You must be able to ssh into your box via localhost (remote login). To make this easier, add your own key to your own account:
|
148
|
+
|
149
|
+
cat ~/.ssh/id_rsa.pub >>~/.ssh/authorized_keys
|
150
|
+
|
151
|
+
To ensure that this has worked, try this:
|
152
|
+
|
153
|
+
ssh localhost
|
154
|
+
|
155
|
+
If it asks you for a password, you've done something wrong.
|
156
|
+
|
157
|
+
To run the specs, execute:
|
158
|
+
|
159
|
+
spec spec/
|
160
|
+
|
161
|
+
To run the cucumber features, execute:
|
162
|
+
|
163
|
+
cucumber features/
|
164
|
+
|
165
|
+
## Acknowledgments
|
166
|
+
|
167
|
+
Special thanks to
|
168
|
+
|
169
|
+
* Brian Takita for the original recipes
|
170
|
+
* Mike Dalessio for his git fu
|
171
|
+
* Chad Wooley for his feature ideas
|
172
|
+
* Tim Holahan for his QA
|
173
|
+
|
174
|
+
## Links
|
175
|
+
|
176
|
+
* http://codeintensity.blogspot.com/2008/06/changelogs-and-deployment-notification.html
|
177
|
+
|
178
|
+
Copyright (c) 2009 [Jeff Dean], released under the MIT license
|
data/bin/autotag
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "auto_tagger"))
|
3
|
+
require 'getoptlong'
|
4
|
+
|
5
|
+
opts = GetoptLong.new(
|
6
|
+
['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
|
7
|
+
)
|
8
|
+
|
9
|
+
def usage
|
10
|
+
puts
|
11
|
+
puts "USAGE: #{File.basename($0)} <stage> [<repository>]"
|
12
|
+
puts
|
13
|
+
puts ' where: stage sets the tag prefix'
|
14
|
+
puts ' repository sets the repository to act on - defualts to the current directory'
|
15
|
+
puts
|
16
|
+
puts ' examples: autotag'
|
17
|
+
puts ' autotag .'
|
18
|
+
puts ' autotag ../'
|
19
|
+
puts ' autotag /data/myrepo'
|
20
|
+
puts ' autotag demo'
|
21
|
+
puts
|
22
|
+
puts
|
23
|
+
exit 0
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.each do |opt, arg|
|
27
|
+
case opt
|
28
|
+
when "--help"
|
29
|
+
usage
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if ARGV[0]
|
34
|
+
AutoTagger.new(ARGV[0], ARGV[1]).create_tag
|
35
|
+
exit 0
|
36
|
+
else
|
37
|
+
usage
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class AutoTagger
|
2
|
+
|
3
|
+
class EnvironmentCannotBeBlankError < StandardError; end
|
4
|
+
|
5
|
+
attr_reader :stage, :repository, :working_directory
|
6
|
+
|
7
|
+
def initialize(stage, path = nil)
|
8
|
+
raise EnvironmentCannotBeBlankError if stage.to_s.strip == ""
|
9
|
+
@working_directory = File.expand_path(path ||= Dir.pwd)
|
10
|
+
@repository = Repository.new(@working_directory)
|
11
|
+
@stage = stage
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_tag(commit = nil)
|
15
|
+
repository.tags.fetch
|
16
|
+
new_tag = repository.tags.create(stage, commit)
|
17
|
+
repository.tags.push
|
18
|
+
new_tag
|
19
|
+
end
|
20
|
+
|
21
|
+
def latest_tag
|
22
|
+
repository.tags.fetch
|
23
|
+
repository.tags.latest_from(stage)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class CapistranoHelper
|
2
|
+
|
3
|
+
attr_reader :variables, :stage, :working_directory
|
4
|
+
|
5
|
+
def initialize(variables)
|
6
|
+
@stage_manager = StageManager.new(variables[:autotagger_stages])
|
7
|
+
@variables = variables
|
8
|
+
@stage = variables[:stage]
|
9
|
+
@working_directory = variables[:working_directory] || Dir.pwd
|
10
|
+
end
|
11
|
+
|
12
|
+
def previous_stage
|
13
|
+
@stage_manager.previous_stage(stage)
|
14
|
+
end
|
15
|
+
|
16
|
+
def branch
|
17
|
+
if variables.has_key?(:head)
|
18
|
+
variables[:branch]
|
19
|
+
elsif variables.has_key?(:tag)
|
20
|
+
variables[:tag]
|
21
|
+
elsif previous_stage && (latest = AutoTagger.new(previous_stage, working_directory).latest_tag)
|
22
|
+
latest
|
23
|
+
else
|
24
|
+
variables[:branch]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def release_tag_entries
|
29
|
+
entries = []
|
30
|
+
@stage_manager.stages.each do |stage|
|
31
|
+
tagger = AutoTagger.new(stage, working_directory)
|
32
|
+
tag = tagger.latest_tag
|
33
|
+
commit = tagger.repository.commit_for(tag)
|
34
|
+
entries << "#{stage.to_s.ljust(10, " ")} #{tag.to_s.ljust(30, " ")} #{commit.to_s}"
|
35
|
+
end
|
36
|
+
entries
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Commander
|
2
|
+
class << self
|
3
|
+
def execute(path, cmd)
|
4
|
+
`#{command_in_context(path, cmd)}`
|
5
|
+
end
|
6
|
+
|
7
|
+
def execute!(path, cmd)
|
8
|
+
system command_in_context(path, cmd)
|
9
|
+
end
|
10
|
+
|
11
|
+
def command_in_context(path, cmd)
|
12
|
+
"cd #{path} && #{cmd}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
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
|
14
|
+
end
|
15
|
+
|
16
|
+
def previous_stage(stage)
|
17
|
+
if stage
|
18
|
+
index = stages.index(stage) - 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
|
data/lib/auto_tagger.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "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
|
+
previous_tag = AutoTagger.new(StageManager.new(autotagger_stages).previous_stage(stage), Dir.pwd).latest_tag
|
47
|
+
tag_name = AutoTagger.new(variables[:stage], variables[:working_directory]).create_tag(previous_tag)
|
48
|
+
logger.info "AUTO TAGGER created tag #{tag_name} from #{previous_tag.inspect}"
|
49
|
+
else
|
50
|
+
tag_name = AutoTagger.new(:production, variables[:working_directory]).create_tag
|
51
|
+
logger.info "AUTO TAGGER created tag #{tag_name}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe StageManager do
|
4
|
+
|
5
|
+
describe ".new" do
|
6
|
+
[nil, ""].each do |value|
|
7
|
+
it "blows up if there are stages == #{value.inspect}" do
|
8
|
+
proc do
|
9
|
+
StageManager.new(value)
|
10
|
+
end.should raise_error(StageManager::NoStagesSpecifiedError)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#previous_stage" do
|
16
|
+
it "returns the previous stage if there is more than one stage, and there is a current stage" do
|
17
|
+
StageManager.new([:foo, :bar]).previous_stage(:bar).should == :foo
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns nil if there is no previous stage" do
|
21
|
+
StageManager.new([:bar]).previous_stage(:bar).should be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns nil if there is no current stage" do
|
25
|
+
StageManager.new([:bar]).previous_stage(nil).should be_nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Tag do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@repository = Object.new
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ".new" do
|
10
|
+
it "sets the repository" do
|
11
|
+
Tag.new(@repository).repository.should == @repository
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#find_all" do
|
16
|
+
it "returns an array of tags" do
|
17
|
+
mock(@repository).run("git tag") { "ci_01\nci_02" }
|
18
|
+
Tag.new(@repository).find_all.should == ["ci_01", "ci_02"]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns an empty array if there are none" do
|
22
|
+
mock(@repository).run("git tag") { "" }
|
23
|
+
Tag.new(@repository).find_all.should be_empty
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#latest_from" do
|
28
|
+
before do
|
29
|
+
@tag = Tag.new(@repository)
|
30
|
+
mock(@tag).find_all { ["ci/01", "ci/02"] }
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns the latest tag that starts with the specified stage" do
|
34
|
+
@tag.latest_from(:ci).should == "ci/02"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns nil if none match" do
|
38
|
+
@tag.latest_from(:staging).should be_nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#fetch_tags" do
|
43
|
+
it "sends the correct command" do
|
44
|
+
mock(@repository).run!("git fetch origin --tags")
|
45
|
+
Tag.new(@repository).fetch
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#push" do
|
50
|
+
it "sends the correct command" do
|
51
|
+
mock(@repository).run!("git push origin --tags")
|
52
|
+
Tag.new(@repository).push
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#create" do
|
57
|
+
it "creates the right command and returns the name" do
|
58
|
+
time = Time.local(2001,1,1)
|
59
|
+
mock(Time).now.once {time}
|
60
|
+
tag_name = "ci/#{time.utc.strftime('%Y%m%d%H%M%S')}"
|
61
|
+
mock(@repository).run!("git tag #{tag_name}")
|
62
|
+
Tag.new(@repository).create("ci").should == tag_name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: auto_tagger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeff Dean
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-28 00:00:00 -04:00
|
13
|
+
default_executable: autotag
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: capistrano
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.5.3
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email:
|
27
|
+
executables:
|
28
|
+
- autotag
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- lib/auto_tagger/auto_tagger.rb
|
35
|
+
- lib/auto_tagger/capistrano_helper.rb
|
36
|
+
- lib/auto_tagger/commander.rb
|
37
|
+
- lib/auto_tagger/repository.rb
|
38
|
+
- lib/auto_tagger/stage_manager.rb
|
39
|
+
- lib/auto_tagger/tag.rb
|
40
|
+
- lib/auto_tagger.rb
|
41
|
+
- recipes/release_tagger.rb
|
42
|
+
- bin/autotag
|
43
|
+
- spec/auto_tagger/auto_tagger_spec.rb
|
44
|
+
- spec/auto_tagger/capistrano_helper_spec.rb
|
45
|
+
- spec/auto_tagger/commander_spec.rb
|
46
|
+
- spec/auto_tagger/repository_spec.rb
|
47
|
+
- spec/auto_tagger/stage_manager_spec.rb
|
48
|
+
- spec/auto_tagger/tag_spec.rb
|
49
|
+
- spec/spec_helper.rb
|
50
|
+
- MIT-LICENSE
|
51
|
+
- README.md
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://github.com/zilkey/git_tagger/tree/master
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
- recipes
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.3.3
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: Helps you automatically create tags for each stage in a multi-stage deploment and deploy from the latest tag from the previous environment
|
81
|
+
test_files: []
|
82
|
+
|