crapapult 0.0.8 → 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.
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: []