robot-army-git-deploy 0.0.4

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2007 Wesabe, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.markdown ADDED
@@ -0,0 +1,4 @@
1
+ Robot Army Git Deploy
2
+ =====================
3
+
4
+ Deployment for Robot Army for projects using a git repository.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ begin
2
+ require 'rubygems'
3
+ require 'thor'
4
+
5
+ $stderr.puts "Robot Army uses thor, not rake. Here are the available tasks:"
6
+ exec "thor -T"
7
+ rescue LoadError
8
+ $stderr.puts "Robot Army uses thor, not rake. Please install thor."
9
+ end
@@ -0,0 +1,12 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ # external libraries
4
+ require 'rubygems'
5
+ require 'grit'
6
+ require 'robot-army'
7
+ require 'fileutils'
8
+ require 'highline'
9
+
10
+ # internal files
11
+ require 'robot-army-git-deploy/git_deployer'
12
+ require 'robot-army-git-deploy/grit_ext'
@@ -0,0 +1,251 @@
1
+ module RobotArmy::GitDeployer
2
+ def self.included(base)
3
+ base.const_set(:DEPLOY_COUNT, 5)
4
+
5
+ base.class_eval do
6
+ method_options :target_revision => :string
7
+
8
+ desc "check", "Checks the deploy status"
9
+ def check(opts={})
10
+ update_server_refs
11
+
12
+ run_pager
13
+
14
+ say "Deployed Revisions"
15
+
16
+ deployed_revisions.each do |host, revision|
17
+ if revision
18
+ commit = commit_from_revision_or_abort(revision)
19
+ puts "%s: %s %s [%s]" % [
20
+ host,
21
+ color(commit.id_abbrev, :yellow),
22
+ commit.message.to_a.first.chomp,
23
+ commit.author.name]
24
+ else
25
+ puts "%s: %s %s" % [
26
+ host,
27
+ color('0000000', :yellow),
28
+ "(no deployed revision)"]
29
+ end
30
+ end
31
+
32
+ puts
33
+
34
+ say "On Deck"
35
+
36
+ if oldest_deployed_revision == target_revision
37
+ puts "Deployed revision is up to date"
38
+ elsif oldest_deployed_revision
39
+ shortlog "#{oldest_deployed_revision}..#{target_revision}"
40
+ diff "#{oldest_deployed_revision}..#{target_revision}"
41
+ else
42
+ shortlog target_revision, :root => true
43
+ diff target_revision, :root => true
44
+ end
45
+ end
46
+
47
+ desc "archive", "Write HEAD to a tgz file"
48
+ def archive
49
+ say "Archiving to #{archive_path}"
50
+ %x{git archive --format=tar #{target_revision} | gzip >#{archive_path}}
51
+ end
52
+
53
+ desc "stage", "Stages the locally-generated archive on each host"
54
+ def stage
55
+ revision = repo.commits.first.id
56
+
57
+ # create the destination directory
58
+ sudo do
59
+ FileUtils.mkdir_p(deploy_path)
60
+ FileUtils.chown(user, group, deploy_path)
61
+ end
62
+
63
+ say "Staging #{app} into #{deploy_path}"
64
+ cptemp(archive_path, :user => user) do |path|
65
+ %x{tar -xvz -f #{path} -C #{deploy_path}}
66
+ File.open(File.join(deploy_path, 'REVISION'), 'w') {|f| f << revision}
67
+ path # just so that we don't try to return a File and generate a warning
68
+ end
69
+ end
70
+
71
+ no_tasks do
72
+
73
+ def install
74
+ say "Installing #{app} into #{current_link}"
75
+ sudo do
76
+ FileUtils.rm_f(current_link)
77
+ FileUtils.ln_sf(deploy_path, current_link)
78
+ end
79
+ update_server_refs(true)
80
+ end
81
+
82
+ def cleanup
83
+ clean_temporary_files
84
+ clean_old_revisions
85
+ end
86
+
87
+ def clean_temporary_files
88
+ say "Cleaning up temporary files"
89
+ FileUtils.rm_f("#{app}-archive.tar.gz")
90
+ end
91
+
92
+ def clean_old_revisions
93
+ say "Cleaning up old revisions"
94
+ deploy_count = self.class.const_get(:DEPLOY_COUNT)
95
+
96
+ sudo do
97
+ deploy_paths = Dir.glob(File.join(deploy_root, '*')).sort
98
+ deploy_paths -= [current_link]
99
+ FileUtils.rm_rf(deploy_paths.first(deploy_paths.size - deploy_count)) if deploy_paths.size > deploy_count
100
+ end
101
+ end
102
+
103
+ desc "run", "Run a full deploy"
104
+ def run
105
+ archive
106
+ stage
107
+ install
108
+ cleanup
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def repo
118
+ @repo ||= Grit::Repo.new(Dir.pwd)
119
+ end
120
+
121
+ def git
122
+ repo.git
123
+ end
124
+
125
+ def revfile
126
+ File.join(current_link, 'REVISION')
127
+ end
128
+
129
+ def deployed_revisions
130
+ @deployed_revisions ||= hosts.zip(Array(remote { File.read(revfile).chomp if File.exist?(revfile) }))
131
+ end
132
+
133
+ def clear_deployed_revisions_cache
134
+ @deployed_revisions = nil
135
+ end
136
+
137
+ def deployed_revisions_equal?
138
+ deployed_revisions.map{|host, revision| revision}.uniq.size == 1
139
+ end
140
+
141
+ def target_revision
142
+ options[:target_revision] || repo.head.commit
143
+ end
144
+
145
+ def oldest_deployed_revision
146
+ oldest_commit = deployed_revisions.
147
+ map {|host, revision| repo.commit(revision) if revision}.
148
+ compact.
149
+ sort.
150
+ first
151
+ return oldest_commit.id if oldest_commit
152
+ end
153
+
154
+ def update_server_refs(refresh=false)
155
+ clear_deployed_revisions_cache if refresh
156
+
157
+ deployed_revisions.each do |host, revision|
158
+ # thanks to doener in #git for this idea
159
+ git.update_ref({}, "refs/servers/#{host}", revision) if revision
160
+ end
161
+ end
162
+
163
+ def clear_deployed_refs
164
+ pairs = git.for_each_ref({:format => "%(objectname) %(refname)"}, 'refs/servers').to_a
165
+ pairs.map!{|line| line.chomp.split(' ')}
166
+ pairs.each do |objectname, refname|
167
+ git.update_ref({:d => true}, refname, objectname)
168
+ end
169
+ end
170
+
171
+ def archive_path
172
+ "#{app}-archive.tar.gz"
173
+ end
174
+
175
+ def deploy_root
176
+ File.expand_path(File.join(root, app))
177
+ end
178
+
179
+ def deploy_path
180
+ File.join(deploy_root, timestamp)
181
+ end
182
+
183
+ def current_link
184
+ File.join(deploy_root, 'current')
185
+ end
186
+
187
+ def color(*args)
188
+ (@highline ||= HighLine.new).color(*args)
189
+ end
190
+
191
+ def log(what, options={})
192
+ puts git.log({:pretty=>'oneline', :'abbrev-commit'=>true, :color=>true}.merge(options), what)
193
+ end
194
+
195
+ def shortlog(what, options={})
196
+ puts git.shortlog({:color=>true}.merge(options), what)
197
+ end
198
+
199
+ def diff(what, options={})
200
+ # dumb, dumb, dumb
201
+ puts git.send(:method_missing, :diff, {:stat => true, :color => true}.merge(options), what)
202
+ end
203
+
204
+ def deployed_revision
205
+ deployed_revisions.find{|host, revision| revision}.last
206
+ end
207
+
208
+ def commit_from_revision_or_abort(revision)
209
+ if commit = repo.commits(revision, 1).first
210
+ return commit
211
+ else
212
+ $stderr.puts "#{color('ERROR', :red)}: The deployed revision (#{color(revision, :yellow)}) was not found in your local repository. Perhaps you need to update first?"
213
+ exit(1)
214
+ end
215
+ end
216
+
217
+ def revfile
218
+ File.join(current_link, 'REVISION')
219
+ end
220
+
221
+ def timestamp
222
+ @timestamp ||= Time.now.utc.strftime("%Y-%m-%d-%H-%M-%S")
223
+ end
224
+
225
+
226
+ def run_pager
227
+ return if PLATFORM =~ /win32/
228
+ return unless STDOUT.tty?
229
+
230
+ read, write = IO.pipe
231
+
232
+ unless Kernel.fork # Child process
233
+ STDOUT.reopen(write)
234
+ STDERR.reopen(write) if STDERR.tty?
235
+ read.close
236
+ write.close
237
+ return
238
+ end
239
+
240
+ # Parent process, become pager
241
+ STDIN.reopen(read)
242
+ read.close
243
+ write.close
244
+
245
+ ENV['LESS'] = 'FSRX' # Don't page if the input is short enough
246
+
247
+ Kernel.select [STDIN] # Wait until we have input before we start the pager
248
+ pager = ENV['PAGER'] || 'less'
249
+ exec pager rescue exec "/bin/sh", "-c", pager
250
+ end
251
+ end
@@ -0,0 +1,18 @@
1
+ class Grit::Commit
2
+ include Comparable
3
+
4
+ def <=>(other)
5
+ raise ArgumentError unless other.is_a?(Grit::Commit)
6
+
7
+ if id == other.id
8
+ return 0
9
+ elsif not @repo.commits("#{id}..#{other.id}").empty?
10
+ return -1
11
+ elsif not @repo.commits("#{other.id}..#{id}").empty?
12
+ return 1
13
+ else
14
+ raise ArgumentError,
15
+ "#{other.inspect} is not an ancestor of #{self.inspect} or vice-versa, and are therefor not comparable"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,230 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class Deploy < RobotArmy::TaskMaster
4
+ include RobotArmy::GitDeployer
5
+
6
+ hosts %w[test1 test2 test3]
7
+
8
+ def remote(hosts=self.class.hosts)
9
+ hosts.size == 1 ? yield : hosts.map { yield }
10
+ end
11
+
12
+ alias_method :sudo, :remote
13
+
14
+ def root
15
+ "/opt"
16
+ end
17
+
18
+ def app
19
+ "test"
20
+ end
21
+
22
+ def user
23
+ "nobody"
24
+ end
25
+
26
+ def group
27
+ "nobody"
28
+ end
29
+
30
+ def color(str, color)
31
+ str
32
+ end
33
+
34
+ # make all methods public so we can test 'em easily
35
+ private_instance_methods.each { |m| public m }
36
+ end
37
+
38
+ class FakeCommit
39
+ attr_reader :id
40
+ include Comparable
41
+
42
+ def initialize(id)
43
+ @id = id
44
+ end
45
+
46
+ def <=>(other)
47
+ id <=> other.id
48
+ end
49
+ end
50
+
51
+ describe RobotArmy::GitDeployer do
52
+ before do
53
+ @deploy = Deploy.new
54
+ @deploy.options = (@options = {})
55
+ end
56
+
57
+ describe "tasks" do
58
+ it "defines a 'check' task" do
59
+ Deploy.tasks['check'].must be_an_instance_of(Thor::Task)
60
+ end
61
+
62
+ it "defines a 'run' task" do
63
+ Deploy.tasks['run'].must be_an_instance_of(Thor::Task)
64
+ end
65
+
66
+ it "defines a 'cleanup' task" do
67
+ Deploy.tasks['cleanup'].must be_an_instance_of(Thor::Task)
68
+ end
69
+
70
+ it "defines a 'install' task" do
71
+ Deploy.tasks['install'].must be_an_instance_of(Thor::Task)
72
+ end
73
+
74
+ it "defines a 'archive' task" do
75
+ Deploy.tasks['archive'].must be_an_instance_of(Thor::Task)
76
+ end
77
+
78
+ it "defines a 'stage' task" do
79
+ Deploy.tasks['stage'].must be_an_instance_of(Thor::Task)
80
+ end
81
+ end
82
+
83
+ describe "plumbing" do
84
+ describe "when the revfile exists on all hosts" do
85
+ before do
86
+ File.stub!(:exist?).with(@deploy.revfile).any_number_of_times.and_return(true)
87
+ File.stub!(:read).with(@deploy.revfile).any_number_of_times.and_return("abcde\n")
88
+ end
89
+
90
+ it "can get the list of deployed revisions" do
91
+ @deploy.deployed_revisions.must == [%w[test1 abcde], %w[test2 abcde], %w[test3 abcde]]
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "when the revfile doesn't exist on a host" do
97
+ before do
98
+ File.stub!(:exist?).with(@deploy.revfile).and_return(true, true, false)
99
+ File.stub!(:read).with(@deploy.revfile).twice.and_return("abcde\n")
100
+ end
101
+
102
+ it "returns nil for that host's revision" do
103
+ @deploy.deployed_revisions.must == [%w[test1 abcde], %w[test2 abcde], ['test3', nil]]
104
+ end
105
+ end
106
+
107
+ describe "oldest_deployed_revision" do
108
+ describe "when all revisions are equal" do
109
+ before do
110
+ @deploy.stub!(:repo).and_return(stub(:repo, :commit => FakeCommit.new('abcde')))
111
+ end
112
+
113
+ it "sorts commits and returns the first (oldest) one" do
114
+ @deploy.
115
+ stub!(:deployed_revisions).
116
+ and_return([%w[test1 abcde], %w[test2 abcde], %w[test3 abcde]])
117
+
118
+ @deploy.oldest_deployed_revision.must == 'abcde'
119
+ end
120
+ end
121
+
122
+ describe "when not all revisions are equal" do
123
+ before do
124
+ @repo = stub(:repo)
125
+ @repo.
126
+ stub!(:commit).
127
+ and_return(FakeCommit.new('cdeab'), FakeCommit.new('abcde'), FakeCommit.new('deabc'))
128
+
129
+ @deploy.
130
+ stub!(:repo).
131
+ and_return(@repo)
132
+ end
133
+
134
+ it "sorts commits and returns the first (oldest) one" do
135
+ @deploy.
136
+ stub!(:deployed_revisions).
137
+ and_return([%w[test1 cdeab], %w[test2 abcde], %w[test3 deabc]])
138
+
139
+ @deploy.oldest_deployed_revision.must == 'abcde'
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "target_revision" do
145
+ it "defaults to the current HEAD" do
146
+ @deploy.stub!(:repo).and_return(stub(:repo, :commits => [FakeCommit.new('abcde')]))
147
+ @deploy.target_revision.must == 'abcde'
148
+ end
149
+
150
+ it "can be set as an option" do
151
+ @options[:target_revision] = 'production'
152
+ @deploy.target_revision.must == 'production'
153
+ end
154
+ end
155
+
156
+ describe "commit_from_revision_or_abort" do
157
+ describe "given a revision in the repository" do
158
+ before do
159
+ @commit = FakeCommit.new('abcde')
160
+ @deploy.stub!(:repo).and_return(stub(:repo, :commits => [@commit]))
161
+ end
162
+
163
+ it "returns the commit for that revision" do
164
+ @deploy.commit_from_revision_or_abort('abcde').must == @commit
165
+ end
166
+ end
167
+
168
+ describe "given a revision not in the repository" do
169
+ before do
170
+ @deploy.stub!(:repo).and_return(stub(:repo, :commits => []))
171
+ end
172
+
173
+ it "warns about the missing commit and exits" do
174
+ @deploy.should_receive(:exit).with(1)
175
+ capture(:stderr) { @deploy.commit_from_revision_or_abort('abcde') }.
176
+ must == "ERROR: The deployed revision (abcde) was not found in your local repository. Perhaps you need to update first?\n"
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "clean_old_revisions" do
182
+ def deployed_revision_paths_for_range(range, include_current=true)
183
+ (include_current ? [@deploy.current_link] : []) + range.map {|i| File.join(@deploy.deploy_root, "deploy-#{i}")}
184
+ end
185
+
186
+ describe "when there are no deployed revisions" do
187
+ before do
188
+ Dir.stub!(:glob).with(File.join(@deploy.deploy_root, '*')).and_return(deployed_revision_paths_for_range(1...1))
189
+ end
190
+
191
+ it "does not remove anything" do
192
+ FileUtils.should_not_receive(:rm_rf)
193
+ silence(:stdout) { @deploy.clean_old_revisions }
194
+ end
195
+ end
196
+
197
+ describe "when there are less than DEPLOY_COUNT deployed revisions" do
198
+ before do
199
+ Dir.stub!(:glob).with(File.join(@deploy.deploy_root, '*')).and_return(deployed_revision_paths_for_range(1...Deploy::DEPLOY_COUNT))
200
+ end
201
+
202
+ it "does not remove anything" do
203
+ FileUtils.should_not_receive(:rm_rf)
204
+ silence(:stdout) { @deploy.clean_old_revisions }
205
+ end
206
+ end
207
+
208
+ describe "when there are exactly DEPLOY_COUNT deployed revisions" do
209
+ before do
210
+ Dir.stub!(:glob).with(File.join(@deploy.deploy_root, '*')).and_return(deployed_revision_paths_for_range(1..Deploy::DEPLOY_COUNT))
211
+ end
212
+
213
+ it "does not remove anything" do
214
+ FileUtils.should_not_receive(:rm_rf)
215
+ silence(:stdout) { @deploy.clean_old_revisions }
216
+ end
217
+ end
218
+
219
+ describe "when there are more than DEPLOY_COUNT deployed revisions" do
220
+ before do
221
+ Dir.stub!(:glob).with(File.join(@deploy.deploy_root, '*')).and_return(deployed_revision_paths_for_range(1..(Deploy::DEPLOY_COUNT+2)))
222
+ end
223
+
224
+ it "removes enough of the oldest deployed revisions to get the count down to DEPLOY_COUNT" do
225
+ FileUtils.should_receive(:rm_rf).with(deployed_revision_paths_for_range(1..2, false)).exactly(@deploy.hosts.size).times
226
+ silence(:stdout) { @deploy.clean_old_revisions }
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,26 @@
1
+ $TESTING=true
2
+ load File.join(File.dirname(__FILE__), '..', 'lib', 'robot-army-git-deploy.rb')
3
+
4
+ module Spec::Expectations::ObjectExpectations
5
+ alias_method :must, :should
6
+ alias_method :must_not, :should_not
7
+ undef_method :should
8
+ undef_method :should_not
9
+ end
10
+
11
+ Spec::Runner.configure do |config|
12
+ def capture(stream)
13
+ begin
14
+ stream = stream.to_s
15
+ eval "$#{stream} = StringIO.new"
16
+ yield
17
+ result = eval("$#{stream}").string
18
+ ensure
19
+ eval("$#{stream} = #{stream.upcase}")
20
+ end
21
+
22
+ result
23
+ end
24
+
25
+ alias silence capture
26
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: robot-army-git-deploy
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 4
9
+ version: 0.0.4
10
+ platform: ruby
11
+ authors:
12
+ - Brian Donovan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2009-09-23 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: robot-army
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 1
30
+ - 7
31
+ version: 0.1.7
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: thor
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ - 11
44
+ - 3
45
+ version: 0.11.3
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: grit
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">"
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ - 0
58
+ - 0
59
+ version: 0.0.0
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: highline
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">"
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ - 0
72
+ - 0
73
+ version: 0.0.0
74
+ type: :runtime
75
+ version_requirements: *id004
76
+ description: Robot Army deployment with git repositories
77
+ email: brian@wesabe.com
78
+ executables: []
79
+
80
+ extensions: []
81
+
82
+ extra_rdoc_files:
83
+ - LICENSE
84
+ - README.markdown
85
+ files:
86
+ - LICENSE
87
+ - README.markdown
88
+ - Rakefile
89
+ - lib/robot-army-git-deploy.rb
90
+ - lib/robot-army-git-deploy/git_deployer.rb
91
+ - lib/robot-army-git-deploy/grit_ext.rb
92
+ has_rdoc: true
93
+ homepage: http://github.com/wesabe/robot-army-git-deploy
94
+ licenses: []
95
+
96
+ post_install_message:
97
+ rdoc_options:
98
+ - --charset=UTF-8
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ requirements: []
116
+
117
+ rubyforge_project: robot-army-git-deploy
118
+ rubygems_version: 1.3.6
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Robot Army deployment with git repositories
122
+ test_files:
123
+ - spec/git_deployer_spec.rb
124
+ - spec/spec_helper.rb