crapapult 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README.md +31 -39
  2. data/Rakefile +5 -2
  3. data/VERSION +1 -1
  4. data/lib/crapapult.rb +123 -98
  5. 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
- [Hudson](http://hudson-ci.org/)/[Jenkins](http://jenkins-ci.org/) to build all
10
- of our JVM-based services. Each time we commit to the various branches of a
11
- service, Hudson runs all the tests and (if they pass, naturally) builds a fat,
12
- deployable JAR containing all the various dependencies.
13
-
14
- Crapapult allows us to deploy new versions of our services *and* synchronize
15
- those deploys with changes to application-specific configuration. (We use
16
- Puppet to provision and maintain machines with broad roles but use Crapapult to
17
- generate all the application-specific configuration.)
18
-
19
- It does so by using Capistrano to SSH into each of the application servers and
20
- using `curl` to download the last successful build artifact from the specified
21
- Hudson job. This tends to be super-fast, especially if your CI box is located in
22
- the same data center as your app servers.
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
- # Specify where your Hudson/Jenkins server is.
60
- hudson "http://build.example.com"
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 the staging branch to production.
78
- environment :production, [:master] do
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 deploy # Deploy the specified branch to the specified environment
96
- cap from:development # Deploy the development branch
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 your development branch to your staging environment, it's just a
102
- simple:
93
+ So to deploy to your staging environment, it's just a simple:
103
94
 
104
- cap from:development to:staging deploy
95
+ cap staging
105
96
 
106
- And you're off to the races.
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.8
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 with a set of allowed branches and an optional
97
+ # Defines an environment which might allow snapshots and an optional
35
98
  # environment-specific configuration block.
36
- def environment(name, branches, &block)
99
+ def environment(name, opts={}, &block)
37
100
  env_block = block || lambda {}
38
-
39
- for b in branches
40
- unless @branches.include?(b)
41
- abort "Environment #{name} allows a branch which hasn't been declared: #{b}"
42
- end
43
- end
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 Hudson server.
68
- def hudson(url)
69
- if exists?(:current_hudson_url)
70
- abort "You're calling #hudson twice. Stop it."
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 the #{current_branch} branch to #{current_environment}"
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 deployed the #{current_branch} branch to #{current_environment}! Congrats!"
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
- artifacts = last_build["artifacts"]
275
- if artifacts.empty?
276
- abort("No artifacts exist for this project!")
277
- end
278
- artifact = if artifacts.length > 1
279
- puts "More than one artifact was found. Please choose:"
280
- artifacts.each_with_index do |artifact, index|
281
- puts "#{index}: #{artifact["relativePath"]}"
282
- end
283
- print "? "
284
- artifact_index = Capistrano::CLI.ui.ask("choice: ").to_i
285
- artifacts[artifact_index]
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
- artifacts.first
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")}-#{last_ref[0..5]}#{File.extname(filename)}"
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?(:current_hudson_url)
318
- abort("No Hudson instance defined.")
342
+ unless exists?(:current_repo)
343
+ abort("No Maven repository defined.")
319
344
  end
320
-
321
- unless exists?(:current_environment)
322
- abort("No target environment specified. Please use one of the to:* tasks.")
345
+
346
+ unless exists?(:current_group_id)
347
+ abort("No group ID defined.")
323
348
  end
324
-
325
- unless exists?(:current_branch)
326
- abort("No source branch specified. Please use one of the from:* tasks.")
349
+
350
+ unless exists?(:current_artifact_id)
351
+ abort("No artifact ID defined.")
327
352
  end
328
353
 
329
- unless allowed_branches.include?(current_branch)
330
- abort "The #{current_environment} environment only allows deploys from the following branches: #{allowed_branches.join(", ")}"
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: 15
4
+ hash: 13
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 8
10
- version: 0.0.8
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-05-13 00:00:00 -07:00
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: json
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: capistrano
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: erubis
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: always_verify_ssl_certificates
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: []