hudson_deployer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/Manifest +5 -0
- data/README.md +100 -0
- data/Rakefile +12 -0
- data/hudson_deployer.gemspec +29 -0
- data/lib/hudson_deployer.rb +289 -0
- metadata +80 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Collin VanDyck
|
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/Manifest
ADDED
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
## Overview
|
2
|
+
|
3
|
+
This gem was written to help simplify Java deployments using fat-jar hudson builds.
|
4
|
+
|
5
|
+
## Example Capfile
|
6
|
+
|
7
|
+
require 'hudson_deployer'
|
8
|
+
|
9
|
+
set :hudson, "build.company.com"
|
10
|
+
set :application, "project"
|
11
|
+
set :build, "project-release"
|
12
|
+
set :version, "2.0.0"
|
13
|
+
set :deployer, "deploy"
|
14
|
+
|
15
|
+
config[:default] = {
|
16
|
+
:jdbc_url => "jdbc:mysql://localhost:3306/project"
|
17
|
+
}
|
18
|
+
|
19
|
+
config[:staging] = {
|
20
|
+
:user => "cvandyck",
|
21
|
+
:roles => {
|
22
|
+
:app => "192.168.185.132"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
config[:production] = {
|
27
|
+
:user => "admin",
|
28
|
+
:roles => {
|
29
|
+
:app => "production-001"
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
before :deploy do
|
34
|
+
if @env == :production
|
35
|
+
run "#{sudo} apt-get install sun-java6-jdk dbus hal -y"
|
36
|
+
else
|
37
|
+
run "#{sudo} apt-get install openjdk-6-jdk dbus hal -y"
|
38
|
+
end
|
39
|
+
create_user deployer, :homedir => "/opt/project", :shell => "/bin/sh"
|
40
|
+
create_directory "/var/log/project"
|
41
|
+
touch "/var/log/project/sysout.log"
|
42
|
+
end
|
43
|
+
|
44
|
+
after :deploy do
|
45
|
+
render_template "/etc/project.conf", :template => "project.conf.erb", :mode => "0600"
|
46
|
+
render_template "/etc/project.jvm.conf", :template => "#{@env}/project.jvm.conf.erb", :mode => "0644"
|
47
|
+
upstart "/etc/init/project.conf", :template => "upstart.conf.erb"
|
48
|
+
run "#{sudo} stop project || true"
|
49
|
+
sleep 5
|
50
|
+
run "#{sudo} start project"
|
51
|
+
end
|
52
|
+
|
53
|
+
## Templates
|
54
|
+
|
55
|
+
Templates are kept in a folder called "templates". They can be named anything, but to make your life easier suffix them with .erb. Render templates with
|
56
|
+
|
57
|
+
render_template(/etc/broccoli, :template => "broccoli.erb")
|
58
|
+
|
59
|
+
Note that the :template parameter assumes a path relative to the templates folder.
|
60
|
+
|
61
|
+
Templates are rendered using the same scope binding as the gem itself. They therefore can access configuration (described below).
|
62
|
+
|
63
|
+
## Upstart
|
64
|
+
|
65
|
+
Servers are bounced using upstart. In the above example, we use upstart to render an upstart configuration file for our project. Actual upstart commands are done using the Capistrano run command
|
66
|
+
|
67
|
+
## Staging vs Production
|
68
|
+
|
69
|
+
It is assumed that unless otherwise specified, it will be run in staging mode. To use production,
|
70
|
+
|
71
|
+
cap production deploy
|
72
|
+
|
73
|
+
## Configuration
|
74
|
+
|
75
|
+
Configuration can be set on a per-environment basis or for all environments. Set configuration variables using the config hash:
|
76
|
+
|
77
|
+
config[:default] = {
|
78
|
+
:jdbc_url => "jdbc:mysql://localhost:3306/project"
|
79
|
+
}
|
80
|
+
|
81
|
+
Set per-environment configuration:
|
82
|
+
|
83
|
+
config[:production] = {
|
84
|
+
:user => "admin",
|
85
|
+
:roles => {
|
86
|
+
:app => "production-001"
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
The :user and :roles keys are special and change the way that Capistrano works, as expected. You may also set any other key/value pair you wish. The combined configuration is available through the config_h parameter:
|
91
|
+
|
92
|
+
"database": {
|
93
|
+
"jdbc_url": "<%= config_h[:jdbc_url] %>",
|
94
|
+
"driver_class": "com.vertica.Driver"
|
95
|
+
}
|
96
|
+
|
97
|
+
## Other Stuff
|
98
|
+
|
99
|
+
© Collin VanDyck 2011. Distributed under the MIT license.
|
100
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
|
5
|
+
Echoe.new('hudson_deployer', '0.0.1') do |p|
|
6
|
+
p.description = "Unmagical Capistrano deployment using Hudson"
|
7
|
+
p.url = "https://github.com/collinvandyck/hudson_deployer"
|
8
|
+
p.author = "Collin VanDyck"
|
9
|
+
p.email = "collinvandyck @nospam@ gmail.com"
|
10
|
+
p.ignore_pattern = ["tmp/*", "script/*"]
|
11
|
+
p.development_dependencies = []
|
12
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{hudson_deployer}
|
5
|
+
s.version = "0.0.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Collin VanDyck"]
|
9
|
+
s.date = %q{2011-03-16}
|
10
|
+
s.description = %q{Unmagical Capistrano deployment using Hudson}
|
11
|
+
s.email = %q{collinvandyck @nospam@ gmail.com}
|
12
|
+
s.extra_rdoc_files = ["LICENSE", "README.md", "lib/hudson_deployer.rb"]
|
13
|
+
s.files = ["LICENSE", "Manifest", "README.md", "Rakefile", "lib/hudson_deployer.rb", "hudson_deployer.gemspec"]
|
14
|
+
s.homepage = %q{https://github.com/collinvandyck/hudson_deployer}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Hudson_deployer", "--main", "README.md"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{hudson_deployer}
|
18
|
+
s.rubygems_version = %q{1.5.2}
|
19
|
+
s.summary = %q{Unmagical Capistrano deployment using Hudson}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
s.specification_version = 3
|
23
|
+
|
24
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
25
|
+
else
|
26
|
+
end
|
27
|
+
else
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,289 @@
|
|
1
|
+
require 'capistrano'
|
2
|
+
require 'rest_client'
|
3
|
+
require 'json'
|
4
|
+
require 'erb'
|
5
|
+
|
6
|
+
class Deploy
|
7
|
+
attr_accessor :application,
|
8
|
+
:version,
|
9
|
+
:user,
|
10
|
+
:build_num,
|
11
|
+
:artifact_url
|
12
|
+
|
13
|
+
def initialize(&block)
|
14
|
+
if block_given?
|
15
|
+
yield self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug
|
20
|
+
puts "Deployment Configuration:"
|
21
|
+
[:application, :version, :user, :build_num].each do |f|
|
22
|
+
puts " #{f}: #{send(f)}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
Capistrano::Configuration.instance(:must_exist).load do
|
29
|
+
|
30
|
+
def _cset(name, *args, &block)
|
31
|
+
unless exists?(name)
|
32
|
+
set(name, *args, &block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def hud_api(url)
|
37
|
+
JSON.parse(RestClient.get(url + "/api/json"))
|
38
|
+
end
|
39
|
+
|
40
|
+
# environment configuration
|
41
|
+
######################################################################
|
42
|
+
|
43
|
+
# by default we are in the staging environment
|
44
|
+
@env = :staging
|
45
|
+
|
46
|
+
task :staging do
|
47
|
+
@env = :staging
|
48
|
+
end
|
49
|
+
|
50
|
+
task :production do
|
51
|
+
@env = :production
|
52
|
+
end
|
53
|
+
|
54
|
+
set(:config) do
|
55
|
+
{
|
56
|
+
:default => {},
|
57
|
+
:staging => {},
|
58
|
+
:production => {}
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def config_h
|
63
|
+
config[:default].merge(config[@env])
|
64
|
+
end
|
65
|
+
|
66
|
+
# required variables
|
67
|
+
######################################################################
|
68
|
+
|
69
|
+
_cset(:hudson) { abort ":hudson must be set (e.g. build.yammer.com)" }
|
70
|
+
_cset(:application) { abort ":application must be set" }
|
71
|
+
_cset(:build) { abort ":build must be set (e.g. app-release)" }
|
72
|
+
_cset(:version) { abort ":version must be set (e.g. 2.0.0)" }
|
73
|
+
_cset(:user) { abort ":user must be set" }
|
74
|
+
_cset(:launch_command) { abort ":launch_command must be set" }
|
75
|
+
|
76
|
+
# derived variables
|
77
|
+
######################################################################
|
78
|
+
|
79
|
+
set(:directory) { "/opt/#{application}" }
|
80
|
+
|
81
|
+
set(:current_release) { "#{directory}/releases/#{Time.now.to_i}" }
|
82
|
+
|
83
|
+
set(:job) do
|
84
|
+
record = hud_api(hudson)["jobs"].find { |j| j["name"] == build }
|
85
|
+
abort("No such hudson build could be found") if !record
|
86
|
+
hud_api(record["url"])
|
87
|
+
end
|
88
|
+
|
89
|
+
set(:current_build) do
|
90
|
+
builds = job["builds"]
|
91
|
+
abort("There are no existing builds for this project") if builds.empty?
|
92
|
+
url = builds.first["url"]
|
93
|
+
hud_api(url).tap do |b|
|
94
|
+
if b["result"] != "SUCCESS"
|
95
|
+
abort("The last build was not successful: #{b["result"]}")
|
96
|
+
end
|
97
|
+
if b["building"] == true
|
98
|
+
abort("Project is currently building. You must wait until it finishes.")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
set(:artifact_url) do
|
104
|
+
artifacts = current_build["artifacts"]
|
105
|
+
if artifacts.empty?
|
106
|
+
abort("No artifacts exist for this project!")
|
107
|
+
end
|
108
|
+
artifact = if artifacts.length > 1
|
109
|
+
puts "More than one artifact was found. Please choose:"
|
110
|
+
artifacts.each_with_index do |artifact, index|
|
111
|
+
puts "#{index}: #{artifact["relativePath"]}"
|
112
|
+
end
|
113
|
+
print "? "
|
114
|
+
artifact_index = Capistrano::CLI.ui.ask("choice: ").to_i
|
115
|
+
artifacts[artifact_index]
|
116
|
+
else
|
117
|
+
artifacts.first
|
118
|
+
end
|
119
|
+
current_build["url"] + "artifact/" + artifact["relativePath"]
|
120
|
+
end
|
121
|
+
|
122
|
+
set(:artifact_filename) do
|
123
|
+
artifact_url.split("/").last
|
124
|
+
end
|
125
|
+
|
126
|
+
set(:tmpdir) do
|
127
|
+
"/tmp/deployer-#{Time.now.to_i}".tap do |tmp|
|
128
|
+
FileUtils.mkdir_p(tmp)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
set(:local_entries) do
|
133
|
+
Dir.entries(File.expand_path(File.dirname(__FILE__) + "/" + application)).reject do |name|
|
134
|
+
name =~ /^\./ || name == "Capfile"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
set(:template_names) do
|
139
|
+
Dir.entries(File.expand_path(File.dirname(__FILE__) + "/" + application + "/templates")).reject do |name|
|
140
|
+
name =~ /^\./
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_deployment
|
145
|
+
Deploy.new do |d|
|
146
|
+
d.application = application
|
147
|
+
d.version = version
|
148
|
+
d.user = user
|
149
|
+
d.build_num = current_build["number"]
|
150
|
+
d.artifact_url = artifact_url
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def make_release_directory
|
155
|
+
run "#{sudo} mkdir -p #{current_release}", :roles => "app"
|
156
|
+
run "#{sudo} chown -R #{deployer} #{current_release}", :roles => "app"
|
157
|
+
end
|
158
|
+
|
159
|
+
def create_local_build
|
160
|
+
from = File.expand_path(File.dirname(__FILE__) + "/" + application)
|
161
|
+
local_entries.each { |f| FileUtils.cp_r f, tmpdir, :verbose => true }
|
162
|
+
end
|
163
|
+
|
164
|
+
def verify_plan
|
165
|
+
puts "*" * 80
|
166
|
+
puts "Deployment plan:"
|
167
|
+
@deploy.debug
|
168
|
+
puts "Copying assets:"
|
169
|
+
puts `find #{tmpdir}`
|
170
|
+
puts "*" * 80
|
171
|
+
unless Capistrano::CLI.ui.ask("Is this what you want?") =~ /^y.*/i
|
172
|
+
exit
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def download_artifact
|
177
|
+
puts "Downloading artifact..."
|
178
|
+
`wget #{@deploy.artifact_url} -O #{tmpdir}/#{artifact_filename}`
|
179
|
+
end
|
180
|
+
|
181
|
+
def execute_actions
|
182
|
+
@rendered_actions.each do |action|
|
183
|
+
run "cd #{current_release} && sh #{action}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def transfer_build
|
188
|
+
puts "Transferring build..."
|
189
|
+
Dir.entries(tmpdir).each do |e|
|
190
|
+
unless e =~ /^\./
|
191
|
+
tmpfile = "/tmp/#{Time.now.to_i}"
|
192
|
+
upload "#{tmpdir}/#{e}", tmpfile
|
193
|
+
run "#{sudo} mv #{tmpfile} #{current_release}/#{e}"
|
194
|
+
run "#{sudo} chown -R #{deployer}:#{deployer} #{current_release}/#{e}"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def symlink
|
200
|
+
link = "#{current_release}/../current"
|
201
|
+
run "#{sudo} rm #{link} || true"
|
202
|
+
run "#{sudo} ln -sF #{current_release} #{link}"
|
203
|
+
end
|
204
|
+
|
205
|
+
def cleanup
|
206
|
+
FileUtils.rm_r tmpdir
|
207
|
+
end
|
208
|
+
|
209
|
+
def create_user(username, opts={})
|
210
|
+
create_directory(directory, :owner => deployer, :group => deployer) do
|
211
|
+
run "if ! id #{username} > /dev/null 2>&1; then #{sudo} useradd --no-create-home --home-dir #{opts[:homedir]} --shell #{opts[:shell]} #{username}; fi", :roles => "app"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def create_directory(path, opts={})
|
216
|
+
opts = { :owner => deployer, :group => deployer, :mode => "0755" }.merge(opts)
|
217
|
+
run "#{sudo} mkdir -p #{path}", :roles => "app"
|
218
|
+
yield if block_given?
|
219
|
+
run "#{sudo} chmod #{opts[:mode]} #{path}"
|
220
|
+
run "#{sudo} chown -R #{opts[:owner]}:#{opts[:group]} #{path}", :roles => "app"
|
221
|
+
end
|
222
|
+
|
223
|
+
def touch(path, opts={})
|
224
|
+
opts = { :owner => deployer, :group => deployer, :mode => "0755" }.merge(opts)
|
225
|
+
run "#{sudo} touch #{path}"
|
226
|
+
run "#{sudo} chown #{opts[:owner]}:#{opts[:group]} #{path}", :roles => "app"
|
227
|
+
end
|
228
|
+
|
229
|
+
def render_template(path, opts={})
|
230
|
+
opts = { :owner => deployer, :group => deployer, :mode => "0644" }.merge(opts)
|
231
|
+
data = erb(File.read("templates/#{opts[:template]}"))
|
232
|
+
filename = "/tmp/file-#{Time.now.to_i}"
|
233
|
+
put data, filename
|
234
|
+
run "#{sudo} mv #{filename} #{path}"
|
235
|
+
run "#{sudo} chmod #{opts[:mode]} #{path}"
|
236
|
+
run "#{sudo} chown -R #{opts[:owner]}:#{opts[:group]} #{path}", :roles => "app"
|
237
|
+
end
|
238
|
+
|
239
|
+
def upstart(path, opts={})
|
240
|
+
render_template path, :template => opts[:template], :owner => "root", :group => "root"
|
241
|
+
end
|
242
|
+
|
243
|
+
def erb(text)
|
244
|
+
ERB.new(text).result(binding)
|
245
|
+
end
|
246
|
+
|
247
|
+
def render_scripts
|
248
|
+
template_names.each do |script_name|
|
249
|
+
filename = tmpdir + "/scripts/" + script_name
|
250
|
+
template = ERB.new(File.read(filename))
|
251
|
+
rendered = template.result(binding)
|
252
|
+
File.open(filename, "w") { |f| f.puts(rendered) }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
task :init do
|
257
|
+
puts "Setting user and roles from config"
|
258
|
+
if u = config_h[:user]
|
259
|
+
puts "Setting user to #{u}"
|
260
|
+
set :user, u
|
261
|
+
end
|
262
|
+
if roles = config_h[:roles]
|
263
|
+
roles.each do |h,v|
|
264
|
+
puts "Setting role #{h} => #{v}"
|
265
|
+
role h, v
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
before :deploy, [:init]
|
271
|
+
|
272
|
+
task :deploy do
|
273
|
+
@deploy = build_deployment
|
274
|
+
create_local_build
|
275
|
+
download_artifact
|
276
|
+
verify_plan
|
277
|
+
make_release_directory
|
278
|
+
transfer_build
|
279
|
+
symlink
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
|
286
|
+
|
287
|
+
|
288
|
+
|
289
|
+
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hudson_deployer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Collin VanDyck
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-03-16 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Unmagical Capistrano deployment using Hudson
|
23
|
+
email: collinvandyck @nospam@ gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- LICENSE
|
30
|
+
- README.md
|
31
|
+
- lib/hudson_deployer.rb
|
32
|
+
files:
|
33
|
+
- LICENSE
|
34
|
+
- Manifest
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- lib/hudson_deployer.rb
|
38
|
+
- hudson_deployer.gemspec
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: https://github.com/collinvandyck/hudson_deployer
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --line-numbers
|
46
|
+
- --inline-source
|
47
|
+
- --title
|
48
|
+
- Hudson_deployer
|
49
|
+
- --main
|
50
|
+
- README.md
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
hash: 11
|
68
|
+
segments:
|
69
|
+
- 1
|
70
|
+
- 2
|
71
|
+
version: "1.2"
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project: hudson_deployer
|
75
|
+
rubygems_version: 1.5.2
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Unmagical Capistrano deployment using Hudson
|
79
|
+
test_files: []
|
80
|
+
|