hudson-remote-api 0.1.0
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.
- data/.gitignore +1 -0
- data/README +20 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/hudson-remote-api.gemspec +47 -0
- data/lib/hudson-remote-api.rb +72 -0
- data/lib/hudson-remote-api/build.rb +29 -0
- data/lib/hudson-remote-api/build_queue.rb +18 -0
- data/lib/hudson-remote-api/errors.rb +3 -0
- data/lib/hudson-remote-api/job.rb +187 -0
- metadata +76 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
hudson_settings.yml
|
data/README
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
hudson-remote-api is ruby library to talk to Hudson's xml remote access api
|
2
|
+
|
3
|
+
Usage:
|
4
|
+
|
5
|
+
require 'hudson-remote-api'
|
6
|
+
|
7
|
+
# Configuration
|
8
|
+
Hudson[:host] = 'localhost'
|
9
|
+
Hudson[:user] = 'hudson'
|
10
|
+
Hudson[:password] = 'password'
|
11
|
+
|
12
|
+
# List all Hudson jobs
|
13
|
+
Hudson::Job.list
|
14
|
+
|
15
|
+
# List all active Hudson jobs
|
16
|
+
Hudson::Job.list_active
|
17
|
+
|
18
|
+
# print the last build number of a job
|
19
|
+
j = Hudson::Job.new('jobname')
|
20
|
+
puts j.last_build
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gemspec|
|
7
|
+
gemspec.name = "hudson-remote-api"
|
8
|
+
gemspec.summary = "Connect to Hudson's remote web API"
|
9
|
+
gemspec.description = "Connect to Hudson's remote web API"
|
10
|
+
gemspec.email = "Druwerd@gmail.com"
|
11
|
+
gemspec.homepage = "http://github.com/Druwerd/hudson-remote-api"
|
12
|
+
gemspec.authors = ["Dru Ibarra"]
|
13
|
+
gemspec.rubyforge_project = gemspec.name
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
|
18
|
+
end
|
19
|
+
|
20
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{hudson-remote-api}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Dru Ibarra"]
|
12
|
+
s.date = %q{2010-09-01}
|
13
|
+
s.description = %q{Connect to Hudson's remote web API}
|
14
|
+
s.email = %q{Druwerd@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"README",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"hudson-remote-api.gemspec",
|
24
|
+
"lib/hudson-remote-api.rb",
|
25
|
+
"lib/hudson-remote-api/build.rb",
|
26
|
+
"lib/hudson-remote-api/build_queue.rb",
|
27
|
+
"lib/hudson-remote-api/errors.rb",
|
28
|
+
"lib/hudson-remote-api/job.rb"
|
29
|
+
]
|
30
|
+
s.homepage = %q{http://github.com/Druwerd/hudson-remote-api}
|
31
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
s.rubyforge_project = %q{hudson-remote-api}
|
34
|
+
s.rubygems_version = %q{1.3.7}
|
35
|
+
s.summary = %q{Connect to Hudson's remote web API}
|
36
|
+
|
37
|
+
if s.respond_to? :specification_version then
|
38
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
39
|
+
s.specification_version = 3
|
40
|
+
|
41
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
42
|
+
else
|
43
|
+
end
|
44
|
+
else
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# This set of classes provides a Ruby interface to Hudson's web xml API
|
2
|
+
#
|
3
|
+
# Author:: Dru Ibarra
|
4
|
+
|
5
|
+
require 'net/http'
|
6
|
+
require 'rexml/document'
|
7
|
+
require 'cgi'
|
8
|
+
require 'yaml'
|
9
|
+
require 'zlib'
|
10
|
+
|
11
|
+
module Hudson
|
12
|
+
@@settings = {:host => 'localhost', :port => 80, :user => nil, :password => nil}
|
13
|
+
|
14
|
+
def self.[](param)
|
15
|
+
return @@settings[param]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.[]=(param,value)
|
19
|
+
@@settings[param]=value
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.settings=(settings)
|
23
|
+
@@settings = settings
|
24
|
+
end
|
25
|
+
|
26
|
+
HUDSON_URL_ROOT = ""
|
27
|
+
# Base class for all Hudson objects
|
28
|
+
class HudsonObject
|
29
|
+
|
30
|
+
def self.get_xml(path)
|
31
|
+
request = Net::HTTP::Get.new(path)
|
32
|
+
request.basic_auth(Hudson[:user], Hudson[:password]) if Hudson[:user] and Hudson[:password]
|
33
|
+
request['Content-Type'] = "text/xml"
|
34
|
+
response = Net::HTTP.start(Hudson[:host], Hudson[:port]){|http| http.request(request)}
|
35
|
+
|
36
|
+
if response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
37
|
+
encoding = response.get_fields("Content-Encoding")
|
38
|
+
if encoding and encoding.include?("gzip")
|
39
|
+
return Zlib::GzipReader.new(StringIO.new(response.body)).read
|
40
|
+
else
|
41
|
+
return response.body
|
42
|
+
end
|
43
|
+
else
|
44
|
+
puts response
|
45
|
+
raise APIError, "Error retrieving #{path}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_xml(path)
|
50
|
+
self.class.get_xml(path)
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_post_request(path, data={})
|
54
|
+
request = Net::HTTP::Post.new(path)
|
55
|
+
request.basic_auth(Hudson[:user], Hudson[:password]) if Hudson[:user] and Hudson[:password]
|
56
|
+
request.set_form_data(data)
|
57
|
+
#puts request.to_yaml
|
58
|
+
Net::HTTP.new(Hudson[:host], Hudson[:port]).start{|http| http.request(request)}
|
59
|
+
end
|
60
|
+
|
61
|
+
def send_xml_post_request(path, xml)
|
62
|
+
request = Net::HTTP::Post.new(path)
|
63
|
+
request.basic_auth(Hudson[:user], Hudson[:password]) if Hudson[:user] and Hudson[:password]
|
64
|
+
request.body = xml
|
65
|
+
#puts request.body
|
66
|
+
#puts request.to_yaml
|
67
|
+
Net::HTTP.new(Hudson[:host], Hudson[:port]).start{|http| http.request(request)}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Dir[File.dirname(__FILE__) + '/hudson-remote-api/*.rb'].each {|file| require file }
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'hudson-remote-api'
|
2
|
+
module Hudson
|
3
|
+
class Build < HudsonObject
|
4
|
+
attr_reader :number, :job, :revisions, :result
|
5
|
+
|
6
|
+
def initialize(job, build_number=nil)
|
7
|
+
@job = Job.new(job) if job.kind_of?(String)
|
8
|
+
@job = job if job.kind_of?(Hudson::Job)
|
9
|
+
if build_number
|
10
|
+
@number = build_number
|
11
|
+
else
|
12
|
+
@number = @job.last_build
|
13
|
+
end
|
14
|
+
@revisions = {}
|
15
|
+
load_build_info
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def load_build_info
|
20
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@job.name}/#{@number}/api/xml"
|
21
|
+
build_info_xml = get_xml(path)
|
22
|
+
build_info_doc = REXML::Document.new(build_info_xml)
|
23
|
+
|
24
|
+
if !build_info_doc.elements["/freeStyleBuild/changeSet"].nil?
|
25
|
+
build_info_doc.elements.each("/freeStyleBuild/changeSet/revision"){|e| @revisions[e.elements["module"].text] = e.elements["revision"].text }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'hudson-remote-api'
|
2
|
+
module Hudson
|
3
|
+
# This class provides an interface to Hudson's build queue
|
4
|
+
class BuildQueue < HudsonObject
|
5
|
+
# List the jobs in the queue
|
6
|
+
def self.list()
|
7
|
+
path = "#{HUDSON_URL_ROOT}/queue/api/xml"
|
8
|
+
xml = get_xml(path)
|
9
|
+
queue = []
|
10
|
+
queue_doc = REXML::Document.new(xml)
|
11
|
+
return queue if queue_doc.elements["/queue/item"].nil?
|
12
|
+
queue_doc.each_element("/queue/item/task") do |job|
|
13
|
+
queue << job.elements["name"].text
|
14
|
+
end
|
15
|
+
queue
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'hudson-remote-api'
|
2
|
+
module Hudson
|
3
|
+
# This class provides an interface to Hudson jobs
|
4
|
+
class Job < HudsonObject
|
5
|
+
|
6
|
+
attr_accessor :name, :config, :repository_url, :repository_urls, :repository_browser_location, :description
|
7
|
+
attr_reader :color, :last_build, :last_completed_build, :last_failed_build, :last_stable_build, :last_successful_build, :last_unsuccessful_build, :next_build_number
|
8
|
+
|
9
|
+
# List all Hudson jobs
|
10
|
+
def self.list()
|
11
|
+
path = "#{HUDSON_URL_ROOT}/api/xml"
|
12
|
+
xml = get_xml(path)
|
13
|
+
|
14
|
+
jobs = []
|
15
|
+
jobs_doc = REXML::Document.new(xml)
|
16
|
+
jobs_doc.each_element("hudson/job") do |job|
|
17
|
+
jobs << job.elements["name"].text
|
18
|
+
end
|
19
|
+
jobs
|
20
|
+
end
|
21
|
+
|
22
|
+
# List all jobs in active execution
|
23
|
+
def self.list_active
|
24
|
+
path = "#{HUDSON_URL_ROOT}/api/xml"
|
25
|
+
xml = get_xml(path)
|
26
|
+
|
27
|
+
active_jobs = []
|
28
|
+
jobs_doc = REXML::Document.new(xml)
|
29
|
+
jobs_doc.each_element("hudson/job") do |job|
|
30
|
+
if job.elements["color"].text.include?("anime")
|
31
|
+
active_jobs << job.elements["name"].text
|
32
|
+
end
|
33
|
+
end
|
34
|
+
active_jobs
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(name)
|
38
|
+
@name = name
|
39
|
+
load_config
|
40
|
+
load_info
|
41
|
+
end
|
42
|
+
|
43
|
+
# Load data from Hudson's Job configuration settings into class variables
|
44
|
+
def load_config()
|
45
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/config.xml"
|
46
|
+
@config = get_xml(path)
|
47
|
+
@config_doc = REXML::Document.new(@config)
|
48
|
+
|
49
|
+
@config_doc = REXML::Document.new(@config)
|
50
|
+
if !@config_doc.elements["/project/scm/locations/hudson.scm.SubversionSCM_-ModuleLocation/remote"].nil?
|
51
|
+
@repository_url = @config_doc.elements["/project/scm/locations/hudson.scm.SubversionSCM_-ModuleLocation/remote"].text || ""
|
52
|
+
end
|
53
|
+
@repository_urls = []
|
54
|
+
if !@config_doc.elements["/project/scm/locations"].nil?
|
55
|
+
@config_doc.elements.each("/project/scm/locations/hudson.scm.SubversionSCM_-ModuleLocation"){|e| @repository_urls << e.elements["remote"].text }
|
56
|
+
end
|
57
|
+
if !@config_doc.elements["/project/scm/browser/location"].nil?
|
58
|
+
@repository_browser_location = @config_doc.elements["/project/scm/browser/location"].text
|
59
|
+
end
|
60
|
+
@description = @config_doc.elements["/project/description"].text || ""
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_info()
|
64
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/api/xml"
|
65
|
+
@info = get_xml(path)
|
66
|
+
@info_doc = REXML::Document.new(@info)
|
67
|
+
|
68
|
+
#FIXME: make sure it's really a freeStyleProject
|
69
|
+
@color = @info_doc.elements["/freeStyleProject/color"].text
|
70
|
+
@last_build = @info_doc.elements["/freeStyleProject/lastBuild/number"].text
|
71
|
+
@last_completed_build = @info_doc.elements["/freeStyleProject/lastCompletedBuild/number"].text
|
72
|
+
@last_failed_build = @info_doc.elements["/freeStyleProject/lastFailedBuild/number"].text if @info_doc.elements["/freeStyleProject/lastFailedBuild/number"]
|
73
|
+
@last_stable_build = @info_doc.elements["/freeStyleProject/lastStableBuild/number"].text if @info_doc.elements["/freeStyleProject/lastStableBuild/number"]
|
74
|
+
@last_successful_build = @info_doc.elements["/freeStyleProject/lastSuccessfulBuild/number"].text if @info_doc.elements["/freeStyleProject/lastSuccessfulBuild/number"]
|
75
|
+
@last_unsuccessful_build = @info_doc.elements["/freeStyleProject/lastUnsuccessfulBuild/number"].text if @info_doc.elements["/freeStyleProject/lastUnsuccessfulBuild/number"]
|
76
|
+
@next_build_number = @info_doc.elements["/freeStyleProject/nextBuildNumber"].text
|
77
|
+
end
|
78
|
+
|
79
|
+
def active?
|
80
|
+
Job.list_active.include?(@name)
|
81
|
+
end
|
82
|
+
|
83
|
+
def wait_for_build_to_finish(poll_freq=10)
|
84
|
+
loop do
|
85
|
+
puts "waiting for all #{@name} builds to finish"
|
86
|
+
sleep poll_freq # wait
|
87
|
+
break if !active? and !BuildQueue.list.include?(@name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create a new job on Hudson server based on the current job object
|
92
|
+
def copy(new_job=nil)
|
93
|
+
new_job = "copy_of_#{@name}" if new_job.nil?
|
94
|
+
path = "#{HUDSON_URL_ROOT}/createItem"
|
95
|
+
|
96
|
+
response = send_post_request(path, {:name=>new_job, :mode=>"copy", :from=>@name})
|
97
|
+
raise(APIError, "Error copying job #{@name}: #{response.body}") if response.class != Net::HTTPFound
|
98
|
+
Job.new(new_job)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Update the job configuration on Hudson server
|
102
|
+
def update(config=nil)
|
103
|
+
@config = config if !config.nil?
|
104
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/config.xml"
|
105
|
+
response = send_xml_post_request(path, @config)
|
106
|
+
response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Set the repository url and update on Hudson server
|
110
|
+
def repository_url=(repository_url)
|
111
|
+
return false if @repository_url.nil?
|
112
|
+
@repository_url = repository_url
|
113
|
+
@config_doc.elements["/project/scm/locations/hudson.scm.SubversionSCM_-ModuleLocation/remote"].text = repository_url
|
114
|
+
@config = @config_doc.to_s
|
115
|
+
update
|
116
|
+
end
|
117
|
+
|
118
|
+
def repository_urls=(repository_urls)
|
119
|
+
return false if !repository_urls.class == Array
|
120
|
+
@repository_urls = repository_urls
|
121
|
+
|
122
|
+
i = 0
|
123
|
+
@config_doc.elements.each("/project/scm/locations/hudson.scm.SubversionSCM_-ModuleLocation") do |location|
|
124
|
+
location.elements["remote"].text = @repository_urls[i]
|
125
|
+
i += 1
|
126
|
+
end
|
127
|
+
|
128
|
+
@config = @config_doc.to_s
|
129
|
+
update
|
130
|
+
end
|
131
|
+
|
132
|
+
# Set the repository browser location and update on Hudson server
|
133
|
+
def repository_browser_location=(repository_browser_location)
|
134
|
+
@repository_browser_location = repository_browser_location
|
135
|
+
@config_doc.elements["/project/scm/browser/location"].text = repository_browser_location
|
136
|
+
@config = @config_doc.to_s
|
137
|
+
update
|
138
|
+
end
|
139
|
+
|
140
|
+
# Set the job description and update on Hudson server
|
141
|
+
def description=(description)
|
142
|
+
@description = description
|
143
|
+
@config_doc.elements["/project/description"].text = description
|
144
|
+
@config = @config_doc.to_s
|
145
|
+
update
|
146
|
+
end
|
147
|
+
|
148
|
+
# Start building this job on Hudson server (can't build parameterized jobs)
|
149
|
+
def build()
|
150
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/build"
|
151
|
+
response = send_post_request(path, {:delay => '0sec'})
|
152
|
+
response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
153
|
+
end
|
154
|
+
|
155
|
+
def disable()
|
156
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/disable"
|
157
|
+
response = send_post_request(path)
|
158
|
+
puts response.class
|
159
|
+
response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
160
|
+
end
|
161
|
+
|
162
|
+
def enable()
|
163
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/enable"
|
164
|
+
response = send_post_request(path)
|
165
|
+
puts response.class
|
166
|
+
response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Delete this job from Hudson server
|
170
|
+
def delete()
|
171
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/doDelete"
|
172
|
+
response = send_post_request(path)
|
173
|
+
response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
174
|
+
end
|
175
|
+
|
176
|
+
def wipe_out_workspace()
|
177
|
+
wait_for_build_to_finish
|
178
|
+
path = "#{HUDSON_URL_ROOT}/job/#{@name}/doWipeOutWorkspace"
|
179
|
+
if !active?
|
180
|
+
response = send_post_request(path)
|
181
|
+
else
|
182
|
+
response = false
|
183
|
+
end
|
184
|
+
response.is_a?(Net::HTTPSuccess) or response.is_a?(Net::HTTPRedirection)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hudson-remote-api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Dru Ibarra
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-09-01 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Connect to Hudson's remote web API
|
23
|
+
email: Druwerd@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- README
|
33
|
+
- Rakefile
|
34
|
+
- VERSION
|
35
|
+
- hudson-remote-api.gemspec
|
36
|
+
- lib/hudson-remote-api.rb
|
37
|
+
- lib/hudson-remote-api/build.rb
|
38
|
+
- lib/hudson-remote-api/build_queue.rb
|
39
|
+
- lib/hudson-remote-api/errors.rb
|
40
|
+
- lib/hudson-remote-api/job.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/Druwerd/hudson-remote-api
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project: hudson-remote-api
|
71
|
+
rubygems_version: 1.3.7
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Connect to Hudson's remote web API
|
75
|
+
test_files: []
|
76
|
+
|