crapapult 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +31 -39
- data/Rakefile +5 -2
- data/VERSION +1 -1
- data/lib/crapapult.rb +123 -98
- metadata +50 -8
data/README.md
CHANGED
@@ -5,25 +5,22 @@ services at [Yammer](http://www.yammer.com).
|
|
5
5
|
|
6
6
|
## What the hell is going on here
|
7
7
|
|
8
|
-
At Yammer, we use
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
Once the artifact is staged (by default in `/opt/APPNAME`), it then uploads the
|
25
|
-
specified set of asset files to each server. After that, it renders and uploads
|
26
|
-
the specified set of template files to each server.
|
8
|
+
At Yammer, we use store all our deployable artifacts in a [Maven](http://maven.apache.org)
|
9
|
+
repository. Each time we commit to a service, Jenkins runs all the tests and (if they pass,
|
10
|
+
naturally) builds a fat, deployable JAR containing all the various dependencies which then gets
|
11
|
+
deployed to our [Nexus](http://nexus.sonatype.org) repository.
|
12
|
+
|
13
|
+
Crapapult allows us to deploy new versions of our services *and* synchronize those deploys with
|
14
|
+
changes to application-specific configuration. (We use Puppet to provision and maintain machines
|
15
|
+
with broad roles but use Crapapult to generate all the application-specific configuration.)
|
16
|
+
|
17
|
+
It does so by using Capistrano to SSH into each of the application servers and using `curl` to
|
18
|
+
download the specified artifact from the Maven repo. This tends to be super-fast, especially if your
|
19
|
+
Maven repo is located in the same data center as your app servers.
|
20
|
+
|
21
|
+
Once the artifact is staged (by default in `/opt/APPNAME`), it then uploads the specified set of
|
22
|
+
asset files to each server. After that, it renders and uploads the specified set of template files
|
23
|
+
to each server.
|
27
24
|
|
28
25
|
Then it restarts your service and gives you a high-five.
|
29
26
|
|
@@ -43,6 +40,11 @@ And start slingin' some crap in your `Capfile`:
|
|
43
40
|
```ruby
|
44
41
|
require "crapapult"
|
45
42
|
|
43
|
+
# Tell us where and what your application is.
|
44
|
+
maven "http://maven.example.com/repo/"
|
45
|
+
group_id "com.example.myapp"
|
46
|
+
artifact_id "myapp-service"
|
47
|
+
|
46
48
|
# Give your application a name.
|
47
49
|
application "myapp" do
|
48
50
|
# Upload the file in assets/myapp.jvm.conf to each host.
|
@@ -56,15 +58,8 @@ application "myapp" do
|
|
56
58
|
upstart "myapp.upstart"
|
57
59
|
end
|
58
60
|
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
# Specify the various branches and their respective build jobs.
|
63
|
-
branch :master, "myapp-release"
|
64
|
-
branch :development, "myapp-development"
|
65
|
-
|
66
|
-
# Define environments with allowed branches.
|
67
|
-
environment :staging, [:master, :development] do
|
61
|
+
# Define environments, optionally allowing snapshots.
|
62
|
+
environment :staging, :allow_snapshots => true do
|
68
63
|
# Set environment-specific data for your templates to work with.
|
69
64
|
data :jdbc_url, "jdbc:postgresql://db.example.com/happy_fun_times"
|
70
65
|
|
@@ -74,8 +69,8 @@ environment :staging, [:master, :development] do
|
|
74
69
|
end
|
75
70
|
end
|
76
71
|
|
77
|
-
# This will disallow deploying
|
78
|
-
environment :production
|
72
|
+
# This will disallow deploying snapshots to production.
|
73
|
+
environment :production do
|
79
74
|
# Set environment-specific data for your templates to work with.
|
80
75
|
data :jdbc_url, "jdbc:postgresql://db.example.com/happy_fun_times"
|
81
76
|
|
@@ -92,18 +87,15 @@ end
|
|
92
87
|
|
93
88
|
## Deploying a thing
|
94
89
|
|
95
|
-
cap
|
96
|
-
cap
|
97
|
-
cap from:master # Deploy the master branch
|
98
|
-
cap to:production # Deploy to the production environment
|
99
|
-
cap to:staging # Deploy to the staging environment
|
90
|
+
cap production # Deploy to the production environment
|
91
|
+
cap staging # Deploy to the staging environment
|
100
92
|
|
101
|
-
So to deploy
|
102
|
-
simple:
|
93
|
+
So to deploy to your staging environment, it's just a simple:
|
103
94
|
|
104
|
-
cap
|
95
|
+
cap staging
|
105
96
|
|
106
|
-
|
97
|
+
You'll be prompted for the version you want to deploy (and, if you're deploying a snapshot, the
|
98
|
+
build number), and you're off to the races.
|
107
99
|
|
108
100
|
--------------------------------------------------------------------------------
|
109
101
|
Copyright (c) 2011 Yammer, Inc. See LICENSE.txt for further details.
|
data/Rakefile
CHANGED
@@ -11,9 +11,12 @@ Jeweler::Tasks.new do |gem|
|
|
11
11
|
gem.email = "coda.hale@gmail.com"
|
12
12
|
gem.authors = ["Coda Hale"]
|
13
13
|
|
14
|
-
gem.add_runtime_dependency "json"
|
15
14
|
gem.add_runtime_dependency "capistrano"
|
16
15
|
gem.add_runtime_dependency "erubis"
|
16
|
+
gem.add_runtime_dependency "nokogiri"
|
17
|
+
gem.add_runtime_dependency "highline"
|
18
|
+
gem.add_runtime_dependency "patron"
|
19
|
+
gem.add_runtime_dependency "yajl-ruby"
|
17
20
|
gem.add_runtime_dependency "always_verify_ssl_certificates"
|
18
21
|
end
|
19
|
-
Jeweler::RubygemsDotOrgTasks.new
|
22
|
+
Jeweler::RubygemsDotOrgTasks.new
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.9
|
data/lib/crapapult.rb
CHANGED
@@ -1,10 +1,64 @@
|
|
1
1
|
require "rubygems"
|
2
|
+
require "patron"
|
3
|
+
require "nokogiri"
|
2
4
|
require "always_verify_ssl_certificates"
|
3
5
|
require "capistrano"
|
4
6
|
require "net/http"
|
5
7
|
require "net/https"
|
6
|
-
require "json"
|
7
8
|
require "erubis"
|
9
|
+
require "yajl/json_gem"
|
10
|
+
|
11
|
+
class Maven
|
12
|
+
def initialize(url)
|
13
|
+
@url = url.gsub(/\/$/, '')
|
14
|
+
@session = Patron::Session.new
|
15
|
+
@session.timeout = 10
|
16
|
+
@session.base_url = @url
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_versions(group_id, artifact_id)
|
20
|
+
resp = @session.get("/#{urlify(group_id)}/#{artifact_id}/maven-metadata.xml")
|
21
|
+
if resp.status == 200
|
22
|
+
Nokogiri::XML(resp.body).css("metadata versioning versions version").map { |e| e.text.strip }
|
23
|
+
else
|
24
|
+
raise "unable to find metadata for #{group_id}:#{artifact_id}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_release_versions(group_id, artifact_id)
|
29
|
+
find_versions(group_id, artifact_id).select { |v| v !~ /SNAPSHOT$/ }
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_snapshot_builds(group_id, artifact_id, version)
|
33
|
+
resp = @session.get("/#{urlify(group_id)}/#{artifact_id}/#{version}/maven-metadata.xml")
|
34
|
+
if resp.status == 200
|
35
|
+
Nokogiri::XML(resp.body).css('metadata > versioning > snapshotVersions').map { |e|
|
36
|
+
ext = e.at("extension")
|
37
|
+
if ext && ext.text.strip == "jar"
|
38
|
+
e.at("value").text.strip
|
39
|
+
else
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
}.flatten
|
43
|
+
else
|
44
|
+
raise "unable to find metadata for #{group_id}:#{artifact_id}:#{version}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def release_artifact_url(group_id, artifact_id, version)
|
49
|
+
"#{@url}/#{urlify(group_id)}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar"
|
50
|
+
end
|
51
|
+
|
52
|
+
def snapshot_artifact_url(group_id, artifact_id, version, build)
|
53
|
+
"#{@url}/#{urlify(group_id)}/#{artifact_id}/#{version}/#{artifact_id}-#{build}.jar"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def urlify(group_id)
|
59
|
+
group_id.gsub('.', '/')
|
60
|
+
end
|
61
|
+
end
|
8
62
|
|
9
63
|
Capistrano::Configuration.instance(:must_exist).load do
|
10
64
|
|
@@ -16,8 +70,7 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
16
70
|
default_run_options[:pty] = true
|
17
71
|
@invoke = tasks.delete(:invoke)
|
18
72
|
@shell = tasks.delete(:shell)
|
19
|
-
|
20
|
-
@branches = []
|
73
|
+
|
21
74
|
@hosts = {}
|
22
75
|
@data = {}
|
23
76
|
|
@@ -25,51 +78,43 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
25
78
|
###################################### API ###################################
|
26
79
|
##############################################################################
|
27
80
|
|
81
|
+
# Defines the artifact's groupId.
|
82
|
+
def group_id(name)
|
83
|
+
set :current_group_id, name
|
84
|
+
end
|
85
|
+
|
86
|
+
# Defines the artifact's artifactId.
|
87
|
+
def artifact_id(name)
|
88
|
+
set :current_artifact_id, name
|
89
|
+
end
|
90
|
+
|
28
91
|
# Defines a host with an optional host-specific configuration block.
|
29
92
|
def host(name, &block)
|
30
93
|
@hosts[name] = block || lambda {}
|
31
94
|
server name, :app
|
32
95
|
end
|
33
96
|
|
34
|
-
# Defines an environment
|
97
|
+
# Defines an environment which might allow snapshots and an optional
|
35
98
|
# environment-specific configuration block.
|
36
|
-
def environment(name,
|
99
|
+
def environment(name, opts={}, &block)
|
37
100
|
env_block = block || lambda {}
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
namespace :to do
|
46
|
-
desc "Deploy to the #{name} environment"
|
47
|
-
task name.to_sym do
|
48
|
-
set :current_environment, name
|
49
|
-
set :allowed_branches, branches
|
50
|
-
env_block.call
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# Defines a branch with a Hudson job name.
|
56
|
-
def branch(name, job)
|
57
|
-
@branches << name.to_sym
|
58
|
-
namespace :from do
|
59
|
-
desc "Deploy the #{name} branch"
|
60
|
-
task name.to_sym do
|
61
|
-
set :current_branch, name
|
62
|
-
set :current_job, job
|
63
|
-
end
|
101
|
+
|
102
|
+
desc "Deploy to the #{name} environment"
|
103
|
+
task name.to_sym do
|
104
|
+
set :current_environment, name
|
105
|
+
set :allow_snapshots, opts[:allow_snapshots]
|
106
|
+
env_block.call
|
107
|
+
execute_task(find_task("deploy"))
|
64
108
|
end
|
65
109
|
end
|
66
|
-
|
67
|
-
# Defines a
|
68
|
-
def
|
69
|
-
if exists?(:
|
70
|
-
abort "You're calling #
|
110
|
+
|
111
|
+
# Defines a Maven repository.
|
112
|
+
def maven(url)
|
113
|
+
if exists?(:current_repo)
|
114
|
+
abort "You're calling #maven twice. Stop it."
|
115
|
+
else
|
116
|
+
set :current_repo, url
|
71
117
|
end
|
72
|
-
set :current_hudson_url, url
|
73
118
|
end
|
74
119
|
|
75
120
|
# Defines an application with an optional owner, group, and configuration
|
@@ -99,7 +144,7 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
99
144
|
puts(">> #{server.host} is OK")
|
100
145
|
else
|
101
146
|
puts(">> #{server.host} is DOWN!!!")
|
102
|
-
puts(resp.body.split("\n").map { |s| ">>> #{s}" }.join)
|
147
|
+
puts(resp.body.split("\n").map { |s| ">>> #{s}" }.join("\n"))
|
103
148
|
end
|
104
149
|
end
|
105
150
|
end
|
@@ -153,12 +198,12 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
153
198
|
end
|
154
199
|
|
155
200
|
# Post a message to your Yammer network when you deploy.
|
156
|
-
def yammer(token, opts={})
|
157
|
-
opts = { :host => "www.yammer.com",
|
158
|
-
:body => %Q{The <%= current_branch %> branch of <%= application_name %> (<%= last_ref[0..5] %>) was just deployed to the <%= current_environment %> environment by @<%= `whoami`.strip %>.}
|
159
|
-
}.merge(opts)
|
160
|
-
|
201
|
+
def yammer(token, opts={})
|
161
202
|
after :deploy do
|
203
|
+
set :deployed_version, exists?(:build) ? "#{version} (#{build})" : version
|
204
|
+
opts = {:host => "www.yammer.com",
|
205
|
+
:body => %Q{<%= application_name %> v<%= deployed_version %> was just deployed to the <%= current_environment %> environment by @<%= `whoami`.strip %>.}
|
206
|
+
}.merge(opts)
|
162
207
|
puts "> Sending notification to Yammer"
|
163
208
|
opts[:body] = Erubis::Eruby.new(opts[:body]).result(binding)
|
164
209
|
http = Net::HTTP.new(opts.delete(:host), 443)
|
@@ -180,11 +225,12 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
180
225
|
##############################################################################
|
181
226
|
|
182
227
|
namespace :deploy do
|
183
|
-
desc "Deploy the specified branch to the specified environment"
|
184
228
|
task :default do
|
185
229
|
check_required_parameters!
|
186
230
|
|
187
|
-
puts "> Deploying
|
231
|
+
puts "> Deploying #{application_name} to #{current_environment}"
|
232
|
+
|
233
|
+
artifact_url # queue this up before hand
|
188
234
|
|
189
235
|
setup
|
190
236
|
stage
|
@@ -194,7 +240,7 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
194
240
|
verify
|
195
241
|
cleanup
|
196
242
|
|
197
|
-
puts "> Successfully
|
243
|
+
puts "> Successfully #{application_name} to #{current_environment}! Congrats!"
|
198
244
|
end
|
199
245
|
|
200
246
|
task :setup, :role => :app do
|
@@ -257,77 +303,56 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
257
303
|
"/tmp/#{application_name}-#{rand(10_000_000)}"
|
258
304
|
end
|
259
305
|
|
260
|
-
set(:last_build) do
|
261
|
-
build = hudson_api(current_hudson_url + "/job/#{current_job}/lastBuild")
|
262
|
-
unless build["result"] == "SUCCESS"
|
263
|
-
abort("The last build of #{current_job} failed; see #{build["url"]} for details")
|
264
|
-
end
|
265
|
-
|
266
|
-
build
|
267
|
-
end
|
268
|
-
|
269
|
-
set(:last_ref) do
|
270
|
-
last_build["actions"].find { |f| f["lastBuiltRevision"] }["lastBuiltRevision"]["SHA1"]
|
271
|
-
end
|
272
|
-
|
273
306
|
set(:artifact_url) do
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
307
|
+
hl = HighLine.new
|
308
|
+
m = Maven.new(current_repo)
|
309
|
+
|
310
|
+
hl.say("Select a version to deploy:")
|
311
|
+
versions = if allow_snapshots
|
312
|
+
m.find_versions(current_group_id, current_artifact_id).reverse[0..2]
|
313
|
+
else
|
314
|
+
m.find_release_versions(current_group_id, current_artifact_id).reverse[0..2]
|
315
|
+
end
|
316
|
+
|
317
|
+
set :version, hl.choose(*versions)
|
318
|
+
puts
|
319
|
+
|
320
|
+
if version =~ /SNAPSHOT$/
|
321
|
+
builds = m.find_snapshot_builds(current_group_id, current_artifact_id, version)
|
322
|
+
hl.say("Select a snapshot build to deploy:")
|
323
|
+
set :build, hl.choose(*builds)
|
324
|
+
m.snapshot_artifact_url(current_group_id, current_artifact_id, version, build)
|
286
325
|
else
|
287
|
-
|
326
|
+
m.release_artifact_url(current_group_id, current_artifact_id, version)
|
288
327
|
end
|
289
|
-
last_build["url"] + "artifact/" + artifact["relativePath"]
|
290
328
|
end
|
291
329
|
|
292
330
|
set(:artifact_filename) do
|
293
331
|
filename = artifact_url.split("/").last
|
294
|
-
"#{application_name}-#{Time.now.strftime("%Y%m%d%H%M%S")}-#{
|
332
|
+
"#{application_name}-#{Time.now.strftime("%Y%m%d%H%M%S")}-#{version}#{File.extname(filename)}"
|
295
333
|
end
|
296
334
|
|
297
335
|
set(:directory) { "/opt/#{application_name}" }
|
298
336
|
|
299
|
-
def hudson_api(url)
|
300
|
-
if url !~ /\/$/
|
301
|
-
url = url + "/api/json"
|
302
|
-
else
|
303
|
-
url = url + "api/json"
|
304
|
-
end
|
305
|
-
|
306
|
-
logger.debug("getting #{url}...")
|
307
|
-
json = Net::HTTP.get(URI.parse(url))
|
308
|
-
# logger.debug("Received: #{json}")
|
309
|
-
JSON.parse(json)
|
310
|
-
end
|
311
|
-
|
312
337
|
def check_required_parameters!
|
313
338
|
unless exists?(:application_name)
|
314
339
|
abort("No application defined.")
|
315
340
|
end
|
316
341
|
|
317
|
-
unless exists?(:
|
318
|
-
abort("No
|
342
|
+
unless exists?(:current_repo)
|
343
|
+
abort("No Maven repository defined.")
|
319
344
|
end
|
320
|
-
|
321
|
-
unless exists?(:
|
322
|
-
abort("No
|
345
|
+
|
346
|
+
unless exists?(:current_group_id)
|
347
|
+
abort("No group ID defined.")
|
323
348
|
end
|
324
|
-
|
325
|
-
unless exists?(:
|
326
|
-
abort("No
|
349
|
+
|
350
|
+
unless exists?(:current_artifact_id)
|
351
|
+
abort("No artifact ID defined.")
|
327
352
|
end
|
328
353
|
|
329
|
-
unless
|
330
|
-
abort
|
354
|
+
unless exists?(:current_environment)
|
355
|
+
abort("No target environment specified. Please use one of the to:* tasks.")
|
331
356
|
end
|
332
357
|
end
|
333
358
|
|
@@ -336,4 +361,4 @@ Capistrano::Configuration.instance(:must_exist).load do
|
|
336
361
|
tasks[:invoke] = @invoke
|
337
362
|
tasks[:shell] = @shell
|
338
363
|
end
|
339
|
-
end
|
364
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: crapapult
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 9
|
10
|
+
version: 0.0.9
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Coda Hale
|
@@ -15,11 +15,11 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-12-21 00:00:00 -08:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
|
-
name:
|
22
|
+
name: capistrano
|
23
23
|
prerelease: false
|
24
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
25
|
none: false
|
@@ -33,7 +33,7 @@ dependencies:
|
|
33
33
|
type: :runtime
|
34
34
|
version_requirements: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
|
-
name:
|
36
|
+
name: erubis
|
37
37
|
prerelease: false
|
38
38
|
requirement: &id002 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
@@ -47,7 +47,7 @@ dependencies:
|
|
47
47
|
type: :runtime
|
48
48
|
version_requirements: *id002
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
|
-
name:
|
50
|
+
name: nokogiri
|
51
51
|
prerelease: false
|
52
52
|
requirement: &id003 !ruby/object:Gem::Requirement
|
53
53
|
none: false
|
@@ -61,7 +61,7 @@ dependencies:
|
|
61
61
|
type: :runtime
|
62
62
|
version_requirements: *id003
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
|
-
name:
|
64
|
+
name: highline
|
65
65
|
prerelease: false
|
66
66
|
requirement: &id004 !ruby/object:Gem::Requirement
|
67
67
|
none: false
|
@@ -74,6 +74,48 @@ dependencies:
|
|
74
74
|
version: "0"
|
75
75
|
type: :runtime
|
76
76
|
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: patron
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
type: :runtime
|
90
|
+
version_requirements: *id005
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: yajl-ruby
|
93
|
+
prerelease: false
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
type: :runtime
|
104
|
+
version_requirements: *id006
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
name: always_verify_ssl_certificates
|
107
|
+
prerelease: false
|
108
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
type: :runtime
|
118
|
+
version_requirements: *id007
|
77
119
|
description: Yammer's Capistrano-based deploy crap.
|
78
120
|
email: coda.hale@gmail.com
|
79
121
|
executables: []
|