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.
@@ -0,0 +1,127 @@
1
+ require "hudkins"
2
+ require "optparse"
3
+ require "yaml"
4
+
5
+ ##
6
+ # === Description
7
+ # Main class for the included bin to interact with Hudson at the command line.
8
+ #
9
+ # The Commands are setup in Hudkins::Command::Exec
10
+ #
11
+ # === Usage
12
+ # see `hudkins -h'
13
+ #
14
+ # a host option is required. Can be configured in env variable +hudkins_host+ or
15
+ # config file. all options can be specified in a yaml file in +~/.hudkinsrc+
16
+ # and/or +`pwd`/.hudkinsrc+
17
+ class Hudkins::Command
18
+ include Hudkins::Common
19
+
20
+ require "hudkins/command/exec"
21
+ # all the run_ commands
22
+ extend Hudkins::Command::Exec
23
+
24
+ class << self
25
+ def run
26
+ parse_options
27
+ begin
28
+ return_string = send( @command )
29
+ puts return_string if return_string
30
+ rescue => e
31
+ raise_usage e.message
32
+ end
33
+ if @options[:irb]
34
+ run_start_irb
35
+ end
36
+ end
37
+
38
+ # commands:
39
+ # helpers:
40
+ def usage_msg
41
+ usage = <<-EOB
42
+
43
+ Usage: hudkins [opts] commands [job_name]
44
+
45
+ Commands: unambiguous_partial_command
46
+ build: start a job building.
47
+ config: pretty print job config. job_name
48
+ create: create new hudson job.
49
+ host: print hudson host url
50
+ list: list jobs. [job_name used as filter].
51
+
52
+
53
+ Optional: job_name (depends on command)
54
+ may also be partial (picks first match)
55
+
56
+ EOB
57
+ end
58
+
59
+ def raise_usage msg = nil
60
+ warn msg if msg
61
+ puts usage_msg
62
+ exit 1
63
+ end
64
+
65
+ def parse_options
66
+ # initialize cmd_list. This seems hacky, but I like having the cmd_list
67
+ # near the usage statement.
68
+ @options = {}
69
+
70
+ OptionParser.new do |opts|
71
+ opts.banner = usage_msg
72
+
73
+ opts.separator ""
74
+ opts.separator "Specific options:"
75
+
76
+ opts.on("--hud-host HOST", "Hudson host to connect to. Overrides rc file") do |h|
77
+ @options[:host] = h
78
+ end
79
+
80
+ opts.on("-i", "--irb", "Drop into irb.") do |i|
81
+ @options[:irb] = i
82
+ end
83
+ opts.on("-v", "--[no-]verbose", "Turn on verbose messages.") do |v|
84
+ $VERBOSE = @options[:verbose] = v
85
+ end
86
+
87
+ opts.separator ""
88
+ opts.separator "Common options:"
89
+
90
+ opts.on_tail("--version", "Show version") do
91
+ puts "Hudkins (#{Hudkins::VERSION}) (c) 2010"
92
+ exit
93
+ end
94
+
95
+ opts.on_tail("-h", "--help", "Show this message") do
96
+ puts opts
97
+ exit
98
+ end
99
+
100
+ end.parse!
101
+
102
+ config.merge! @options
103
+
104
+ @command, @job_name = ARGV.shift(2)
105
+ @command ||= "default"
106
+
107
+ @hud = Hudkins.new hud_host
108
+ job @job_name
109
+
110
+ parse_command
111
+ end
112
+
113
+ def parse_command
114
+ # select unambiguous command to run.
115
+ cmds = cmd_list.select {|c| Regexp.new(@command.to_s, Regexp::IGNORECASE) === c}
116
+ case cmds.size
117
+ when 0 then
118
+ raise_usage "#{@command} is not recognized as a valid command."
119
+ when 1 then
120
+ @command = "run_" << cmds.first
121
+ else
122
+ raise_usage "Ambiguous command. Please specify:\n #{cmds.join("\n ")}\n\n"
123
+ end
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,113 @@
1
+ class Hudkins
2
+ ##
3
+ # === Description
4
+ # mixin for interacting with the lib
5
+ #
6
+ # the bin is setup to run any command prefaced with `run_'
7
+ # each command should return something to be puts or nil
8
+ module Command::Exec
9
+ ##
10
+ # default command. here as a place holder
11
+ def run_default
12
+ end
13
+
14
+ def run_build
15
+ "building!!!"
16
+ end
17
+
18
+ def run_config job_name = @job_name
19
+ required_params job_name => "job_name is required for this command."
20
+ [
21
+ job( job_name ).name,
22
+ job( job_name ).config
23
+ ]
24
+ end
25
+
26
+ def run_create job_name = @job_name
27
+ required_params job_name => "job_name is required for this command."
28
+ "creating!!!"
29
+ end
30
+
31
+ def run_host
32
+ hud_host
33
+ end
34
+
35
+ def run_list job_name = @job_name
36
+ names( job_name || "" )
37
+ end
38
+
39
+ def run_start_irb
40
+ puts <<-EOS
41
+
42
+ ---
43
+ Welcome to hudkins irb console.
44
+ type hudkins for help.
45
+
46
+ EOS
47
+ require "hudkins/command/irb_start"
48
+ # turn job names into methods
49
+ extend Hudkins::Command::Irb
50
+ IRB.start_session(nil, binding)
51
+ end
52
+
53
+ # helper commands
54
+
55
+ def hud
56
+ @hud
57
+ end
58
+
59
+ def job job_name = nil
60
+ if job_name
61
+ @job = hud.jobs.find_by_name job_name
62
+ end
63
+ @job
64
+ end
65
+
66
+ # could probably be cleaned up
67
+ def required_params values
68
+ e = Hudkins::ArgumentError.new ""
69
+ raize = false
70
+ values.each do |k,msg|
71
+ e << msg unless k
72
+ raize = true unless k
73
+ end
74
+ raise e if raize
75
+ end
76
+
77
+ def cmd_list
78
+ command_list.map {|m| m.gsub(/^run_/, '')}
79
+ end
80
+
81
+ def command_list
82
+ self.methods.select {|m| m =~ /^run_/}
83
+ end
84
+
85
+ def names name = ""
86
+ hud.jobs.names name
87
+ end
88
+
89
+ def hud_host
90
+ ENV["hudkins_host"] || config[:host] || raise_usage( "no hudkins_host defined." )
91
+ end
92
+
93
+ def config
94
+ @config ||= load_rc
95
+ end
96
+
97
+ def load_rc
98
+ hudkins_rc.inject({}) do |ret, rc|
99
+ h = YAML.load_file( rc )
100
+ ret.merge! h if h
101
+ ret
102
+ end
103
+ end
104
+
105
+ def hudkins_rc
106
+ [
107
+ File.expand_path("~/.hudkinsrc"),
108
+ File.join(Dir.pwd, ".hudkinsrc")
109
+ ].select {|f| File.size? f} # exists and is non 0
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,86 @@
1
+ require "irb"
2
+
3
+ # thank you!
4
+ # http://jameskilton.com/2009/04/02/embedding-irb-into-your-ruby-application/
5
+
6
+ module IRB # :nodoc:
7
+ # originally used the code snippet from the above link, which turned out to
8
+ # be a snippet from `ruby-debug'. However, I was having problems with CNTR-C
9
+ # and I realized I wanted to mimic irb, just be able to actually pass Irb.new
10
+ # a workspace param which it expected.
11
+ def self.start_session(ap_path = nil, binding = nil)
12
+ @@hack_binding = binding
13
+ IRB.start ap_path
14
+ end
15
+
16
+ def self.get_binding
17
+ @@hack_binding
18
+ end
19
+
20
+ class Irb
21
+ alias_method :org_initialize, :initialize
22
+ def initialize *args
23
+ workspace = args.shift
24
+ workspace ||= WorkSpace.new(IRB.get_binding)
25
+ args.unshift workspace
26
+ org_initialize *args
27
+ end
28
+ end
29
+ end
30
+
31
+ class Hudkins
32
+ module Command::Irb
33
+
34
+ ##
35
+ # In the irb console I want to access the same commands as
36
+ # Hudkins::Command::Exec but without the `run_' part.
37
+ def self.extend_object obj # :doc:
38
+ # in IRB allow run_* commands to be exec without `run_'
39
+ obj.command_list.each do |cmd|
40
+ s = <<-EOE
41
+ def self.#{cmd.gsub(/^run_/, '')} *args; #{cmd} *args; end
42
+ EOE
43
+ obj.module_eval s
44
+ end
45
+ super
46
+ end
47
+
48
+ ##
49
+ # This is cool. Override default method to add convenience methods for each
50
+ # job_name in #jobs. This allows `job_name' to be used as a method to
51
+ # access that particular job.
52
+ def method_missing sym, *args, &block
53
+ if job = @hud.jobs.find_by_name( sym )
54
+ job
55
+ else
56
+ super sym, *args, &block
57
+ end
58
+ end
59
+
60
+ ##
61
+ # help method in irb console
62
+ def hudkins
63
+ puts <<-EOS
64
+
65
+ self is Hudkins::Command
66
+ hudkins => This help message
67
+ reload! => reload lib
68
+ hud => Hudkins.new <hud_host>
69
+ job [job_name] => Hudkins::Job
70
+ job_name => Hudkins::Job
71
+ commands:
72
+ #{cmd_list.join("\n ")}
73
+
74
+ EOS
75
+ end
76
+
77
+ ##
78
+ # helper method in irb console to reload all the lib files.
79
+ def reload!
80
+ $".grep( /hudkins/ ).each do |f|
81
+ load f
82
+ end
83
+ nil
84
+ end
85
+ end # Command::Irb
86
+ end # Hudkins
@@ -0,0 +1,69 @@
1
+ class Hudkins
2
+ # common methods available for all classes
3
+ module Common
4
+
5
+ # hide instance variables
6
+ def inspect( string = nil )
7
+ new_string = " " << string if string
8
+ "#<%s:0x%x%s>" % [self.class, (self.object_id << 1), new_string]
9
+ end
10
+
11
+ ##
12
+ # === Description
13
+ # useful for escaping parameters to be sent to #get/#post
14
+ #
15
+ # === Parameters
16
+ # +Hash+:: converts a simple one dimentional hash to a url parameter
17
+ # query string and uri escapes it.
18
+ # +else+:: converts to string and uri escapes it.
19
+ def url_escape msg
20
+ case msg
21
+ when Hash then
22
+ URI.escape( msg.map {|k,v| "#{k}=#{v}" }.join("&") )
23
+ else
24
+ URI.escape( msg.to_s )
25
+ end
26
+ end
27
+
28
+ #--
29
+ # these classes are being included in the @hudkins class.
30
+ #++
31
+
32
+ def hudkins
33
+ @hudkins
34
+ end
35
+
36
+ ##
37
+ # accessor methods for child classes to get
38
+ def get *args
39
+ @hudkins.get *args
40
+ end
41
+
42
+ ##
43
+ # accessor methods for child classes to post
44
+ def post *args
45
+ @hudkins.post *args
46
+ end
47
+
48
+ ##
49
+ # TODO host is not universally accessible so this method might not belong here..
50
+ # === Notes
51
+ # My Hudson server is not publicially accessible so when I can't connect,
52
+ # Socket#getaddrinfo (RestClient) times out after more than 20 seconds! I
53
+ # created a healper class Hudkins::HostLookup to timeout Socket#getaddrinfo
54
+ # after Hudkins::HostLookup#TIMEOUT seconds (defaults to 2).
55
+ def host_available?
56
+ HostLookup.available? @host, @options[:host_timeout]
57
+ end
58
+
59
+ ##
60
+ # TODO host is not universally accessible so this method might not belong here..
61
+ # Raise TimeoutError unless hudson host is responding within specified number of seconds.
62
+ def check_host_availability
63
+ # host is URI.
64
+ msg = "`%s' did not respond within the set number of seconds." % [ host.host ]
65
+ raise TimeoutError, msg unless host_available?
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,7 @@
1
+ class Hudkins
2
+ class ArgumentError < ::ArgumentError
3
+ def << (msg)
4
+ self.message << "\n" + msg
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,168 @@
1
+ # === Description
2
+ # Primary class for interacting with a Hudson job
3
+ #
4
+ # === Examples
5
+ # hud = Hudkins.new "http://hudson.com"
6
+ # job = hud.jobs.find_by_name :job_name
7
+ #
8
+ # job.disabled? # => true
9
+ # job.disabled = false
10
+ # job.post_config!
11
+ #
12
+ # === attr_accessor_from_config methods
13
+ #
14
+ # I created custom attr_accessor like DSL methods that create accessor like
15
+ # methods for the Hudkins::Job object to easily interact with the xml config.
16
+ # Paradigm is to use method_name? for boolean values and method_name! for any
17
+ # methods that post updates to the server.
18
+ #
19
+ class Hudkins::Job
20
+ include Hudkins::Common
21
+ extend Hudkins::Mixin
22
+
23
+ include Comparable
24
+
25
+ attr_reader :name, :url, :color, :path, :config
26
+
27
+ #TODO figure out how to define a custom rdoc method to correctly document these
28
+
29
+ ##
30
+ # :attr_accessor: scm_url
31
+ attr_accessor_from_config :scm_url, "//scm//remote"
32
+ ##
33
+ # :attr_accessor: description
34
+ attr_accessor_from_config :description, "//project//descriptoin"
35
+ ##
36
+ # :attr_accessor: can_roam
37
+ attr_accessor_from_config :can_roam, "//project//canRoam", :bool
38
+ ##
39
+ # :attr_accessor: disabled
40
+ attr_accessor_from_config :disabled, "//project//disabled", :bool
41
+ ##
42
+ # :attr_accessor: blocked_by_upstream
43
+ attr_accessor_from_config :blocked_by_upstream, "//project//blockBuildWhenUpstreamBuilding", :bool
44
+ ##
45
+ # :attr_accessor: concurrent_builds
46
+ attr_accessor_from_config :concurrent_builds, "//project//concurrentBuild", :bool
47
+ attr_accessor_from_config :mail_recipients, "//publishers//recipients"
48
+ # this is really an array. I haven't added functionality for that yet
49
+ attr_accessor_from_config :shell_builders, "//builders/hudson.tasks.Shell", :array
50
+ ##
51
+ # :attr_accessor: :rotate_logs_num
52
+ # Number of builds to keep
53
+ # -1 == infinite
54
+ attr_accessor_from_config :rotate_logs_num, "//logRotator/numToKeep", :int
55
+ ##
56
+ # :attr_accessor: :rotate_logs_days
57
+ # Number of days to keep builds
58
+ # -1 == infinite
59
+ attr_accessor_from_config :rotate_logs_days, "//logRotator/daysToKeep", Integer
60
+
61
+ def initialize hudkins, data
62
+ @hudkins = hudkins
63
+ @name = data["name"]
64
+ @url = URI.parse data["url"]
65
+ @color = data["color"]
66
+ @path = @url.path
67
+ end
68
+
69
+ def inspect
70
+ super "@name=#{@name}"
71
+ end
72
+
73
+ def url
74
+ @url.to_s
75
+ end
76
+
77
+ # Enumerables/Comparables...
78
+ def <=> other
79
+ # TODO how do I implement jobs.sort(&:path) ?
80
+ self.name <=> other.name
81
+ end
82
+
83
+ ##
84
+ # === Description
85
+ # get the job's config
86
+ # takes string (xml) or other's config
87
+ def update_config config = nil
88
+ config = case config
89
+ when String then
90
+ hudkins.parse_string( config, :xml )
91
+ when Nokogiri::XML::Document, NilClass then
92
+ config
93
+ else
94
+ raise "unknown config type #{config.class}"
95
+ end
96
+ @config = config || hudkins.get_parsed( path + "/config.xml", :accept => "application/xml" )
97
+ end
98
+
99
+ ##
100
+ # === Description
101
+ # accessor for job's config. Initializes then caches. Use update_config if out of date.
102
+ def config
103
+ @config ||= update_config
104
+ end
105
+
106
+ ##
107
+ # === Description
108
+ # Post the job's config back to the server to update it.
109
+ def post_config!
110
+ post path + "/config.xml", @config
111
+ end
112
+
113
+ def build!
114
+ get path + "/build"
115
+ end
116
+
117
+ def delete!
118
+ post path + "/doDelete"
119
+ end
120
+
121
+ def recreate!
122
+ hudkins.add_job name, config
123
+ end
124
+
125
+ def disable!
126
+ post path + "/disable"
127
+ end
128
+
129
+ def enable!
130
+ post path + "/enable"
131
+ end
132
+
133
+ ##
134
+ # === Description
135
+ # The remote api allows for updating just the description. I had to tweak the
136
+ # name because I still wanted description to be an attr_accessor_from_config
137
+ #
138
+ # === Example
139
+ # job.quick_description! # => "this is the description for job"
140
+ # job.quick_description! = "this is the new desc." # => Response obj
141
+ def quick_description! msg = nil
142
+ # another yuck!
143
+ if msg
144
+ post path + "/description?" + url_escape(:description => msg)
145
+ else
146
+ get( path + "/description" ).body
147
+ end
148
+ end
149
+
150
+ ##
151
+ # === Description
152
+ # Copy job to new_job_name
153
+ #
154
+ # === Example
155
+ # new_job = job.copy "new-job-name"
156
+ # new_job.scm_url = "http://svn/new/job/path"
157
+ # new_job.post_config!
158
+ def copy new_job_name
159
+ # post /createItem?name=NEWJOBNAME&mode=copy&from=FROMJOBNAME
160
+ response = post "/createItem?" +
161
+ url_escape(:name => new_job_name, :mode => "copy", :from => name)
162
+ # return new job object
163
+ hudkins.update_jobs.find_by_name new_job_name if response.success?
164
+ end
165
+
166
+ # not sure if we want this.
167
+ # alias_method :dup, :copy
168
+ end