hudkins 0.0.1
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/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.txt +6 -0
- data/Manifest.txt +29 -0
- data/README.txt +98 -0
- data/Rakefile +25 -0
- data/bin/hudkins +5 -0
- data/lib/assets/config.xml +30 -0
- data/lib/assets/config_snippets/builders.xml +5 -0
- data/lib/assets/free_style_project.xml.erb +14 -0
- data/lib/assets/hudkinsrc +4 -0
- data/lib/hudkins.rb +263 -0
- data/lib/hudkins/command.rb +127 -0
- data/lib/hudkins/command/exec.rb +113 -0
- data/lib/hudkins/command/irb_start.rb +86 -0
- data/lib/hudkins/common.rb +69 -0
- data/lib/hudkins/errors.rb +7 -0
- data/lib/hudkins/job.rb +168 -0
- data/lib/hudkins/jobs.rb +86 -0
- data/lib/hudkins/mixin.rb +103 -0
- data/lib/hudkins/rake.rb +20 -0
- data/lib/hudkins/restclient.rb +130 -0
- data/lib/hudkins/sysinfo.rb +11 -0
- data/test/fixtures/config.erb +30 -0
- data/test/fixtures/jobs.erb +22 -0
- data/test/fixtures/new_project_config.erb +14 -0
- data/test/test_helper.rb +50 -0
- data/test/unit/hudkins/test_job.rb +65 -0
- data/test/unit/hudkins/test_jobs.rb +27 -0
- data/test/unit/test_hudkins.rb +74 -0
- metadata +153 -0
data/lib/hudkins/jobs.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
class Hudkins
|
2
|
+
# this class is just a convenience wrapper around an array of jobs.
|
3
|
+
# _FIX_ consider that Enumerable methods returns a Hudkins::Jobs obj instead of an array..
|
4
|
+
class Jobs
|
5
|
+
include Hudkins::Common
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(hudkins)
|
9
|
+
@hudkins = hudkins
|
10
|
+
data = @hudkins.get_parsed( "/api/json", :accept => "application/json" )
|
11
|
+
@jobs = Array.new
|
12
|
+
data["jobs"].each do |job|
|
13
|
+
@jobs << Hudkins::Job.new( @hudkins, job )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
# Enumerable
|
22
|
+
# This returns @jobs if no block_given.. so in effect we can use array
|
23
|
+
# methods like this: jobs.each.last
|
24
|
+
# is this hacky? Normal?
|
25
|
+
def each
|
26
|
+
@jobs.each do |job|
|
27
|
+
yield job if block_given?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def size
|
32
|
+
@jobs.size
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# === Description
|
37
|
+
# convenience method for returning all (or part) of just the names of jobs
|
38
|
+
def names name = ""
|
39
|
+
find_all_by_name( name ).map(&:name)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# :method: find_by_
|
44
|
+
# === Description
|
45
|
+
# Implements find_by_ and find_all_by_ methods according to any method to
|
46
|
+
# which that job responds.
|
47
|
+
#
|
48
|
+
# === Examples
|
49
|
+
# jobs.find_by_name "job_name"
|
50
|
+
#
|
51
|
+
# jobs.find_all_by_url /svn/x
|
52
|
+
|
53
|
+
##
|
54
|
+
# nodoc
|
55
|
+
def method_missing sym, *args, &block # :nodoc:
|
56
|
+
meth, name = missing_method_name( sym )
|
57
|
+
if name
|
58
|
+
self.send(meth) do |job|
|
59
|
+
# I can't remember why I'm using union... :/
|
60
|
+
# job.send(name).to_s.gsub(/-/, "_") =~ Regexp.union( args.map(&:to_s) )
|
61
|
+
# args is an array because of method_missing. but find(_all)_by only
|
62
|
+
# takes one parameter (for now..)
|
63
|
+
# to_s for symbols, also removes warnings if given a regexp with options.
|
64
|
+
arg = args.first.to_s #.gsub(/-/, "_")
|
65
|
+
regex = Regexp.new(arg, Regexp::IGNORECASE)
|
66
|
+
# gsub used so I can pass in symbols as names (which don't allow dashes
|
67
|
+
# to_s as job.url returns a URI
|
68
|
+
job.send(name).to_s.gsub(/-/, "_") =~ regex
|
69
|
+
end
|
70
|
+
else
|
71
|
+
super sym, *args, &block
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def respond_to? sym
|
76
|
+
missing_method_name sym or super sym
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def missing_method_name sym # :nodoc:
|
81
|
+
# (find|find_all)_by_(name|url|...)
|
82
|
+
[$1, $2] if sym.to_s =~ /^(.*)_by_(.*)$/ and Hudkins::Job.method_defined? $2
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
#--
|
2
|
+
##
|
3
|
+
# attr_accessor_from_config :method_name, "search_path"[, type]
|
4
|
+
# attr_reader_from_config :method_name, "search_path"[, type]
|
5
|
+
# attr_writer_from_config :method_name, "search_path"[, type]
|
6
|
+
#++
|
7
|
+
|
8
|
+
class Hudkins
|
9
|
+
##
|
10
|
+
# extend Hudkins::Mixin to include DSL style methods
|
11
|
+
module Mixin
|
12
|
+
##
|
13
|
+
# === Description
|
14
|
+
# Similar to attr_reader but takes additional parameters
|
15
|
+
#
|
16
|
+
# :bool type addes a method_name? alias
|
17
|
+
#
|
18
|
+
# === Examples:
|
19
|
+
# class MyObj
|
20
|
+
# attr_reader_from_config :disabled, "//project//disabled", :bool
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# MyObj.new.disabled? # => true
|
24
|
+
#
|
25
|
+
# === Parameters:
|
26
|
+
#
|
27
|
+
# +method_name+:: attr_reader method name
|
28
|
+
# +search_path+:: Nokogiri.at search path
|
29
|
+
# +type+:: :fixnum # => converts config content to integer
|
30
|
+
#
|
31
|
+
# :bool # => creates a boolean reader method. useful
|
32
|
+
# when the config returns string "true" but True class is
|
33
|
+
# desirable
|
34
|
+
# Class # => ie. a value of `Integer' will result in
|
35
|
+
# typecasting via Integer(value)
|
36
|
+
def attr_reader_from_config method_name, search_path, type = :default
|
37
|
+
# could we do something like inspect el.children.size? for arrays?
|
38
|
+
define_method method_name do
|
39
|
+
el = config.at(search_path)
|
40
|
+
if el
|
41
|
+
case type
|
42
|
+
when :bool then
|
43
|
+
/true/i === el.content
|
44
|
+
when :fixnum, :int then
|
45
|
+
el.content.to_i
|
46
|
+
when :array then
|
47
|
+
warn ":array not implemented yet"
|
48
|
+
#value = []
|
49
|
+
#el.children
|
50
|
+
when Class then
|
51
|
+
# Integer(value)
|
52
|
+
Kernel.send(type.name, el.content)
|
53
|
+
else
|
54
|
+
el.content
|
55
|
+
end
|
56
|
+
else
|
57
|
+
warn "`#{search_path}' was not found in config."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
# use alias_method instead of explicitely defining the method so I can
|
61
|
+
# call the method_name without the ? in internal methods.
|
62
|
+
alias_method "#{method_name}?".to_sym, method_name if :bool === type
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# === Description
|
67
|
+
# see attr_reader_from_config
|
68
|
+
#
|
69
|
+
# update config node
|
70
|
+
def attr_writer_from_config method_name, search_path, type = :default
|
71
|
+
define_method "#{method_name}=".to_sym do |arg|
|
72
|
+
config.at(search_path).content = arg
|
73
|
+
arg
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# === Description
|
79
|
+
# see attr_reader_from_config
|
80
|
+
#
|
81
|
+
# (optionally) update config node and immediately post back the config then
|
82
|
+
# return the updated value
|
83
|
+
#
|
84
|
+
# `!' is added to end of method_name to signify post call
|
85
|
+
def attr_post_from_config method_name, search_path, type = :default
|
86
|
+
attr_reader_from_config method_name, search_path, type unless self.respond_to? method_name
|
87
|
+
define_method "#{method_name}!".to_sym do |arg|
|
88
|
+
# complains if arg isn't given..
|
89
|
+
send("#{method_name}=", arg) if arg
|
90
|
+
post_config!
|
91
|
+
self.send(method_name)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# combines attr_{reader,writer,post}_from_config
|
97
|
+
def attr_accessor_from_config *args
|
98
|
+
attr_reader_from_config *args
|
99
|
+
attr_writer_from_config *args
|
100
|
+
attr_post_from_config *args
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/hudkins/rake.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rest_client"
|
3
|
+
require "json"
|
4
|
+
#require "active_record"
|
5
|
+
require "nokogiri"
|
6
|
+
|
7
|
+
$: << File.join( File.dirname( __FILE__ ), ".." )
|
8
|
+
require 'hudkins'
|
9
|
+
|
10
|
+
# Load our rakefile extensions
|
11
|
+
Dir["#{File.dirname(__FILE__)}/../tasks/**/*.rake"].sort.each { |ext| load ext }
|
12
|
+
|
13
|
+
##
|
14
|
+
# rake hudkins:update job=rservices-m
|
15
|
+
#
|
16
|
+
# rake hudkins:update_main
|
17
|
+
# rake hudkins:update_release_candidate
|
18
|
+
# rake hudkins:update_in_production
|
19
|
+
#
|
20
|
+
# rake hudkins:update:rservices_main
|
@@ -0,0 +1,130 @@
|
|
1
|
+
class Hudkins
|
2
|
+
##
|
3
|
+
# === Description
|
4
|
+
# Convenience class for RestClient responses
|
5
|
+
#
|
6
|
+
# RestClient.get returns (configurable via &block parameter to Hudkins#get)
|
7
|
+
# [#response, #request, #result]. I wanted to have all these available but
|
8
|
+
# not as an array :/
|
9
|
+
#
|
10
|
+
# === Example
|
11
|
+
# # get response headers
|
12
|
+
# hud.get( "/path" ).response.headers
|
13
|
+
#
|
14
|
+
# # do something based on response success
|
15
|
+
# response = hud.get "/path"
|
16
|
+
# raise response.message unless response.success?
|
17
|
+
class Response
|
18
|
+
attr_accessor :response, :request, :result
|
19
|
+
##
|
20
|
+
# === Example
|
21
|
+
# hud.get("/path") do |*args|
|
22
|
+
# Hudkins::Response.new( *args )
|
23
|
+
# end
|
24
|
+
# # => returns Hudkins::Response object
|
25
|
+
def initialize response, request, result
|
26
|
+
@response = response
|
27
|
+
@request = request
|
28
|
+
@result = result
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# === Description
|
33
|
+
# response = get "/path"
|
34
|
+
# JSON.pasre response.body
|
35
|
+
def body
|
36
|
+
response.body
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# === Description
|
41
|
+
# response status code
|
42
|
+
def code
|
43
|
+
result.code.to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# === Description
|
48
|
+
# RestClient response error
|
49
|
+
def errors
|
50
|
+
RestClient::Exceptions::EXCEPTIONS_MAP[response.code].new response
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# === Description
|
55
|
+
# #errors message
|
56
|
+
def message
|
57
|
+
errors.message
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# === Description
|
62
|
+
# was the response successful?
|
63
|
+
# does a simple test if the response code was 2xx
|
64
|
+
def success?
|
65
|
+
# I think RestClient::Response doesn't use Net::HTTP... any more, but I
|
66
|
+
# couldn't figure out how they want you to do this without doing case
|
67
|
+
# format or something like RestClient::Ok.. etc. (There is no equivalent
|
68
|
+
# RestClient::Success, although there is a RestClient::Redirect
|
69
|
+
case result
|
70
|
+
when Net::HTTPSuccess, Net::HTTPRedirection then
|
71
|
+
true
|
72
|
+
else
|
73
|
+
false
|
74
|
+
end
|
75
|
+
#case code
|
76
|
+
#when (200..299)
|
77
|
+
#true
|
78
|
+
#when (302)
|
79
|
+
## Found. seems to be status code for successful posts.. :/
|
80
|
+
#true
|
81
|
+
#else
|
82
|
+
#false
|
83
|
+
#end
|
84
|
+
end
|
85
|
+
# http_body.match(/<h1>Error<\/h1><p>([^<]*)<\/p>/)[0]
|
86
|
+
|
87
|
+
##
|
88
|
+
# Hudson returns type :javascript for get "/api/json" even if I explicietly set the :accept header
|
89
|
+
# and :html for config.xml
|
90
|
+
# so I'm going to use the request headers to get the returned content type... blah
|
91
|
+
|
92
|
+
def response_type
|
93
|
+
# unbelievable hudson!
|
94
|
+
# this probably isn't the best way, but MIME::Types returns an array...
|
95
|
+
MIME::Type.new request.headers[:accept]
|
96
|
+
end
|
97
|
+
|
98
|
+
def type
|
99
|
+
response_type.sub_type.to_sym
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# the idea behind this is that if the hudson server isn't available,
|
104
|
+
# getaddrinfo has a really long timeout.. like over 20 seconds! This is
|
105
|
+
# really a DNS issue, but I still don't want to wait that long and I couldn't
|
106
|
+
# figure out how to set a better timeout.
|
107
|
+
module HostLookup
|
108
|
+
# defaults to 2
|
109
|
+
TIMEOUT = 2
|
110
|
+
##
|
111
|
+
# timeout Socket.getaddrinfo after TIMEOUT seconds
|
112
|
+
#
|
113
|
+
# returns true/false
|
114
|
+
def self.available? uri, timeout = nil
|
115
|
+
timeout ||= TIMEOUT
|
116
|
+
pid = fork do
|
117
|
+
Socket.getaddrinfo( uri.host, uri.scheme )
|
118
|
+
end
|
119
|
+
begin
|
120
|
+
Timeout::timeout(timeout) do
|
121
|
+
Process.wait(pid)
|
122
|
+
end
|
123
|
+
true
|
124
|
+
rescue Timeout::Error
|
125
|
+
Process.kill(9, pid)
|
126
|
+
false
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
def find_svn_url host_name
|
4
|
+
url = "http://example.com/_priv/sysinfo"
|
5
|
+
uri = URI.parse url
|
6
|
+
uri.host = host_name
|
7
|
+
body = RestClient.get uri.to_s
|
8
|
+
# is there a better way to do this? Rservices and Syndication differ in where
|
9
|
+
# they put this info
|
10
|
+
svn_url = body.scan(/repository":"([^ ]*) r\d*",/).first
|
11
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?><project>
|
2
|
+
<actions/>
|
3
|
+
<description/>
|
4
|
+
<keepDependencies>false</keepDependencies>
|
5
|
+
<properties/>
|
6
|
+
<scm class="hudson.scm.SubversionSCM">
|
7
|
+
<locations>
|
8
|
+
<hudson.scm.SubversionSCM_-ModuleLocation>
|
9
|
+
<remote>https://subversion/project_name/branches/current_branch</remote>
|
10
|
+
<local>.</local>
|
11
|
+
</hudson.scm.SubversionSCM_-ModuleLocation>
|
12
|
+
</locations>
|
13
|
+
<useUpdate>true</useUpdate>
|
14
|
+
<doRevert>false</doRevert>
|
15
|
+
<excludedRegions/>
|
16
|
+
<includedRegions/>
|
17
|
+
<excludedUsers/>
|
18
|
+
<excludedRevprop/>
|
19
|
+
<excludedCommitMessages/>
|
20
|
+
</scm>
|
21
|
+
<canRoam>true</canRoam>
|
22
|
+
<disabled>true</disabled>
|
23
|
+
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
|
24
|
+
<triggers class="vector"/>
|
25
|
+
<concurrentBuild>false</concurrentBuild>
|
26
|
+
<builders/>
|
27
|
+
<publishers/>
|
28
|
+
<buildWrappers/>
|
29
|
+
</project>
|
30
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{"assignedLabels":[{}],
|
2
|
+
"mode":"NORMAL",
|
3
|
+
"nodeDescription":"the master Hudson node",
|
4
|
+
"nodeName":"",
|
5
|
+
"numExecutors":2,
|
6
|
+
"description":null,
|
7
|
+
"jobs":[
|
8
|
+
{"name":"project_name",
|
9
|
+
"url":"http://example.com/job/project_name/",
|
10
|
+
"color":"blue"},
|
11
|
+
{"name":"project_name2",
|
12
|
+
"url":"http://example.com/job/project_name2/",
|
13
|
+
"color":"blue"}],
|
14
|
+
"overallLoad":{},
|
15
|
+
"primaryView":{"name":"All",
|
16
|
+
"url":"http://example.com/"},
|
17
|
+
"slaveAgentPort":0,
|
18
|
+
"useCrumbs":false,
|
19
|
+
"useSecurity":false,
|
20
|
+
"views":[{"name":"All",
|
21
|
+
"url":"http://example.com/"}]}
|
22
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
2
|
+
<project>
|
3
|
+
<keepDependencies>false</keepDependencies>
|
4
|
+
<properties/>
|
5
|
+
<scm class="hudson.scm.NullSCM"/>
|
6
|
+
<canRoam>true</canRoam>
|
7
|
+
<disabled>false</disabled>
|
8
|
+
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
|
9
|
+
<triggers class="vector"/>
|
10
|
+
<concurrentBuild>false</concurrentBuild>
|
11
|
+
<builders/>
|
12
|
+
<publishers/>
|
13
|
+
<buildWrappers/>
|
14
|
+
</project>
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require 'minitest/unit'
|
3
|
+
require "mocha"
|
4
|
+
require "ostruct"
|
5
|
+
require "erb"
|
6
|
+
MiniTest::Unit.autorun
|
7
|
+
|
8
|
+
|
9
|
+
class MiniTest::Unit::TestCase
|
10
|
+
|
11
|
+
# minitest probably already has a fixtures something. but I don't have access
|
12
|
+
# to the docs currently.
|
13
|
+
# mock_files.config # => test/fixtures/config.erb
|
14
|
+
def mock_files
|
15
|
+
@mock_files ||= begin
|
16
|
+
obj = OpenStruct.new
|
17
|
+
glob = File.join( File.dirname(__FILE__), "fixtures", "*.erb")
|
18
|
+
files = Dir.glob( glob )
|
19
|
+
files.each do |file|
|
20
|
+
name = File.basename( file, ".erb" ).gsub(/\.-/, "_")
|
21
|
+
result = File.read( file )
|
22
|
+
content = ERB.new(result, 0, "%<>").result(binding)
|
23
|
+
obj.send "#{name}=", content # ruby is pretty amazing!
|
24
|
+
end
|
25
|
+
obj
|
26
|
+
end
|
27
|
+
end # mock_files
|
28
|
+
|
29
|
+
def hudkins_setup
|
30
|
+
@host = "http://example.com"
|
31
|
+
@hud = Hudkins.new @host
|
32
|
+
@mock_response = mock("response").responds_like(Hudkins::Response.new 1,2,3)
|
33
|
+
@mock_response.stubs(:success?).returns(true)
|
34
|
+
mock_rc_resource(:get, mock_files.jobs, :json)
|
35
|
+
@hud.jobs
|
36
|
+
end
|
37
|
+
|
38
|
+
def mock_rc_resource method, body = nil, type = nil
|
39
|
+
@mock_response.stubs(:body).returns(body) if body
|
40
|
+
@mock_response.stubs(:type).returns(type) if type
|
41
|
+
rc_request = RestClient::Request.expects(:execute).with(has_entry(:method => method))
|
42
|
+
if block_given?
|
43
|
+
yield(rc_request)
|
44
|
+
else
|
45
|
+
rc_request.returns( @mock_response )
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
end # MiniTest::Unit::TestCase
|