hudkins 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|