jenkins-maestrodev 0.6.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.
- checksums.yaml +7 -0
- data/Changelog.md +58 -0
- data/Gemfile +3 -0
- data/README.md +162 -0
- data/Rakefile +110 -0
- data/bin/jenkins +9 -0
- data/cucumber.yml +8 -0
- data/features/build_details.feature +44 -0
- data/features/configure.feature +36 -0
- data/features/default_host.feature +26 -0
- data/features/development.feature +14 -0
- data/features/listing_jobs.feature +34 -0
- data/features/manage_jobs.feature +263 -0
- data/features/manage_slave_nodes.feature +83 -0
- data/features/step_definitions/common_steps.rb +192 -0
- data/features/step_definitions/fixture_project_steps.rb +8 -0
- data/features/step_definitions/jenkins_steps.rb +116 -0
- data/features/step_definitions/scm_steps.rb +12 -0
- data/features/support/common.rb +37 -0
- data/features/support/env.rb +19 -0
- data/features/support/hooks.rb +16 -0
- data/features/support/jenkins_helpers.rb +6 -0
- data/features/support/matchers.rb +10 -0
- data/fixtures/jenkins/envfile.hpi +0 -0
- data/fixtures/jenkins/git.hpi +0 -0
- data/fixtures/jenkins/github.hpi +0 -0
- data/fixtures/jenkins/greenballs.hpi +0 -0
- data/fixtures/jenkins/rake.hpi +0 -0
- data/fixtures/jenkins/ruby.hpi +0 -0
- data/fixtures/projects/erlang/rebar.config +1 -0
- data/fixtures/projects/non-bundler/Rakefile +4 -0
- data/fixtures/projects/rails-3/.gitignore +4 -0
- data/fixtures/projects/rails-3/Gemfile +30 -0
- data/fixtures/projects/rails-3/Gemfile.lock +74 -0
- data/fixtures/projects/rails-3/README +256 -0
- data/fixtures/projects/rails-3/Rakefile +7 -0
- data/fixtures/projects/rails-3/app/controllers/application_controller.rb +3 -0
- data/fixtures/projects/rails-3/app/helpers/application_helper.rb +2 -0
- data/fixtures/projects/rails-3/app/views/layouts/application.html.erb +14 -0
- data/fixtures/projects/rails-3/config.ru +4 -0
- data/fixtures/projects/rails-3/config/application.rb +42 -0
- data/fixtures/projects/rails-3/config/boot.rb +13 -0
- data/fixtures/projects/rails-3/config/database.yml +22 -0
- data/fixtures/projects/rails-3/config/environment.rb +5 -0
- data/fixtures/projects/rails-3/config/environments/development.rb +26 -0
- data/fixtures/projects/rails-3/config/environments/production.rb +49 -0
- data/fixtures/projects/rails-3/config/environments/test.rb +35 -0
- data/fixtures/projects/rails-3/config/initializers/backtrace_silencers.rb +7 -0
- data/fixtures/projects/rails-3/config/initializers/inflections.rb +10 -0
- data/fixtures/projects/rails-3/config/initializers/mime_types.rb +5 -0
- data/fixtures/projects/rails-3/config/initializers/secret_token.rb +7 -0
- data/fixtures/projects/rails-3/config/initializers/session_store.rb +8 -0
- data/fixtures/projects/rails-3/config/locales/en.yml +5 -0
- data/fixtures/projects/rails-3/config/routes.rb +58 -0
- data/fixtures/projects/rails-3/db/seeds.rb +7 -0
- data/fixtures/projects/rails-3/doc/README_FOR_APP +2 -0
- data/fixtures/projects/rails-3/lib/tasks/.gitkeep +0 -0
- data/fixtures/projects/rails-3/public/404.html +26 -0
- data/fixtures/projects/rails-3/public/422.html +26 -0
- data/fixtures/projects/rails-3/public/500.html +26 -0
- data/fixtures/projects/rails-3/public/favicon.ico +0 -0
- data/fixtures/projects/rails-3/public/images/rails.png +0 -0
- data/fixtures/projects/rails-3/public/index.html +239 -0
- data/fixtures/projects/rails-3/public/javascripts/application.js +2 -0
- data/fixtures/projects/rails-3/public/javascripts/controls.js +965 -0
- data/fixtures/projects/rails-3/public/javascripts/dragdrop.js +974 -0
- data/fixtures/projects/rails-3/public/javascripts/effects.js +1123 -0
- data/fixtures/projects/rails-3/public/javascripts/prototype.js +6001 -0
- data/fixtures/projects/rails-3/public/javascripts/rails.js +175 -0
- data/fixtures/projects/rails-3/public/robots.txt +5 -0
- data/fixtures/projects/rails-3/public/stylesheets/.gitkeep +0 -0
- data/fixtures/projects/rails-3/script/rails +6 -0
- data/fixtures/projects/rails-3/test/performance/browsing_test.rb +9 -0
- data/fixtures/projects/rails-3/test/test_helper.rb +13 -0
- data/fixtures/projects/rails-3/vendor/plugins/.gitkeep +0 -0
- data/fixtures/projects/ruby/Gemfile +3 -0
- data/fixtures/projects/ruby/Gemfile.lock +10 -0
- data/fixtures/projects/ruby/Rakefile +4 -0
- data/jenkins.gemspec +34 -0
- data/lib/jenkins.rb +6 -0
- data/lib/jenkins/api.rb +312 -0
- data/lib/jenkins/cli.rb +361 -0
- data/lib/jenkins/cli/formatting.rb +53 -0
- data/lib/jenkins/config.rb +27 -0
- data/lib/jenkins/core_ext/hash.rb +9 -0
- data/lib/jenkins/core_ext/object/blank.rb +77 -0
- data/lib/jenkins/hudson-cli.jar +0 -0
- data/lib/jenkins/job_config_builder.rb +417 -0
- data/lib/jenkins/project_scm.rb +22 -0
- data/lib/jenkins/remote.rb +11 -0
- data/lib/jenkins/version.rb +3 -0
- data/spec/api_spec.rb +67 -0
- data/spec/fixtures/ec2_global.config.xml +103 -0
- data/spec/fixtures/erlang.single.config.xml +59 -0
- data/spec/fixtures/rails.multi.config.xml +82 -0
- data/spec/fixtures/rails.single.config.triggers.xml +84 -0
- data/spec/fixtures/rails.single.config.xml +80 -0
- data/spec/fixtures/ruby.multi-ruby-multi-labels.config.xml +84 -0
- data/spec/fixtures/ruby.multi.config.xml +77 -0
- data/spec/fixtures/ruby.single.config.xml +58 -0
- data/spec/fixtures/ruby.user-defined-axis.config.xml +69 -0
- data/spec/fixtures/therubyracer.config.xml +77 -0
- data/spec/hash_key_cleaner_spec.rb +25 -0
- data/spec/job_config_builder_spec.rb +233 -0
- data/spec/spec_helper.rb +15 -0
- metadata +291 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
// Technique from Juriy Zaytsev
|
|
3
|
+
// http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
|
|
4
|
+
function isEventSupported(eventName) {
|
|
5
|
+
var el = document.createElement('div');
|
|
6
|
+
eventName = 'on' + eventName;
|
|
7
|
+
var isSupported = (eventName in el);
|
|
8
|
+
if (!isSupported) {
|
|
9
|
+
el.setAttribute(eventName, 'return;');
|
|
10
|
+
isSupported = typeof el[eventName] == 'function';
|
|
11
|
+
}
|
|
12
|
+
el = null;
|
|
13
|
+
return isSupported;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isForm(element) {
|
|
17
|
+
return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isInput(element) {
|
|
21
|
+
if (Object.isElement(element)) {
|
|
22
|
+
var name = element.nodeName.toUpperCase()
|
|
23
|
+
return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
|
|
24
|
+
}
|
|
25
|
+
else return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var submitBubbles = isEventSupported('submit'),
|
|
29
|
+
changeBubbles = isEventSupported('change')
|
|
30
|
+
|
|
31
|
+
if (!submitBubbles || !changeBubbles) {
|
|
32
|
+
// augment the Event.Handler class to observe custom events when needed
|
|
33
|
+
Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
|
|
34
|
+
function(init, element, eventName, selector, callback) {
|
|
35
|
+
init(element, eventName, selector, callback)
|
|
36
|
+
// is the handler being attached to an element that doesn't support this event?
|
|
37
|
+
if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
|
|
38
|
+
(!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
|
|
39
|
+
// "submit" => "emulated:submit"
|
|
40
|
+
this.eventName = 'emulated:' + this.eventName
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!submitBubbles) {
|
|
47
|
+
// discover forms on the page by observing focus events which always bubble
|
|
48
|
+
document.on('focusin', 'form', function(focusEvent, form) {
|
|
49
|
+
// special handler for the real "submit" event (one-time operation)
|
|
50
|
+
if (!form.retrieve('emulated:submit')) {
|
|
51
|
+
form.on('submit', function(submitEvent) {
|
|
52
|
+
var emulated = form.fire('emulated:submit', submitEvent, true)
|
|
53
|
+
// if custom event received preventDefault, cancel the real one too
|
|
54
|
+
if (emulated.returnValue === false) submitEvent.preventDefault()
|
|
55
|
+
})
|
|
56
|
+
form.store('emulated:submit', true)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!changeBubbles) {
|
|
62
|
+
// discover form inputs on the page
|
|
63
|
+
document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
|
|
64
|
+
// special handler for real "change" events
|
|
65
|
+
if (!input.retrieve('emulated:change')) {
|
|
66
|
+
input.on('change', function(changeEvent) {
|
|
67
|
+
input.fire('emulated:change', changeEvent, true)
|
|
68
|
+
})
|
|
69
|
+
input.store('emulated:change', true)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleRemote(element) {
|
|
75
|
+
var method, url, params;
|
|
76
|
+
|
|
77
|
+
var event = element.fire("ajax:before");
|
|
78
|
+
if (event.stopped) return false;
|
|
79
|
+
|
|
80
|
+
if (element.tagName.toLowerCase() === 'form') {
|
|
81
|
+
method = element.readAttribute('method') || 'post';
|
|
82
|
+
url = element.readAttribute('action');
|
|
83
|
+
params = element.serialize();
|
|
84
|
+
} else {
|
|
85
|
+
method = element.readAttribute('data-method') || 'get';
|
|
86
|
+
url = element.readAttribute('href');
|
|
87
|
+
params = {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
new Ajax.Request(url, {
|
|
91
|
+
method: method,
|
|
92
|
+
parameters: params,
|
|
93
|
+
evalScripts: true,
|
|
94
|
+
|
|
95
|
+
onComplete: function(request) { element.fire("ajax:complete", request); },
|
|
96
|
+
onSuccess: function(request) { element.fire("ajax:success", request); },
|
|
97
|
+
onFailure: function(request) { element.fire("ajax:failure", request); }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
element.fire("ajax:after");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleMethod(element) {
|
|
104
|
+
var method = element.readAttribute('data-method'),
|
|
105
|
+
url = element.readAttribute('href'),
|
|
106
|
+
csrf_param = $$('meta[name=csrf-param]')[0],
|
|
107
|
+
csrf_token = $$('meta[name=csrf-token]')[0];
|
|
108
|
+
|
|
109
|
+
var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
|
|
110
|
+
element.parentNode.insert(form);
|
|
111
|
+
|
|
112
|
+
if (method !== 'post') {
|
|
113
|
+
var field = new Element('input', { type: 'hidden', name: '_method', value: method });
|
|
114
|
+
form.insert(field);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (csrf_param) {
|
|
118
|
+
var param = csrf_param.readAttribute('content'),
|
|
119
|
+
token = csrf_token.readAttribute('content'),
|
|
120
|
+
field = new Element('input', { type: 'hidden', name: param, value: token });
|
|
121
|
+
form.insert(field);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
form.submit();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
document.on("click", "*[data-confirm]", function(event, element) {
|
|
129
|
+
var message = element.readAttribute('data-confirm');
|
|
130
|
+
if (!confirm(message)) event.stop();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
document.on("click", "a[data-remote]", function(event, element) {
|
|
134
|
+
if (event.stopped) return;
|
|
135
|
+
handleRemote(element);
|
|
136
|
+
event.stop();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
document.on("click", "a[data-method]", function(event, element) {
|
|
140
|
+
if (event.stopped) return;
|
|
141
|
+
handleMethod(element);
|
|
142
|
+
event.stop();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
document.on("submit", function(event) {
|
|
146
|
+
var element = event.findElement(),
|
|
147
|
+
message = element.readAttribute('data-confirm');
|
|
148
|
+
if (message && !confirm(message)) {
|
|
149
|
+
event.stop();
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var inputs = element.select("input[type=submit][data-disable-with]");
|
|
154
|
+
inputs.each(function(input) {
|
|
155
|
+
input.disabled = true;
|
|
156
|
+
input.writeAttribute('data-original-value', input.value);
|
|
157
|
+
input.value = input.readAttribute('data-disable-with');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
var element = event.findElement("form[data-remote]");
|
|
161
|
+
if (element) {
|
|
162
|
+
handleRemote(element);
|
|
163
|
+
event.stop();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
document.on("ajax:after", "form", function(event, element) {
|
|
168
|
+
var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
|
|
169
|
+
inputs.each(function(input) {
|
|
170
|
+
input.value = input.readAttribute('data-original-value');
|
|
171
|
+
input.removeAttribute('data-original-value');
|
|
172
|
+
input.disabled = false;
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
})();
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
|
3
|
+
|
|
4
|
+
APP_PATH = File.expand_path('../../config/application', __FILE__)
|
|
5
|
+
require File.expand_path('../../config/boot', __FILE__)
|
|
6
|
+
require 'rails/commands'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
|
2
|
+
require File.expand_path('../../config/environment', __FILE__)
|
|
3
|
+
require 'rails/test_help'
|
|
4
|
+
|
|
5
|
+
class ActiveSupport::TestCase
|
|
6
|
+
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
|
|
7
|
+
#
|
|
8
|
+
# Note: You'll currently still have to declare fixtures explicitly in integration tests
|
|
9
|
+
# -- they do not yet inherit this setting
|
|
10
|
+
fixtures :all
|
|
11
|
+
|
|
12
|
+
# Add more helper methods to be used by all tests here...
|
|
13
|
+
end
|
|
File without changes
|
data/jenkins.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "jenkins/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "jenkins-maestrodev"
|
|
7
|
+
s.version = Jenkins::VERSION
|
|
8
|
+
s.platform = Gem::Platform::RUBY
|
|
9
|
+
s.authors = ["Charles Lowell", "Nic Williams"]
|
|
10
|
+
s.email = ["cowboyd@thefrontside.net", "drnicwilliams@gmail.com"]
|
|
11
|
+
s.homepage = "https://github.com/jenkinsci/jenkins.rb/tree/master/ruby-tools/cli"
|
|
12
|
+
s.summary = %q{Painless Continuous Integration with Jenkins Server}
|
|
13
|
+
s.description = %q{A suite of utilities for bringing continous integration to your projects (not the other way around) with jenkins CI}
|
|
14
|
+
|
|
15
|
+
s.rubyforge_project = "jenkins"
|
|
16
|
+
|
|
17
|
+
s.files = `git ls-files`.split("\n")
|
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
20
|
+
s.require_paths = ["lib"]
|
|
21
|
+
|
|
22
|
+
s.add_dependency("term-ansicolor", ">= 1.0.4")
|
|
23
|
+
s.add_dependency("httparty", "~> 0.6.1")
|
|
24
|
+
s.add_dependency("builder", "~> 2.1.2")
|
|
25
|
+
s.add_dependency("thor", "~> 0.15.0")
|
|
26
|
+
s.add_dependency("hpricot")
|
|
27
|
+
s.add_dependency("json_pure", ">= 1.5.1")
|
|
28
|
+
|
|
29
|
+
s.add_development_dependency "jenkins-war", ">= 1.396"
|
|
30
|
+
s.add_development_dependency "rake"
|
|
31
|
+
s.add_development_dependency "cucumber", "~> 1.0"
|
|
32
|
+
s.add_development_dependency "rspec", "~> 2.0"
|
|
33
|
+
s.add_development_dependency "awesome_print"
|
|
34
|
+
end
|
data/lib/jenkins.rb
ADDED
data/lib/jenkins/api.rb
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
require 'httparty'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
require 'jenkins/core_ext/hash'
|
|
8
|
+
require 'jenkins/config'
|
|
9
|
+
|
|
10
|
+
YAML::ENGINE.yamler = "syck" if RUBY_VERSION >= '1.9'
|
|
11
|
+
|
|
12
|
+
module Jenkins
|
|
13
|
+
module Api
|
|
14
|
+
include HTTParty
|
|
15
|
+
|
|
16
|
+
headers 'content-type' => 'application/json'
|
|
17
|
+
format :json
|
|
18
|
+
# http_proxy 'localhost', '8888'
|
|
19
|
+
|
|
20
|
+
JobAlreadyExistsError = Class.new(Exception)
|
|
21
|
+
|
|
22
|
+
def self.setup_base_url(options = {})
|
|
23
|
+
# Handle single strings
|
|
24
|
+
options = { :host => options } if options.is_a? String
|
|
25
|
+
options = options.with_clean_keys
|
|
26
|
+
# Thor's HashWithIndifferentAccess is based on string keys which URI::HTTP.build ignores
|
|
27
|
+
options = options.inject({}) { |mem, (key, val)| mem[key.to_sym] = val; mem }
|
|
28
|
+
|
|
29
|
+
options = setup_authentication(options)
|
|
30
|
+
|
|
31
|
+
# Handle URL style hosts by parsing the URL
|
|
32
|
+
if options.keys.length == 1 && options.key?(:host)
|
|
33
|
+
parsed_uri = URI::parse(options[:host])
|
|
34
|
+
options = {
|
|
35
|
+
:host => parsed_uri.host,
|
|
36
|
+
:port => parsed_uri.port,
|
|
37
|
+
:path => parsed_uri.path,
|
|
38
|
+
:ssl => parsed_uri.scheme == 'https'
|
|
39
|
+
}
|
|
40
|
+
if parsed_uri.user && parsed_uri.password
|
|
41
|
+
basic_auth parsed_uri.user, parsed_uri.password
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
options[:host] ||= ENV['JENKINS_HOST']
|
|
46
|
+
options[:port] ||= ENV['JENKINS_PORT']
|
|
47
|
+
options[:port] &&= options[:port].to_i
|
|
48
|
+
|
|
49
|
+
if options[:host]
|
|
50
|
+
uri_class = options.delete(:ssl) ? URI::HTTPS : URI::HTTP
|
|
51
|
+
uri = uri_class.build(options)
|
|
52
|
+
else
|
|
53
|
+
if Jenkins::Config.config["base_uri"]
|
|
54
|
+
uri = Jenkins::Config.config["base_uri"]
|
|
55
|
+
else
|
|
56
|
+
return false # Nothing to work with.
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
base_uri uri.to_s
|
|
60
|
+
uri
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# returns true if successfully create a new job on Jenkins
|
|
64
|
+
# +job_config+ is a Jenkins::JobConfigBuilder instance
|
|
65
|
+
# +options+ are:
|
|
66
|
+
# :override - true, will delete any existing job with same name, else error
|
|
67
|
+
#
|
|
68
|
+
# returns true if successful, else false
|
|
69
|
+
#
|
|
70
|
+
# TODO Exceptions?
|
|
71
|
+
def self.create_job(name, job_config, options = {})
|
|
72
|
+
options = options.with_clean_keys
|
|
73
|
+
delete_job(name) if options[:override]
|
|
74
|
+
begin
|
|
75
|
+
res = post "/createItem/api/xml?name=#{CGI.escape(name)}", {
|
|
76
|
+
:body => job_config.to_xml, :format => :xml, :headers => { 'content-type' => 'application/xml' }
|
|
77
|
+
}
|
|
78
|
+
if res.code.to_i == 200
|
|
79
|
+
cache_configuration!
|
|
80
|
+
true
|
|
81
|
+
else
|
|
82
|
+
show_me_the_error(res)
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
rescue REXML::ParseException => e
|
|
86
|
+
# For some reason, if the job exists we get back half a page of HTML
|
|
87
|
+
raise JobAlreadyExistsError.new(name)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# returns true if successfully updated a job on Jenkins
|
|
92
|
+
# +job_config+ is a Jenkins::JobConfigBuilder instance
|
|
93
|
+
#
|
|
94
|
+
# returns true if successful, else false
|
|
95
|
+
#
|
|
96
|
+
# TODO Exceptions?
|
|
97
|
+
def self.update_job(name, job_config)
|
|
98
|
+
res = post "#{job_url name}/config.xml", {
|
|
99
|
+
:body => job_config.to_xml, :format => :xml, :headers => { 'content-type' => 'application/xml' }
|
|
100
|
+
}
|
|
101
|
+
if res.code.to_i == 200
|
|
102
|
+
cache_configuration!
|
|
103
|
+
true
|
|
104
|
+
else
|
|
105
|
+
show_me_the_error(res)
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Attempts to delete a job +name+
|
|
111
|
+
def self.delete_job(name)
|
|
112
|
+
res = post_plain "#{job_url name}/doDelete"
|
|
113
|
+
res.code.to_i == 302
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.build_job(name)
|
|
117
|
+
res = get_plain "/job/#{name}/build"
|
|
118
|
+
res.code.to_i == 302
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.summary
|
|
122
|
+
json = get "/api/json"
|
|
123
|
+
cache_configuration!
|
|
124
|
+
json
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.job_names
|
|
128
|
+
summary["jobs"].map {|job| job["name"]}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Return hash of job statuses
|
|
132
|
+
def self.job(name)
|
|
133
|
+
begin
|
|
134
|
+
json = get "/job/#{name}/api/json"
|
|
135
|
+
cache_configuration!
|
|
136
|
+
json
|
|
137
|
+
rescue Crack::ParseError
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Return a hash of information about a build.
|
|
143
|
+
def self.build_details(job_name, build_number)
|
|
144
|
+
begin
|
|
145
|
+
json = get "/job/#{job_name}/#{build_number}/api/json"
|
|
146
|
+
cache_configuration!
|
|
147
|
+
json
|
|
148
|
+
rescue Crack::ParseError
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Return the console log information about a build.
|
|
154
|
+
def self.console(job_name, axe, build_number)
|
|
155
|
+
path = "/job/#{job_name}/#{build_number}/"
|
|
156
|
+
path << "#{axe}/" if axe
|
|
157
|
+
path << "consoleText"
|
|
158
|
+
log = get_plain path
|
|
159
|
+
cache_configuration!
|
|
160
|
+
log.body
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.nodes
|
|
164
|
+
json = get "/computer/api/json"
|
|
165
|
+
cache_configuration!
|
|
166
|
+
json
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Adds SSH nodes only, for now
|
|
170
|
+
def self.add_node(options = {})
|
|
171
|
+
options = options.with_clean_keys
|
|
172
|
+
default_options = Hash.new
|
|
173
|
+
if options[:vagrant]
|
|
174
|
+
default_options.merge!(
|
|
175
|
+
:slave_port => 2222,
|
|
176
|
+
:slave_user => 'vagrant',
|
|
177
|
+
:master_key => "/Library/Ruby/Gems/1.8/gems/vagrant-0.6.7/keys/vagrant", # FIXME - hardcoded master username assumption
|
|
178
|
+
:slave_fs => "/vagrant/tmp/jenkins-slave/",
|
|
179
|
+
:description => "Automatically created by Jenkins.rb",
|
|
180
|
+
:executors => 2,
|
|
181
|
+
:exclusive => true
|
|
182
|
+
)
|
|
183
|
+
else
|
|
184
|
+
default_options.merge!(
|
|
185
|
+
:slave_port => 22,
|
|
186
|
+
:slave_user => 'deploy',
|
|
187
|
+
:master_key => "/home/deploy/.ssh/id_rsa", # FIXME - hardcoded master username assumption
|
|
188
|
+
:slave_fs => "/data/jenkins-slave/",
|
|
189
|
+
:description => "Automatically created by Jenkins.rb",
|
|
190
|
+
:executors => 2,
|
|
191
|
+
:exclusive => true
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
options = default_options.merge(options)
|
|
195
|
+
|
|
196
|
+
slave_host = options[:slave_host]
|
|
197
|
+
name = options[:name] || slave_host
|
|
198
|
+
labels = options[:labels].split(/\s*,\s*/).join(' ') if options[:labels]
|
|
199
|
+
|
|
200
|
+
type = "hudson.slaves.DumbSlave$DescriptorImpl"
|
|
201
|
+
|
|
202
|
+
fields = {
|
|
203
|
+
"name" => name,
|
|
204
|
+
"type" => type,
|
|
205
|
+
|
|
206
|
+
"json" => {
|
|
207
|
+
"name" => name,
|
|
208
|
+
"nodeDescription" => options[:description],
|
|
209
|
+
"numExecutors" => options[:executors],
|
|
210
|
+
"remoteFS" => options[:slave_fs],
|
|
211
|
+
"labelString" => labels,
|
|
212
|
+
"mode" => options[:exclusive] ? "EXCLUSIVE" : "NORMAL",
|
|
213
|
+
"type" => type,
|
|
214
|
+
"retentionStrategy" => { "stapler-class" => "hudson.slaves.RetentionStrategy$Always" },
|
|
215
|
+
"nodeProperties" => { "stapler-class-bag" => "true" },
|
|
216
|
+
"launcher" => {
|
|
217
|
+
"stapler-class" => "hudson.plugins.sshslaves.SSHLauncher",
|
|
218
|
+
"host" => slave_host,
|
|
219
|
+
"port" => options[:slave_port],
|
|
220
|
+
"username" => options[:slave_user],
|
|
221
|
+
"privatekey" => options[:master_key],
|
|
222
|
+
}
|
|
223
|
+
}.to_json
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
url = URI.parse("#{base_uri}/computer/doCreateItem")
|
|
227
|
+
|
|
228
|
+
req = Net::HTTP::Post.new(url.path)
|
|
229
|
+
req.set_form_data(fields)
|
|
230
|
+
|
|
231
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
232
|
+
|
|
233
|
+
response = http.request(req)
|
|
234
|
+
case response
|
|
235
|
+
when Net::HTTPFound
|
|
236
|
+
{ :name => name, :slave_host => slave_host }
|
|
237
|
+
else
|
|
238
|
+
# error message looks like:
|
|
239
|
+
# <td id="main-panel">
|
|
240
|
+
# <h1>Error</h1><p>Slave called 'localhost' already exists</p>
|
|
241
|
+
require "hpricot"
|
|
242
|
+
error = Hpricot(response.body).search("td#main-panel p").text
|
|
243
|
+
unless error.blank?
|
|
244
|
+
puts error
|
|
245
|
+
else
|
|
246
|
+
puts response.body # so we can find other errors
|
|
247
|
+
end
|
|
248
|
+
false
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.delete_node(name)
|
|
253
|
+
post_plain("#{base_uri}/computer/#{CGI::escape(name).gsub('+', '%20')}/doDelete/api/json")
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Helper for POST that don't barf at Jenkins's crappy API responses
|
|
257
|
+
def self.post_plain(path, data = "", options = {})
|
|
258
|
+
options = options.with_clean_keys
|
|
259
|
+
uri = URI.parse base_uri
|
|
260
|
+
res = Net::HTTP.start(uri.host, uri.port) do |http|
|
|
261
|
+
if RUBY_VERSION =~ /1.8/
|
|
262
|
+
http.post(path, options)
|
|
263
|
+
else
|
|
264
|
+
http.post(path, data, options)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Helper for GET that don't barf at Jenkins's crappy API responses
|
|
270
|
+
def self.get_plain(path, options = {})
|
|
271
|
+
options = options.with_clean_keys
|
|
272
|
+
uri = URI.parse base_uri
|
|
273
|
+
res = Net::HTTP.start(uri.host, uri.port) { |http| http.get(path, options) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.cache_configuration!
|
|
277
|
+
Jenkins::Config.config["base_uri"] = base_uri
|
|
278
|
+
Jenkins::Config.config["basic_auth"] = default_options[:basic_auth]
|
|
279
|
+
Jenkins::Config.store!
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
private
|
|
283
|
+
def self.setup_authentication(options)
|
|
284
|
+
username, password = options.delete(:username), options.delete(:password)
|
|
285
|
+
if username && password
|
|
286
|
+
basic_auth username, password
|
|
287
|
+
elsif Jenkins::Config.config["basic_auth"]
|
|
288
|
+
basic_auth Jenkins::Config.config["basic_auth"]["username"],
|
|
289
|
+
Jenkins::Config.config["basic_auth"]["password"]
|
|
290
|
+
end
|
|
291
|
+
options
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def self.job_url(name)
|
|
295
|
+
"#{base_uri}/job/#{URI.escape(name)}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def self.show_me_the_error(response)
|
|
299
|
+
require "hpricot"
|
|
300
|
+
doc = Hpricot(response.body)
|
|
301
|
+
error_msg = doc.search("td#main-panel p")
|
|
302
|
+
unless error_msg.inner_text.blank?
|
|
303
|
+
$stderr.puts error_msg.inner_text
|
|
304
|
+
else
|
|
305
|
+
# TODO - what are the errors we get?
|
|
306
|
+
puts "Server error:"
|
|
307
|
+
p response.code
|
|
308
|
+
puts response.body
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|