lhj-tools 0.2.13 → 0.2.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,1933 +0,0 @@
1
- require 'json'
2
- require 'net/http'
3
- require 'net/https'
4
- require 'base64'
5
- require 'uri'
6
- require 'logger'
7
- require 'open-uri'
8
-
9
- module Lhj
10
- module JenkinsApi
11
- class Client
12
- class Job
13
-
14
- include JenkinsApi::UriHelper
15
-
16
- # Version that jenkins started to include queued build info in build response
17
- JENKINS_QUEUE_ID_SUPPORT_VERSION = '1.519'
18
-
19
- attr_reader :plugin_collection
20
-
21
- # Initialize the Job object and store the reference to Client object
22
- #
23
- # @param client [Client] the client object
24
- #
25
- # @return [Job] the job object
26
- #
27
- def initialize(client, *plugin_settings)
28
- @client = client
29
- @logger = @client.logger
30
- end
31
-
32
- # Return a string representation of the object
33
- #
34
- def to_s
35
- "#<JenkinsApi::Client::Job>"
36
- end
37
-
38
- # Create or Update a job with the name specified and the xml given
39
- #
40
- # @param job_name [String] the name of the job
41
- # @param xml [String] the xml configuration of the job
42
- #
43
- # @see #create
44
- # @see #update
45
- #
46
- # @return [String] the HTTP status code from the POST request
47
- #
48
- def create_or_update(job_name, xml)
49
- if exists?(job_name)
50
- update(job_name, xml)
51
- else
52
- create(job_name, xml)
53
- end
54
- end
55
-
56
- # Create a job with the name specified and the xml given
57
- #
58
- # @param job_name [String] the name of the job
59
- # @param xml [String] the xml configuration of the job
60
- #
61
- # @see #create_or_update
62
- # @see #update
63
- #
64
- # @return [String] the HTTP status code from the POST request
65
- #
66
- def create(job_name, xml)
67
- @logger.info "Creating job '#{job_name}'"
68
- @client.post_config("/createItem?name=#{form_encode job_name}", xml)
69
- end
70
-
71
- # Update a job with the name specified and the xml given
72
- #
73
- # @param job_name [String] the name of the job
74
- # @param xml [String] the xml configuration of the job
75
- #
76
- # @see #create_or_update
77
- # @see #create
78
- #
79
- # @return [String] the HTTP status code from the POST request
80
- #
81
- def update(job_name, xml)
82
- @logger.info "Updating job '#{job_name}'"
83
- post_config(job_name, xml)
84
- end
85
-
86
- # Create or Update a job with params given as a hash instead of the xml
87
- # This gives some flexibility for creating/updating simple jobs so the
88
- # user doesn't have to learn about handling xml.
89
- #
90
- # @param params [Hash] parameters to create a freestyle project
91
- #
92
- # @option params [String] :name
93
- # the name of the job
94
- # @option params [Boolean] :keep_dependencies (false)
95
- # whether to keep the dependencies or not
96
- # @option params [Boolean] :block_build_when_downstream_building (false)
97
- # whether to block build when the downstream project is building
98
- # @option params [Boolean] :block_build_when_upstream_building (false)
99
- # whether to block build when the upstream project is building
100
- # @option params [Boolean] :concurrent_build (false)
101
- # whether to allow concurrent execution of builds
102
- # @option params [String] :scm_provider
103
- # the type of source control. Supported providers: git, svn, and cvs
104
- # @option params [String] :scm_url
105
- # the remote url for the selected scm provider
106
- # @option params [String] :scm_credentials_id
107
- # the id of the credentials to use for authenticating with scm. Only for "git"
108
- # @option params [String] :scm_git_tool
109
- # the git executable. Defaults to "Default"; only for "git"
110
- # @option params [String] :scm_module
111
- # the module to download. Only for use with "cvs" scm provider
112
- # @option params [String] :scm_branch (master)
113
- # the branch to use in scm.
114
- # @option params [String] :scm_tag
115
- # the tag to download from scm. Only for use with "cvs" scm provider
116
- # @option params [Boolean] :scm_use_head_if_tag_not_found
117
- # whether to use head if specified tag is not found. Only for "cvs"
118
- # @option params [String] :timer
119
- # the timer for running builds periodically
120
- # @option params [String] :shell_command
121
- # the command to execute in the shell
122
- # @option params [String] :notification_email
123
- # the email for sending notification
124
- # @option params [String] :skype_targets
125
- # the skype targets for sending notifications to. Use * to specify
126
- # group chats. Use space to separate multiple targets. Note that this
127
- # option requires the "skype" plugin to be installed in jenkins.
128
- # Example: testuser *testgroup
129
- # @option params [String] :skype_strategy (change)
130
- # the skype strategy to be used for sending notifications.
131
- # Valid values: all, failure, failure_and_fixed, change.
132
- # @option params [Boolean] :skype_notify_on_build_start (false)
133
- # whether to notify skype targets on build start
134
- # @option params [Boolean] :skype_notify_suspects (false)
135
- # whether to notify suspects on skype
136
- # @option params [Boolean] :skype_notify_culprits (false)
137
- # whether to notify culprits on skype
138
- # @option params [Boolean] :skype_notify_fixers (false)
139
- # whether to notify fixers on skype
140
- # @option params [Boolean] :skype_notify_upstream_committers (false)
141
- # whether to notify upstream committers on skype
142
- # @option params [String] :skype_message (summary_and_scm_changes)
143
- # the information to be sent as notification message. Valid:
144
- # just_summary, summary_and_scm_changes,
145
- # summary_and_build_parameters, summary_scm_changes_and_failed_tests.
146
- # @option params [String] :child_projects
147
- # the projects to add as downstream projects
148
- # @option params [String] :child_threshold (failure)
149
- # the threshold for child projects. Valid options: success, failure,
150
- # or unstable.
151
- #
152
- # @see #create_freestyle
153
- # @see #update_freestyle
154
- #
155
- # @return [String] the HTTP status code from the POST request
156
- #
157
- def create_or_update_freestyle(params)
158
- if exists?(params[:name])
159
- update_freestyle(params)
160
- else
161
- create_freestyle(params)
162
- end
163
- end
164
-
165
- # Create a freestyle project by accepting a Hash of parameters. For the
166
- # parameter description see #create_of_update_freestyle
167
- #
168
- # @param params [Hash] the parameters for creating a job
169
- #
170
- # @example Create a Freestype Project
171
- # create_freestyle(
172
- # :name => "test_freestyle_job",
173
- # :keep_dependencies => true,
174
- # :concurrent_build => true,
175
- # :scm_provider => "git",
176
- # :scm_url => "git://github.com./arangamani/jenkins_api_client.git",
177
- # :scm_branch => "master",
178
- # :shell_command => "bundle install\n rake func_tests"
179
- # )
180
- #
181
- # @see #create_or_update_freestyle
182
- # @see #create
183
- # @see #update_freestyle
184
- #
185
- # @return [String] the HTTP status code from the POST request
186
- #
187
- def create_freestyle(params)
188
- xml = build_freestyle_config(params)
189
- create(params[:name], xml)
190
- end
191
-
192
- # Update a job with params given as a hash instead of the xml. For the
193
- # parameter description see #create_or_update_freestyle
194
- #
195
- # @param params [Hash] parameters to update a freestyle project
196
- #
197
- # @see #create_or_update_freestyle
198
- # @see #update
199
- # @see #create_freestyle
200
- #
201
- # @return [String] the HTTP status code from the POST request
202
- #
203
- def update_freestyle(params)
204
- xml = build_freestyle_config(params)
205
- update(params[:name], xml)
206
- end
207
-
208
- # Builds the XML configuration based on the parameters passed as a Hash
209
- #
210
- # @param params [Hash] the parameters for building XML configuration
211
- #
212
- # @return [String] the generated XML configuration of the project
213
- #
214
- def build_freestyle_config(params)
215
- # Supported SCM providers
216
- supported_scm = ["git", "subversion", "cvs"]
217
-
218
- # Set default values for params that are not specified.
219
- raise ArgumentError, "Job name must be specified" \
220
- unless params.is_a?(Hash) && params[:name]
221
-
222
- [
223
- :keep_dependencies,
224
- :block_build_when_downstream_building,
225
- :block_build_when_upstream_building,
226
- :concurrent_build
227
- ].each do |param|
228
- params[param] = false if params[param].nil?
229
- end
230
-
231
- if params[:notification_email]
232
- if params[:notification_email_for_every_unstable].nil?
233
- params[:notification_email_for_every_unstable] = false
234
- end
235
- if params[:notification_email_send_to_individuals].nil?
236
- params[:notification_email_send_to_individuals] ||= false
237
- end
238
- end
239
-
240
- # SCM configurations and Error handling.
241
- unless params[:scm_provider].nil?
242
- unless supported_scm.include?(params[:scm_provider])
243
- raise "SCM #{params[:scm_provider]} is currently not supported"
244
- end
245
- raise "SCM URL must be specified" if params[:scm_url].nil?
246
- params[:scm_branch] = "master" if params[:scm_branch].nil?
247
- if params[:scm_use_head_if_tag_not_found].nil?
248
- params[:scm_use_head_if_tag_not_found] = false
249
- end
250
- end
251
-
252
- # Child projects configuration and Error handling
253
- if params[:child_threshold].nil? && !params[:child_projects].nil?
254
- params[:child_threshold] = "failure"
255
- end
256
-
257
- @logger.debug "Creating a freestyle job with params: #{params.inspect}"
258
-
259
- # Build the Job xml file based on the parameters given
260
- builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
261
- xml.project do
262
- xml.actions
263
- xml.description
264
- xml.keepDependencies "#{params[:keep_dependencies]}"
265
- xml.properties
266
- #buildlogs related stuff
267
- if params[:discard_old_builds]
268
- xml.logRotator(:class => 'hudson.tasks.LogRotator') do
269
- xml.daysToKeep params[:discard_old_builds][:daysToKeep] || -1
270
- xml.numToKeep params[:discard_old_builds][:numToKeep] || -1
271
- xml.artifactDaysToKeep params[:discard_old_builds][:artifactDaysToKeep] || -1
272
- xml.artifactNumToKeep params[:discard_old_builds][:artifactNumToKeep] || -1
273
- end
274
- end
275
-
276
- # SCM related stuff
277
- if params[:scm_provider] == 'subversion'
278
- # Build subversion related XML portion
279
- scm_subversion(params, xml)
280
- elsif params[:scm_provider] == "cvs"
281
- # Build CVS related XML portion
282
- scm_cvs(params, xml)
283
- elsif params[:scm_provider] == "git"
284
- # Build Git related XML portion
285
- scm_git(params, xml)
286
- else
287
- xml.scm(:class => "hudson.scm.NullSCM")
288
- end
289
- # Restrict job to run in a specified node
290
- if params[:restricted_node]
291
- xml.assignedNode "#{params[:restricted_node]}"
292
- xml.canRoam "false"
293
- else
294
- xml.canRoam "true"
295
- end
296
- xml.disabled "false"
297
- xml.blockBuildWhenDownstreamBuilding(
298
- "#{params[:block_build_when_downstream_building]}")
299
- xml.blockBuildWhenUpstreamBuilding(
300
- "#{params[:block_build_when_upstream_building]}")
301
- xml.triggers.vector do
302
- if params[:timer]
303
- xml.send("hudson.triggers.TimerTrigger") do
304
- xml.spec params[:timer]
305
- end
306
- end
307
-
308
- if params[:scm_trigger]
309
- xml.send("hudson.triggers.SCMTrigger") do
310
- xml.spec params[:scm_trigger]
311
- xml.ignorePostCommitHooks params.fetch(:ignore_post_commit_hooks) { false }
312
- end
313
- end
314
- end
315
- xml.concurrentBuild "#{params[:concurrent_build]}"
316
- # Shell command stuff
317
- xml.builders do
318
- if params[:shell_command]
319
- xml.send("hudson.tasks.Shell") do
320
- xml.command "#{params[:shell_command]}"
321
- end
322
- end
323
- end
324
- # Adding Downstream projects
325
- xml.publishers do
326
- # Build portion of XML that adds child projects
327
- child_projects(params, xml) if params[:child_projects]
328
- # Build portion of XML that adds email notification
329
- notification_email(params, xml) if params[:notification_email]
330
- # Build portion of XML that adds skype notification
331
- skype_notification(params, xml) if params[:skype_targets]
332
- artifact_archiver(params[:artifact_archiver], xml)
333
- end
334
- xml.buildWrappers
335
- end
336
- end
337
-
338
- xml_doc = Nokogiri::XML(builder.to_xml)
339
- plugin_collection.configure(xml_doc).to_xml
340
- end
341
-
342
- # Adding email notification to a job
343
- #
344
- # @param [Hash] params parameters to add email notification
345
- #
346
- # @option params [String] :name Name of the job
347
- # @option params [String] :notification_email Email address to send
348
- # @option params [Boolean] :notification_email_for_every_unstable
349
- # Send email notification email for every unstable build
350
- #
351
- def add_email_notification(params)
352
- raise "No job name specified" unless params[:name]
353
- raise "No email address specified" unless params[:notification_email]
354
- @logger.info "Adding '#{params[:notification_email]}' to be" +
355
- " notified for '#{params[:name]}'"
356
- xml = get_config(params[:name])
357
- n_xml = Nokogiri::XML(xml)
358
- if n_xml.xpath("//hudson.tasks.Mailer").empty?
359
- p_xml = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |b_xml|
360
- notification_email(params, b_xml)
361
- end
362
- email_xml = Nokogiri::XML(p_xml.to_xml).xpath(
363
- "//hudson.tasks.Mailer"
364
- ).first
365
- n_xml.xpath("//publishers").first.add_child(email_xml)
366
- post_config(params[:name], n_xml.to_xml)
367
- end
368
- end
369
-
370
- # Adding skype notificaiton to a job
371
- #
372
- # @param [Hash] params parameters for adding skype notification
373
- # * +:name+ name of the job to add skype notification
374
- # * +:skype_targets+ skype targets for sending notifications to. Use *
375
- # to specify group chats. Use space to separate multiple targets.
376
- # Example: testuser, *testgroup.
377
- # * +:skype_strategy+ skype strategy to be used for sending
378
- # notifications. Valid values: all, failure, failure_and_fixed,
379
- # change. Default: change.
380
- # * +:skype_notify_on_build_start+ Default: false
381
- # * +:skype_notify_suspects+ Default: false
382
- # * +:skype_notify_culprits+ Default: false
383
- # * +:skype_notify_fixers+ Default: false
384
- # * +:skype_notify_upstream_committers+ Default: false
385
- # * +:skype_message+ what should be sent as notification message. Valid:
386
- # just_summary, summary_and_scm_changes, summary_and_build_parameters,
387
- # summary_scm_changes_and_failed_tests.
388
- # Default: summary_and_scm_changes
389
- #
390
- def add_skype_notification(params)
391
- raise "No job name specified" unless params[:name]
392
- raise "No Skype target specified" unless params[:skype_targets]
393
- @logger.info "Adding Skype notification for '#{params[:name]}'"
394
- xml = get_config(params[:name])
395
- n_xml = Nokogiri::XML(xml)
396
- if n_xml.xpath("//hudson.plugins.skype.im.transport.SkypePublisher").empty?
397
- p_xml = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |b_xml|
398
- skype_notification(params, b_xml)
399
- end
400
- skype_xml = Nokogiri::XML(p_xml.to_xml).xpath(
401
- "//hudson.plugins.skype.im.transport.SkypePublisher"
402
- ).first
403
- n_xml.xpath("//publishers").first.add_child(skype_xml)
404
- post_config(params[:name], n_xml.to_xml)
405
- end
406
- end
407
-
408
- # Configure post-build step to archive artifacts
409
- #
410
- # @param artifact_params [Hash] parameters controlling how artifacts are archived
411
- #
412
- # @option artifact_params [String] :artifact_files
413
- # pattern or names of files to archive
414
- # @option artifact_params [String] :excludes
415
- # pattern or names of files to exclude
416
- # @option artifact_params [Boolean] :fingerprint (false)
417
- # fingerprint the archives
418
- # @option artifact_params [Boolean] :allow_empty_archive (false)
419
- # whether to allow empty archives
420
- # @option artifact_params [Boolean] :only_if_successful (false)
421
- # only archive if successful
422
- # @option artifact_params [Boolean] :default_excludes (false)
423
- # exclude defaults automatically
424
- #
425
- # @return [Nokogiri::XML::Builder]
426
- #
427
- def artifact_archiver(artifact_params, xml)
428
- return xml if artifact_params.nil?
429
-
430
- xml.send('hudson.tasks.ArtifactArchiver') do |x|
431
- x.artifacts artifact_params.fetch(:artifact_files) { '' }
432
- x.excludes artifact_params.fetch(:excludes) { '' }
433
- x.fingerprint artifact_params.fetch(:fingerprint) { false }
434
- x.allowEmptyArchive artifact_params.fetch(:allow_empty_archive) { false }
435
- x.onlyIfSuccessful artifact_params.fetch(:only_if_successful) { false }
436
- x.defaultExcludes artifact_params.fetch(:default_excludes) { false }
437
- end
438
-
439
- xml
440
- end
441
-
442
- # Rename a job given the old name and new name
443
- #
444
- # @param [String] old_job Name of the old job
445
- # @param [String] new_job Name of the new job.
446
- #
447
- def rename(old_job, new_job)
448
- @logger.info "Renaming job '#{old_job}' to '#{new_job}'"
449
- @client.api_post_request("/job/#{path_encode old_job}/doRename?newName=#{form_encode new_job}")
450
- end
451
-
452
- # Delete a job given the name
453
- #
454
- # @param job_name [String] the name of the job to delete
455
- #
456
- # @return [String] the response from the HTTP POST request
457
- #
458
- def delete(job_name)
459
- @logger.info "Deleting job '#{job_name}'"
460
- @client.api_post_request("/job/#{path_encode job_name}/doDelete")
461
- end
462
-
463
- # Deletes all jobs from Jenkins
464
- #
465
- # @note This method will remove all jobs from Jenkins. Please use with
466
- # caution.
467
- #
468
- def delete_all!
469
- @logger.info "Deleting all jobs from jenkins"
470
- list_all.each { |job| delete(job) }
471
- end
472
-
473
- # Wipe out the workspace for a job given the name
474
- #
475
- # @param job_name [String] the name of the job to wipe out the workspace
476
- #
477
- # @return [String] response from the HTTP POST request
478
- #
479
- def wipe_out_workspace(job_name)
480
- @logger.info "Wiping out the workspace of job '#{job_name}'"
481
- @client.api_post_request("/job/#{path_encode job_name}/doWipeOutWorkspace")
482
- end
483
-
484
- # Stops a running build of a job
485
- # This method will stop the current/most recent build if no build number
486
- # is specified. The build will be stopped only if it was
487
- # in 'running' state.
488
- #
489
- # @param job_name [String] the name of the job to stop the build
490
- # @param build_number [Number] the build number to stop
491
- #
492
- def stop_build(job_name, build_number = 0)
493
- build_number = get_current_build_number(job_name) if build_number == 0
494
- raise "No builds for #{job_name}" unless build_number
495
- @logger.info "Stopping job '#{job_name}' Build ##{build_number}"
496
- # Check and see if the build is running
497
- is_building = @client.api_get_request(
498
- "/job/#{path_encode job_name}/#{build_number}"
499
- )["building"]
500
- if is_building
501
- @client.api_post_request("/job/#{path_encode job_name}/#{build_number}/stop")
502
- end
503
- end
504
-
505
- alias_method :stop, :stop_build
506
- alias_method :abort, :stop_build
507
-
508
- # Re-create the same job
509
- # This is a hack to clear any existing builds
510
- #
511
- # @param job_name [String] the name of the job to recreate
512
- #
513
- # @return [String] the response from the HTTP POST request
514
- #
515
- def recreate(job_name)
516
- @logger.info "Recreating job '#{job_name}'"
517
- job_xml = get_config(job_name)
518
- delete(job_name)
519
- create(job_name, job_xml)
520
- end
521
-
522
- # Copy a job
523
- #
524
- # @param from_job_name [String] the name of the job to copy from
525
- # @param to_job_name [String] the name of the job to copy to
526
- #
527
- # @return [String] the response from the HTTP POST request
528
- #
529
- def copy(from_job_name, to_job_name = nil)
530
- to_job_name = "copy_of_#{from_job_name}" if to_job_name.nil?
531
- @logger.info "Copying job '#{from_job_name}' to '#{to_job_name}'"
532
- @client.api_post_request(
533
- "/createItem?name=#{path_encode to_job_name}&mode=copy&from=#{path_encode from_job_name}"
534
- )
535
- end
536
-
537
- # Get progressive console output from Jenkins server for a job
538
- #
539
- # @param [String] job_name Name of the Jenkins job
540
- # @param [Number] build_num Specific build number to obtain the
541
- # console output from. Default is the recent build
542
- # @param [Number] start start offset to get only a portion of the text
543
- # @param [String] mode Mode of text output. 'text' or 'html'
544
- #
545
- # @return [Hash] response
546
- # * +output+ console output of the job
547
- # * +size+ size of the text. This can be used as 'start' for the
548
- # next call to get progressive output
549
- # * +more+ more data available for the job. 'true' if available
550
- # and nil otherwise
551
- #
552
- def get_console_output(job_name, build_num = 0, start = 0, mode = 'text')
553
- build_num = get_current_build_number(job_name) if build_num == 0
554
- if build_num == 0
555
- puts "No builds for this job '#{job_name}' yet."
556
- return nil
557
- end
558
- if mode == 'text'
559
- mode = 'Text'
560
- elsif mode == 'html'
561
- mode = 'Html'
562
- else
563
- raise "Mode should either be 'text' or 'html'. You gave: #{mode}"
564
- end
565
- get_msg = "/job/#{path_encode job_name}/#{build_num}/logText/progressive#{mode}?"
566
- get_msg << "start=#{start}"
567
- raw_response = true
568
- api_response = @client.api_get_request(get_msg, nil, nil, raw_response)
569
- #puts "Response: #{api_response.header['x-more-data']}"
570
- response = {}
571
- response['output'] = api_response.body
572
- response['size'] = api_response.header['x-text-size']
573
- response['more'] = api_response.header['x-more-data']
574
-
575
- response
576
- end
577
-
578
- # List all jobs on the Jenkins CI server
579
- #
580
- # @return [Array<String>] the names of all jobs in jenkins
581
- #
582
- def list_all
583
- response_json = @client.api_get_request("", "tree=jobs[name]")["jobs"]
584
- response_json.map { |job| job["name"] }.sort
585
- end
586
-
587
- # Checks if the given job exists in Jenkins
588
- #
589
- # @param job_name [String] the name of the job to check
590
- #
591
- # @return [Boolean] whether the job exists in jenkins or not
592
- #
593
- def exists?(job_name)
594
- list(job_name).include?(job_name)
595
- end
596
-
597
- # List all Jobs matching the given status
598
- # You can optionally pass in jobs list to filter the status from
599
- #
600
- # @param status [String] the job status to filter
601
- # @param jobs [Array<String>] if specified this array will be used for
602
- # filtering by the status otherwise the filtering will be done using
603
- # all jobs available in jenkins
604
- #
605
- # @return [Array<String>] filtered jobs
606
- #
607
- def list_by_status(status, jobs = [])
608
- jobs = list_all if jobs.empty?
609
- @logger.info "Obtaining jobs matching status '#{status}'"
610
- json_response = @client.api_get_request("", "tree=jobs[name,color]")
611
- filtered_jobs = []
612
- json_response["jobs"].each do |job|
613
- if color_to_status(job["color"]) == status &&
614
- jobs.include?(job["name"])
615
- filtered_jobs << job["name"]
616
- end
617
- end
618
- filtered_jobs
619
- end
620
-
621
- # List all jobs that match the given regex
622
- #
623
- # @param filter [String] a regular expression or a string to filter jobs
624
- # @param ignorecase [Boolean] whether to ignore case or not
625
- #
626
- # @return [Array<String>] jobs matching the given pattern
627
- #
628
- def list(filter, ignorecase = true)
629
- @logger.info "Obtaining jobs matching filter '#{filter}'"
630
- response_json = @client.api_get_request("")
631
- jobs = []
632
- response_json["jobs"].each do |job|
633
- if ignorecase
634
- jobs << job["name"] if job["name"] =~ /#{filter}/i
635
- else
636
- jobs << job["name"] if job["name"] =~ /#{filter}/
637
- end
638
- end
639
- jobs
640
- end
641
-
642
- # List all jobs on the Jenkins CI server along with their details
643
- #
644
- # @return [Array<Hash>] the details of all jobs in jenkins
645
- #
646
- def list_all_with_details
647
- @logger.info "Obtaining the details of all jobs"
648
- response_json = @client.api_get_request("")
649
- response_json["jobs"]
650
- end
651
-
652
- # List details of a specific job
653
- #
654
- # @param job_name [String] the name of the job to obtain the details from
655
- #
656
- # @return [Hash] the details of the specified job
657
- #
658
- def list_details(job_name)
659
- @logger.info "Obtaining the details of '#{job_name}'"
660
- @client.api_get_request("/job/#{path_encode job_name}")
661
- end
662
-
663
- # List upstream projects of a specific job
664
- #
665
- # @param job_name [String] the name of the job to obtain upstream
666
- # projects for
667
- #
668
- def get_upstream_projects(job_name)
669
- @logger.info "Obtaining the upstream projects of '#{job_name}'"
670
- response_json = @client.api_get_request("/job/#{path_encode job_name}")
671
- response_json["upstreamProjects"]
672
- end
673
-
674
- # List downstream projects of a specific job
675
- #
676
- # @param job_name [String] the name of the job to obtain downstream
677
- # projects for
678
- #
679
- def get_downstream_projects(job_name)
680
- @logger.info "Obtaining the down stream projects of '#{job_name}'"
681
- response_json = @client.api_get_request("/job/#{path_encode job_name}")
682
- response_json["downstreamProjects"]
683
- end
684
-
685
- # Obtain build details of a specific job
686
- #
687
- # @param [String] job_name
688
- #
689
- def get_builds(job_name, options = {})
690
- @logger.info "Obtaining the build details of '#{job_name}'"
691
- url = "/job/#{path_encode job_name}"
692
-
693
- tree = options[:tree] || nil
694
- response_json = @client.api_get_request url, tree_string(tree)
695
- response_json["builds"]
696
- end
697
-
698
- # This method maps the color to status of a job
699
- #
700
- # @param [String] color color given by the API for a job
701
- #
702
- # @return [String] status status of the given job matching the color
703
- #
704
- def color_to_status(color)
705
- case color
706
- when "blue"
707
- "success"
708
- when "red"
709
- "failure"
710
- when "yellow"
711
- "unstable"
712
- when /anime/
713
- "running"
714
- # In the recent version of Jenkins (> 1.517), jobs that are not built
715
- # yet have a color of "notbuilt" instead of "grey". Include that to the
716
- # not_run condition so it is backward compatible.
717
- when "grey", "notbuilt"
718
- "not_run"
719
- when "aborted"
720
- "aborted"
721
- when "disabled"
722
- "disabled"
723
- else
724
- "invalid"
725
- end
726
- end
727
-
728
- # Determine if the build is queued
729
- #
730
- # @param [String] job_name
731
- #
732
- # @return [Integer] build number if queued, or [Boolean] false if not queued
733
- #
734
- def queued?(job_name)
735
- queue_result = @client.api_get_request("/job/#{path_encode job_name}")['inQueue']
736
- if queue_result
737
- return @client.api_get_request("/job/#{path_encode job_name}")['nextBuildNumber']
738
- else
739
- return queue_result
740
- end
741
- end
742
-
743
- # Obtain the current build status of the job
744
- # By default Jenkins returns the color of the job status icon
745
- # This function translates the color into a meaningful status
746
- #
747
- # @param [String] job_name
748
- #
749
- # @return [String] status current status of the given job
750
- #
751
- def get_current_build_status(job_name)
752
- @logger.info "Obtaining the current build status of '#{job_name}'"
753
- response_json = @client.api_get_request("/job/#{path_encode job_name}")
754
- color_to_status(response_json["color"])
755
- end
756
-
757
- alias_method :status, :get_current_build_status
758
-
759
- # Obtain the current build number of the given job
760
- # This function returns nil if there were no builds for the given job.
761
- #
762
- # @param [String] job_name
763
- #
764
- # @return [Integer] current build number of the given job
765
- #
766
- def get_current_build_number(job_name)
767
- @logger.info "Obtaining the current build number of '#{job_name}'"
768
- @client.api_get_request("/job/#{path_encode job_name}")['nextBuildNumber'].to_i - 1
769
- end
770
-
771
- alias_method :build_number, :get_current_build_number
772
-
773
- # Build a Jenkins job, optionally waiting for build to start and
774
- # returning the build number.
775
- # Adds support for new/old Jenkins servers where build_queue id may
776
- # not be available. Also adds support for periodic callbacks, and
777
- # optional cancellation of queued_job if not started within allowable
778
- # time window (if build_queue option available)
779
- #
780
- # Notes:
781
- # 'opts' may be a 'true' or 'false' value to maintain
782
- # compatibility with old method signature, where true indicates
783
- # 'return_build_number'. In this case, true is translated to:
784
- # { 'build_start_timeout' => @client_timeout }
785
- # which simulates earlier behavior.
786
- #
787
- # progress_proc
788
- # Optional proc that is called periodically while waiting for
789
- # build to start.
790
- # Initial call (with poll_count == 0) indicates build has been
791
- # requested, and that polling is starting.
792
- # Final call will indicate one of build_started or cancelled.
793
- # params:
794
- # max_wait [Integer] Same as opts['build_start_timeout']
795
- # current_wait [Integer]
796
- # poll_count [Integer] How many times has queue been polled
797
- #
798
- # completion_proc
799
- # Optional proc that is called <just before> the 'build' method
800
- # exits.
801
- # params:
802
- # build_number [Integer] Present if build started or nil
803
- # build_cancelled [Boolean] True if build timed out and was
804
- # successfully removed from build-queue
805
- #
806
- # @param [String] job_name the name of the job
807
- # @param [Hash] params the parameters for parameterized build
808
- # @param [Hash] opts options for this method
809
- # * +build_start_timeout+ [Integer] How long to wait for queued
810
- # build to start before giving up. Default: 0/nil
811
- # * +cancel_on_build_start_timeout+ [Boolean] Should an attempt be
812
- # made to cancel the queued build if it hasn't started within
813
- # 'build_start_timeout' seconds? This only works on newer versions
814
- # of Jenkins where JobQueue is exposed in build post response.
815
- # Default: false
816
- # * +poll_interval+ [Integer] How often should we check with CI
817
- # Server while waiting for start. Default: 2 (seconds)
818
- # * +progress_proc+ [Proc] A proc that will receive progress notitications. Default: nil
819
- # * +completion_proc+ [Proc] A proc that is called <just before>
820
- # this method (build) exits. Default: nil
821
- #
822
- # @return [Integer] build number, or nil if not started (IF TIMEOUT SPECIFIED)
823
- # @return [String] HTTP response code (per prev. behavior) (NO TIMEOUT SPECIFIED)
824
- #
825
- def build(job_name, params = {}, opts = {})
826
- if opts.nil? || opts.is_a?(FalseClass)
827
- opts = {}
828
- elsif opts.is_a?(TrueClass)
829
- opts = { 'build_start_timeout' => @client_timeout }
830
- end
831
-
832
- opts['job_name'] = job_name
833
-
834
- msg = "Building job '#{job_name}'"
835
- msg << " with parameters: #{params.inspect}" unless params.empty?
836
- @logger.info msg
837
-
838
- if (opts['build_start_timeout'] || 0) > 0
839
- # Best-guess build-id
840
- # This is only used if we go the old-way below... but we can use this number to detect if multiple
841
- # builds were queued
842
- current_build_id = get_current_build_number(job_name)
843
- expected_build_id = current_build_id > 0 ? current_build_id + 1 : 1
844
- end
845
-
846
- if (params.nil? or params.empty?)
847
- response = @client.api_post_request("/job/#{path_encode job_name}/build",
848
- {},
849
- true)
850
- else
851
- response = @client.api_post_request("/job/#{path_encode job_name}/buildWithParameters",
852
- params,
853
- true)
854
- end
855
-
856
- if (opts['build_start_timeout'] || 0) > 0
857
- if @client.compare_versions(@client.get_jenkins_version, JENKINS_QUEUE_ID_SUPPORT_VERSION) >= 0
858
- return get_build_id_from_queue(response, expected_build_id, opts)
859
- else
860
- return get_build_id_the_old_way(expected_build_id, opts)
861
- end
862
- else
863
- return response.code
864
- end
865
- end
866
-
867
- def get_build_id_from_queue(response, expected_build_id, opts)
868
- # If we get this far the API hasn't detected an error response (it would raise Exception)
869
- # So no need to check response code
870
- # Obtain the queue ID from the location
871
- # header and wait till the build is moved to one of the executors and a
872
- # build number is assigned
873
- build_start_timeout = opts['build_start_timeout']
874
- poll_interval = opts['poll_interval'] || 2
875
- poll_interval = 1 if poll_interval < 1
876
- progress_proc = opts['progress_proc']
877
- completion_proc = opts['completion_proc']
878
- job_name = opts['job_name']
879
-
880
- if response["location"]
881
- task_id_match = response["location"].match(/\/item\/(\d*)\//)
882
- task_id = task_id_match.nil? ? nil : task_id_match[1]
883
- unless task_id.nil?
884
- @logger.info "Job queued for #{job_name}, will wait up to #{build_start_timeout} seconds for build to start..."
885
-
886
- # Let progress proc know we've queued the build
887
- progress_proc.call(build_start_timeout, 0, 0) if progress_proc
888
-
889
- # Wait for the build to start
890
- begin
891
- start = Time.now.to_i
892
- Timeout::timeout(build_start_timeout) do
893
- started = false
894
- attempts = 0
895
-
896
- while !started
897
- # Don't really care about the response... if we get thru here, then it must have worked.
898
- # Jenkins will return 404's until the job starts
899
- queue_item = @client.queue.get_item_by_id(task_id)
900
-
901
- if queue_item['executable'].nil?
902
- # Job not started yet
903
- attempts += 1
904
-
905
- progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
906
- # Every 5 attempts (~10 seconds)
907
- @logger.info "Still waiting..." if attempts % 5 == 0
908
-
909
- sleep poll_interval
910
- else
911
- build_number = queue_item['executable']['number']
912
- completion_proc.call(build_number, false) if completion_proc
913
-
914
- return build_number
915
- end
916
- end
917
- end
918
- rescue Timeout::Error
919
- # Well, we waited - and the job never started building
920
- # Attempt to kill off queued job (if flag set)
921
- if opts['cancel_on_build_start_timeout']
922
- @logger.info "Job for '#{job_name}' did not start in a timely manner, attempting to cancel pending build..."
923
-
924
- begin
925
- @client.api_post_request("/queue/cancelItem?id=#{task_id}")
926
- @logger.info "Job cancelled"
927
- completion_proc.call(nil, true) if completion_proc
928
- rescue JenkinsApi::Exceptions::ApiException => e
929
- completion_proc.call(nil, false) if completion_proc
930
- @logger.warn "Error while attempting to cancel pending job for '#{job_name}'. #{e.class} #{e}"
931
- raise
932
- end
933
- else
934
- @logger.info "Jenkins build for '#{job_name}' failed to start in a timely manner"
935
- completion_proc.call(nil, false) if completion_proc
936
- end
937
-
938
- # Old version used to throw timeout error, so we should let that go thru now
939
- raise
940
- rescue JenkinsApi::Exceptions::ApiException => e
941
- # Jenkins Api threw an error at us
942
- completion_proc.call(nil, false) if completion_proc
943
- @logger.warn "Problem while waiting for '#{job_name}' build to start. #{e.class} #{e}"
944
- raise
945
- end
946
- else
947
- @logger.warn "Jenkins did not return a queue_id for '#{job_name}' build (location: #{response['location']})"
948
- return get_build_id_the_old_way(expected_build_id, opts)
949
- end
950
- else
951
- @logger.warn "Jenkins did not return a location header for '#{job_name}' build"
952
- return get_build_id_the_old_way(expected_build_id, opts)
953
- end
954
- end
955
-
956
- private :get_build_id_from_queue
957
-
958
- def get_build_id_the_old_way(expected_build_id, opts)
959
- # Try to wait until the build starts so we can mimic queue
960
- # Wait for the build to start
961
- build_start_timeout = opts['build_start_timeout']
962
- poll_interval = opts['poll_interval'] || 2
963
- poll_interval = 1 if poll_interval < 1
964
- progress_proc = opts['progress_proc']
965
- completion_proc = opts['completion_proc']
966
- job_name = opts['job_name']
967
-
968
- @logger.info "Build requested for '#{job_name}', will wait up to #{build_start_timeout} seconds for build to start..."
969
-
970
- # Let progress proc know we've queued the build
971
- progress_proc.call(build_start_timeout, 0, 0) if progress_proc
972
-
973
- begin
974
- start = Time.now.to_i
975
- Timeout::timeout(build_start_timeout) do
976
- attempts = 0
977
-
978
- while true
979
- attempts += 1
980
-
981
- # Don't really care about the response... if we get thru here, then it must have worked.
982
- # Jenkins will return 404's until the job starts
983
- begin
984
- get_build_details(job_name, expected_build_id)
985
- completion_proc.call(expected_build_id, false) if completion_proc
986
-
987
- return expected_build_id
988
- rescue JenkinsApi::Exceptions::NotFound => e
989
- progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
990
-
991
- # Every 5 attempts (~10 seconds)
992
- @logger.info "Still waiting..." if attempts % 5 == 0
993
-
994
- sleep poll_interval
995
- end
996
- end
997
- end
998
- rescue Timeout::Error
999
- # Well, we waited - and the job never started building
1000
- # Now we need to raise an exception so that the build can be officially failed
1001
- completion_proc.call(nil, false) if completion_proc
1002
- @logger.info "Jenkins '#{job_name}' build failed to start in a timely manner"
1003
-
1004
- # Old version used to propagate timeout error
1005
- raise
1006
- rescue JenkinsApi::Exceptions::ApiException => e
1007
- completion_proc.call(nil, false) if completion_proc
1008
- # Jenkins Api threw an error at us
1009
- @logger.warn "Problem while waiting for '#{job_name}' build ##{expected_build_id} to start. #{e.class} #{e}"
1010
- raise
1011
- end
1012
- end
1013
-
1014
- private :get_build_id_the_old_way
1015
-
1016
- # Programatically schedule SCM polling for the specified job
1017
- #
1018
- # @param job_name [String] the name of the job
1019
- #
1020
- # @return [String] the response code from the HTTP post request
1021
- #
1022
- def poll(job_name)
1023
- @logger.info "Polling SCM changes for job '#{job_name}'"
1024
- @client.api_post_request("/job/#{job_name}/polling")
1025
- end
1026
-
1027
- # Enable a job given the name of the job
1028
- #
1029
- # @param [String] job_name
1030
- #
1031
- def enable(job_name)
1032
- @logger.info "Enabling job '#{job_name}'"
1033
- @client.api_post_request("/job/#{path_encode job_name}/enable")
1034
- end
1035
-
1036
- # Disable a job given the name of the job
1037
- #
1038
- # @param [String] job_name
1039
- #
1040
- def disable(job_name)
1041
- @logger.info "Disabling job '#{job_name}'"
1042
- @client.api_post_request("/job/#{path_encode job_name}/disable")
1043
- end
1044
-
1045
- # Obtain the configuration stored in config.xml of a specific job
1046
- #
1047
- # @param [String] job_name
1048
- #
1049
- # @return [String] XML Config.xml of the job
1050
- #
1051
- def get_config(job_name)
1052
- @logger.info "Obtaining the config.xml of '#{job_name}'"
1053
- @client.get_config("/job/#{path_encode job_name}")
1054
- end
1055
-
1056
- # Post the configuration of a job given the job name and the config.xml
1057
- #
1058
- # @param [String] job_name
1059
- # @param [String] xml
1060
- #
1061
- # @return [String] response_code return code from HTTP POST
1062
- #
1063
- def post_config(job_name, xml)
1064
- @logger.info "Posting the config.xml of '#{job_name}'"
1065
- @client.post_config("/job/#{path_encode job_name}/config.xml", xml)
1066
- end
1067
-
1068
- # Obtain the test results for a specific build of a job
1069
- #
1070
- # @param [String] job_name
1071
- # @param [Number] build_num
1072
- #
1073
- def get_test_results(job_name, build_num)
1074
- build_num = get_current_build_number(job_name) if build_num == 0
1075
- @logger.info "Obtaining the test results of '#{job_name}'" +
1076
- " Build ##{build_num}"
1077
- @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/testReport")
1078
- rescue Exceptions::NotFound
1079
- # Not found is acceptable, as not all builds will have test results
1080
- # and this is what jenkins throws at us in that case
1081
- nil
1082
- end
1083
-
1084
- # Obtain the plugin results for a specific build of a job
1085
- #
1086
- # @param [String] job_name
1087
- # @param [Number] build_num
1088
- # @param [String] plugin_name
1089
- #
1090
- def get_plugin_results(job_name, build_num, plugin_name)
1091
- build_num = get_current_build_number(job_name) if build_num == 0
1092
- @logger.info "Obtaining the '#{plugin_name}' plugin results of '#{job_name}'" +
1093
- " Build ##{build_num}"
1094
- @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/#{plugin_name}Result")
1095
- rescue Exceptions::NotFound
1096
- # Not found is acceptable, as not all builds will have plugin results
1097
- # and this is what jenkins throws at us in that case
1098
- nil
1099
- end
1100
-
1101
- # Obtain detailed build info for a job
1102
- #
1103
- # @param [String] job_name
1104
- # @param [Number] build_num
1105
- #
1106
- def get_build_details(job_name, build_num)
1107
- build_num = get_current_build_number(job_name) if build_num == 0
1108
- @logger.info "Obtaining the build details of '#{job_name}'" +
1109
- " Build ##{build_num}"
1110
-
1111
- @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/")
1112
- end
1113
-
1114
- # Change the description of a specific job
1115
- #
1116
- # @param [String] job_name
1117
- # @param [String] description
1118
- #
1119
- # @return [String] response_code return code from HTTP POST
1120
- #
1121
- def change_description(job_name, description)
1122
- @logger.info "Changing the description of '#{job_name}' to '#{description}'"
1123
- xml = get_config(job_name)
1124
- n_xml = Nokogiri::XML(xml)
1125
- desc = n_xml.xpath("//description").first
1126
- desc.content = "#{description}"
1127
- xml_modified = n_xml.to_xml
1128
- post_config(job_name, xml_modified)
1129
- end
1130
-
1131
- # Block the build of the job when downstream is building
1132
- #
1133
- # @param [String] job_name
1134
- #
1135
- # @return [String] response_code return code from HTTP POST
1136
- #
1137
- def block_build_when_downstream_building(job_name)
1138
- @logger.info "Blocking builds of '#{job_name}' when downstream" +
1139
- " projects are building"
1140
- xml = get_config(job_name)
1141
- n_xml = Nokogiri::XML(xml)
1142
- node = n_xml.xpath("//blockBuildWhenDownstreamBuilding").first
1143
- if node.content == "false"
1144
- node.content = "true"
1145
- xml_modified = n_xml.to_xml
1146
- post_config(job_name, xml_modified)
1147
- end
1148
- end
1149
-
1150
- # Unblock the build of the job when downstream is building
1151
- #
1152
- # @param [String] job_name
1153
- #
1154
- # @return [String] response_code return code from HTTP POST
1155
- #
1156
- def unblock_build_when_downstream_building(job_name)
1157
- @logger.info "Unblocking builds of '#{job_name}' when downstream" +
1158
- " projects are building"
1159
- xml = get_config(job_name)
1160
- n_xml = Nokogiri::XML(xml)
1161
- node = n_xml.xpath("//blockBuildWhenDownstreamBuilding").first
1162
- if node.content == "true"
1163
- node.content = "false"
1164
- xml_modified = n_xml.to_xml
1165
- post_config(job_name, xml_modified)
1166
- end
1167
- end
1168
-
1169
- # Block the build of the job when upstream is building
1170
- #
1171
- # @param [String] job_name
1172
- #
1173
- # @return [String] response_code return code from HTTP POST
1174
- #
1175
- def block_build_when_upstream_building(job_name)
1176
- @logger.info "Blocking builds of '#{job_name}' when upstream" +
1177
- " projects are building"
1178
- xml = get_config(job_name)
1179
- n_xml = Nokogiri::XML(xml)
1180
- node = n_xml.xpath("//blockBuildWhenUpstreamBuilding").first
1181
- if node.content == "false"
1182
- node.content = "true"
1183
- xml_modified = n_xml.to_xml
1184
- post_config(job_name, xml_modified)
1185
- end
1186
- end
1187
-
1188
- # Unblock the build of the job when upstream is building
1189
- #
1190
- # @param [String] job_name
1191
- #
1192
- # @return [String] response_code return code from HTTP POST
1193
- #
1194
- def unblock_build_when_upstream_building(job_name)
1195
- @logger.info "Unblocking builds of '#{job_name}' when upstream" +
1196
- " projects are building"
1197
- xml = get_config(job_name)
1198
- n_xml = Nokogiri::XML(xml)
1199
- node = n_xml.xpath("//blockBuildWhenUpstreamBuilding").first
1200
- if node.content == "true"
1201
- node.content = "false"
1202
- xml_modified = n_xml.to_xml
1203
- post_config(job_name, xml_modified)
1204
- end
1205
- end
1206
-
1207
- # Allow or disable concurrent build execution
1208
- #
1209
- # @param [String] job_name
1210
- # @param [Bool] option true or false
1211
- #
1212
- # @return [String] response_code return code from HTTP POST
1213
- #
1214
- def execute_concurrent_builds(job_name, option)
1215
- @logger.info "Setting the concurrent build execution option of" +
1216
- " '#{job_name}' to #{option}"
1217
- xml = get_config(job_name)
1218
- n_xml = Nokogiri::XML(xml)
1219
- node = n_xml.xpath("//concurrentBuild").first
1220
- if node.content != "#{option}"
1221
- node.content = option == true ? "true" : "false"
1222
- xml_modified = n_xml.to_xml
1223
- post_config(job_name, xml_modified)
1224
- end
1225
- end
1226
-
1227
- # Obtain the build parameters of a job. It returns an array of hashes with
1228
- # details of job params.
1229
- #
1230
- # @param [String] job_name
1231
- #
1232
- # @return [Array] params_array Array of parameters for the given job
1233
- #
1234
- def get_build_params(job_name)
1235
- @logger.info "Obtaining the build params of '#{job_name}'"
1236
- xml = get_config(job_name)
1237
- n_xml = Nokogiri::XML(xml)
1238
- params = n_xml.xpath("//parameterDefinitions").first
1239
- params_array = []
1240
- if params
1241
- params.children.each do |param|
1242
- param_hash = {}
1243
- case param.name
1244
- when "hudson.model.StringParameterDefinition",
1245
- "hudson.model.BooleanParameterDefinition",
1246
- "hudson.model.TextParameterDefinition",
1247
- "hudson.model.PasswordParameterDefinition"
1248
- param_hash[:type] = 'string' if param.name =~ /string/i
1249
- param_hash[:type] = 'boolean' if param.name =~ /boolean/i
1250
- param_hash[:type] = 'text' if param.name =~ /text/i
1251
- param_hash[:type] = 'password' if param.name =~ /password/i
1252
- param.children.each do |value|
1253
- param_hash[:name] = value.content if value.name == "name"
1254
- if value.name == "description"
1255
- param_hash[:description] = value.content
1256
- end
1257
- if value.name == "defaultValue"
1258
- param_hash[:default] = value.content
1259
- end
1260
- end
1261
- when "hudson.model.RunParameterDefinition"
1262
- param_hash[:type] = 'run'
1263
- param.children.each do |value|
1264
- param_hash[:name] = value.content if value.name == "name"
1265
- if value.name == "description"
1266
- param_hash[:description] = value.content
1267
- end
1268
- if value.name == "projectName"
1269
- param_hash[:project] = value.content
1270
- end
1271
- end
1272
- when "hudson.model.FileParameterDefinition"
1273
- param_hash[:type] = 'file'
1274
- param.children.each do |value|
1275
- param_hash[:name] = value.content if value.name == "name"
1276
- if value.name == "description"
1277
- param_hash[:description] = value.content
1278
- end
1279
- end
1280
- when "hudson.scm.listtagsparameter.ListSubversionTagsParameterDefinition"
1281
- param_hash[:type] = 'list_tags'
1282
- param.children.each do |value|
1283
- if value.name == "name"
1284
- param_hash[:name] = value.content
1285
- end
1286
- if value.name == "description"
1287
- param_hash[:description] = value.content
1288
- end
1289
- if value.name == "tagsDir"
1290
- param_hash[:tags_dir] = value.content
1291
- end
1292
- if value.name == "tagsFilter"
1293
- param_hash[:tags_filter] = value.content
1294
- end
1295
- if value.name == "reverseByDate"
1296
- param_hash[:reverse_by_date] = value.content
1297
- end
1298
- if value.name == "reverseByName"
1299
- param_hash[:reverse_by_name] = value.content
1300
- end
1301
- if value.name == "defaultValue"
1302
- param_hash[:default] = value.content
1303
- end
1304
- param_hash[:max_tags] = value.content if value.name == "maxTags"
1305
- param_hash[:uuid] = value.content if value.name == "uuid"
1306
- end
1307
- when "hudson.model.ChoiceParameterDefinition"
1308
- param_hash[:type] = 'choice'
1309
- param.children.each do |value|
1310
- param_hash[:name] = value.content if value.name == "name"
1311
- param_hash[:description] = value.content \
1312
- if value.name == "description"
1313
- choices = []
1314
- if value.name == "choices"
1315
- value.children.each do |value_child|
1316
- if value_child.name == "a"
1317
- value_child.children.each do |choice_child|
1318
- choices << choice_child.content.strip \
1319
- unless choice_child.content.strip.empty?
1320
- end
1321
- end
1322
- end
1323
- end
1324
- param_hash[:choices] = choices unless choices.empty?
1325
- end
1326
- end
1327
- params_array << param_hash unless param_hash.empty?
1328
- end
1329
- end
1330
- params_array
1331
- end
1332
-
1333
- # Add downstream projects to a specific job given the job name,
1334
- # projects to be added as downstream projects, and the threshold
1335
- #
1336
- # @param [String] job_name
1337
- # @param [String] downstream_projects
1338
- # @param [String] threshold - failure, success, or unstable
1339
- # @param [Boolean] overwrite - true or false
1340
- #
1341
- # @return [String] response_code return code from HTTP POST
1342
- #
1343
- def add_downstream_projects(job_name,
1344
- downstream_projects,
1345
- threshold, overwrite = false)
1346
- @logger.info "Adding #{downstream_projects.inspect} as downstream" +
1347
- " projects for '#{job_name}' with the threshold of '#{threshold}'" +
1348
- " and overwrite option of '#{overwrite}'"
1349
- name, ord, col = get_threshold_params(threshold)
1350
- xml = get_config(job_name)
1351
- n_xml = Nokogiri::XML(xml)
1352
- child_projects_node = n_xml.xpath("//childProjects").first
1353
- if child_projects_node
1354
- if overwrite
1355
- child_projects_node.content = "#{downstream_projects}"
1356
- else
1357
- to_replace = child_projects_node.content +
1358
- ", #{downstream_projects}"
1359
- child_projects_node.content = to_replace
1360
- end
1361
- else
1362
- publisher_node = n_xml.xpath("//publishers").first
1363
- build_trigger_node = publisher_node.add_child(
1364
- "<hudson.tasks.BuildTrigger/>"
1365
- )
1366
- child_project_node = build_trigger_node.first.add_child(
1367
- "<childProjects>#{downstream_projects}</childProjects>"
1368
- )
1369
- threshold_node = child_project_node.first.add_next_sibling(
1370
- "<threshold/>"
1371
- )
1372
- threshold_node.first.add_child(
1373
- "<name>#{name}</name><ordinal>#{ord}</ordinal><color>#{col}</color>"
1374
- )
1375
- end
1376
- xml_modified = n_xml.to_xml
1377
- post_config(job_name, xml_modified)
1378
- end
1379
-
1380
- # Remove all downstream projects of a specific job
1381
- #
1382
- # @param [String] job_name
1383
- #
1384
- # @return [String] response_code return code from HTTP POST
1385
- #
1386
- def remove_downstream_projects(job_name)
1387
- @logger.info "Removing the downstream projects of '#{job_name}'"
1388
- xml = get_config(job_name)
1389
- n_xml = Nokogiri::XML(xml)
1390
- n_xml.search("//hudson.tasks.BuildTrigger").each do |node|
1391
- child_project_trigger = false
1392
- node.search("//childProjects").each do |child_node|
1393
- child_project_trigger = true
1394
- child_node.search("//threshold").each do |threshold_node|
1395
- threshold_node.children.each do |threshold_value_node|
1396
- threshold_value_node.content = nil
1397
- threshold_value_node.remove
1398
- end
1399
- threshold_node.content = nil
1400
- threshold_node.remove
1401
- end
1402
- child_node.content = nil
1403
- child_node.remove
1404
- end
1405
- node.content = nil
1406
- node.remove
1407
- end
1408
- publisher_node = n_xml.search("//publishers").first
1409
- publisher_node.content = nil if publisher_node.children.empty?
1410
- xml_modified = n_xml.to_xml
1411
- post_config(job_name, xml_modified)
1412
- end
1413
-
1414
- # Add upstream projects to a specific job given the job name,
1415
- # projects to be added as upstream projects, and the threshold
1416
- #
1417
- # @param [String] job_name
1418
- # @param [String] upstream_projects - separated with comma
1419
- # @param [String] threshold - failure, success, or unstable
1420
- # @param [Boolean] overwrite - true or false
1421
- #
1422
- # @return [String] response_code return code from HTTP POST
1423
- #
1424
- def add_upstream_projects(job_name,
1425
- upstream_projects,
1426
- threshold, overwrite = false)
1427
- @logger.info "Adding #{upstream_projects.inspect} as upstream" +
1428
- " projects for '#{job_name}' with the threshold of '#{threshold}'" +
1429
- " and overwrite option of '#{overwrite}'"
1430
- name, ord, col = get_threshold_params(threshold)
1431
- xml = get_config(job_name)
1432
- n_xml = Nokogiri::XML(xml)
1433
- upstream_projects_node = n_xml.xpath("//upstreamProjects").first
1434
- if upstream_projects_node
1435
- if overwrite
1436
- upstream_projects_node.content = "#{upstream_projects}"
1437
- else
1438
- to_replace = upstream_projects_node.content +
1439
- ", #{upstream_projects}"
1440
- upstream_projects_node.content = to_replace
1441
- end
1442
- else
1443
- triggers_node = n_xml.xpath("//triggers").first
1444
- reverse_build_trigger_node = triggers_node.add_child(
1445
- "<jenkins.triggers.ReverseBuildTrigger/>"
1446
- )
1447
- reverse_build_trigger_node.first.add_child(
1448
- "<spec/>"
1449
- )
1450
- reverse_build_trigger_node.first.add_child(
1451
- "<upstreamProjects>#{upstream_projects}</upstreamProjects>"
1452
- )
1453
- threshold_node = reverse_build_trigger_node.first.add_child(
1454
- "<threshold/>"
1455
- )
1456
- threshold_node.first.add_child(
1457
- "<name>#{name}</name><ordinal>#{ord}</ordinal><color>#{col}</color>"
1458
- )
1459
- end
1460
- xml_modified = n_xml.to_xml
1461
- post_config(job_name, xml_modified)
1462
- end
1463
-
1464
- # Remove all upstream projects of a specific job
1465
- #
1466
- # @param [String] job_name
1467
- #
1468
- # @return [String] response_code return code from HTTP POST
1469
- #
1470
- def remove_upstream_projects(job_name)
1471
- @logger.info "Removing the upstream projects of '#{job_name}'"
1472
- xml = get_config(job_name)
1473
- n_xml = Nokogiri::XML(xml)
1474
- n_xml.search("//jenkins.triggers.ReverseBuildTrigger").remove
1475
- xml_modified = n_xml.to_xml
1476
- post_config(job_name, xml_modified)
1477
- end
1478
-
1479
- # Resctrict the given job to a specific node
1480
- #
1481
- # @param [String] job_name
1482
- # @param [String] node_name
1483
- #
1484
- # @return [String] response_code return code from HTTP POST
1485
- #
1486
- def restrict_to_node(job_name, node_name)
1487
- @logger.info "Restricting '#{job_name}' to '#{node_name}' node"
1488
- xml = get_config(job_name)
1489
- n_xml = Nokogiri::XML(xml)
1490
- if (node = n_xml.xpath("//assignedNode").first)
1491
- node.content = node_name
1492
- else
1493
- project = n_xml.xpath("//scm").first
1494
- project.add_next_sibling("<assignedNode>#{node_name}</assignedNode>")
1495
- roam_node = n_xml.xpath("//canRoam").first
1496
- roam_node.content = "false"
1497
- end
1498
- xml_modified = n_xml.to_xml
1499
- post_config(job_name, xml_modified)
1500
- end
1501
-
1502
- # Unchain any existing chain between given job names
1503
- #
1504
- # @param [Array] job_names Array of job names to be unchained
1505
- #
1506
- def unchain(job_names)
1507
- @logger.info "Unchaining jobs: #{job_names.inspect}"
1508
- job_names.each { |job| remove_downstream_projects(job) }
1509
- end
1510
-
1511
- # Chain the jobs given based on specified criteria
1512
- #
1513
- # @param [Array] job_names Array of job names to be chained
1514
- # @param [String] threshold threshold for running the next job
1515
- # @param [Array] criteria criteria which should be applied for
1516
- # picking the jobs for the chain
1517
- # @param [Integer] parallel Number of jobs that should be considered
1518
- # for parallel run
1519
- #
1520
- # @return [Array] job_names Names of jobs that are in the top of the
1521
- # chain
1522
- def chain(job_names, threshold, criteria, parallel = 1)
1523
- raise "Parallel jobs should be at least 1" if parallel < 1
1524
- unchain(job_names)
1525
-
1526
- @logger.info "Chaining jobs: #{job_names.inspect}" +
1527
- " with threshold of '#{threshold}' and criteria as '#{criteria}'" +
1528
- " with #{parallel} number of parallel jobs"
1529
- filtered_job_names = []
1530
- if criteria.include?("all") || criteria.empty?
1531
- filtered_job_names = job_names
1532
- else
1533
- job_names.each do |job|
1534
- filtered_job_names << job if criteria.include?(
1535
- @client.job.get_current_build_status(job)
1536
- )
1537
- end
1538
- end
1539
-
1540
- filtered_job_names.each_with_index do |job_name, index|
1541
- break if index >= (filtered_job_names.length - parallel)
1542
- @client.job.add_downstream_projects(
1543
- job_name, filtered_job_names[index + parallel], threshold, true
1544
- )
1545
- end
1546
- if parallel > filtered_job_names.length
1547
- parallel = filtered_job_names.length
1548
- end
1549
- filtered_job_names[0..parallel - 1]
1550
- end
1551
-
1552
- # Get a list of promoted builds for given job
1553
- #
1554
- # @param [String] job_name
1555
- # @return [Hash] Hash map of promitions and the promoted builds. Promotions that didn't took place yet
1556
- # return nil
1557
- def get_promotions(job_name)
1558
- result = {}
1559
-
1560
- @logger.info "Obtaining the promotions of '#{job_name}'"
1561
- response_json = @client.api_get_request("/job/#{job_name}/promotion")
1562
-
1563
- response_json["processes"].each do |promotion|
1564
- @logger.info "Getting promotion details of '#{promotion['name']}'"
1565
-
1566
- if promotion['color'] == 'notbuilt'
1567
- result[promotion['name']] = nil
1568
- else
1569
- promo_json = @client.api_get_request("/job/#{job_name}/promotion/latest/#{promotion['name']}")
1570
- result[promotion['name']] = promo_json['target']['number']
1571
- end
1572
- end
1573
-
1574
- result
1575
- end
1576
-
1577
- # Create a new promotion process
1578
- #
1579
- # This must be called before set/get promote config can be used on a process
1580
- #
1581
- # Must be called after updating the job's config
1582
- # @param [String] job_name
1583
- # @param [String] process The process name
1584
- # @return [String] Process config
1585
- def init_promote_process(job_name, process, config)
1586
- @logger.info "Creating new process #{process} for job #{job_name}"
1587
- @client.post_config("/job/#{job_name}/promotion/createProcess?name=#{process}", config)
1588
- end
1589
-
1590
- # Get a job's promotion config
1591
- #
1592
- # @param [String] job_name
1593
- # @param [String] process The process name
1594
- # @return [String] Promote config
1595
- def get_promote_config(job_name, process)
1596
- @logger.info "Getting promote config for job '#{job_name}' process '#{process}'"
1597
- @client.get_config("/job/#{job_name}/promotion/process/#{process}/config.xml")
1598
- end
1599
-
1600
- # Set a job's promotion config
1601
- #
1602
- # @param [String] job_name
1603
- # @param [String] process The process name
1604
- # @param [String] Job config
1605
- # @return nil
1606
- def set_promote_config(job_name, process, config)
1607
- @logger.info "Setting promote config for job '#{job_name}' process '#{process}' to #{config}"
1608
- @client.post_config("/job/#{job_name}/promotion/process/#{process}/config.xml", config)
1609
- end
1610
-
1611
- # Delete a job's promotion config
1612
- #
1613
- # @param [String] job_name
1614
- # @param [String] process The process name
1615
- # @return nil
1616
- def delete_promote_config(job_name, process)
1617
- @logger.info "Deleting promote config for job '#{job_name}' process '#{process}'"
1618
- @client.post_config("/job/#{job_name}/promotion/process/#{process}/doDelete")
1619
- end
1620
-
1621
- #A Method to find artifacts path from the Current Build
1622
- #
1623
- # @param [String] job_name
1624
- # @param [Integer] build_number
1625
- # defaults to latest build
1626
- #
1627
- def find_artifact(job_name, build_number = 0)
1628
- find_artifacts(job_name, build_number).first
1629
- end
1630
-
1631
- #A Method to check artifact exists path from the Current Build
1632
- #
1633
- # @param [String] job_name
1634
- # @param [Integer] build_number
1635
- # defaults to latest build
1636
- #
1637
- def artifact_exists?(job_name, build_number = 0)
1638
- begin
1639
- artifact_path(job_name: job_name, build_number: build_number)
1640
-
1641
- return true
1642
- rescue Exception => e
1643
- return false
1644
- end
1645
- end
1646
-
1647
- # Find the artifacts for build_number of job_name, defaulting to current job
1648
- #
1649
- # @param [String] job_name
1650
- # @param [Integer] build_number Optional build number
1651
- # @return [String, Hash] JSON response from Jenkins
1652
- #
1653
- def find_artifacts(job_name, build_number = nil)
1654
- response_json = get_build_details(job_name, build_number)
1655
- artifact_path(build_details: response_json).map do |p|
1656
- URI.escape("#{response_json['url']}artifact/#{p['relativePath']}")
1657
- end
1658
- end
1659
-
1660
- # Find the artifacts for the current job
1661
- #
1662
- # @param [String] job_name
1663
- # @return [String, Hash] JSON response from Jenkins
1664
- #
1665
- def find_latest_artifacts(job_name)
1666
- find_artifacts(job_name)
1667
- end
1668
-
1669
- private
1670
-
1671
- # Obtains the threshold params used by jenkins in the XML file
1672
- # given the threshold
1673
- #
1674
- # @param [String] threshold success, failure, or unstable
1675
- #
1676
- # @return [String] status readable status matching the color
1677
- #
1678
- def get_threshold_params(threshold)
1679
- case threshold
1680
- when 'success'
1681
- name = 'SUCCESS'
1682
- ordinal = 0
1683
- color = 'BLUE'
1684
- when 'unstable'
1685
- name = 'UNSTABLE'
1686
- ordinal = 1
1687
- color = 'YELLOW'
1688
- when 'failure'
1689
- name = 'FAILURE'
1690
- ordinal = 2
1691
- color = 'RED'
1692
- end
1693
- return name, ordinal, color
1694
- end
1695
-
1696
- # This private method builds portion of XML that adds subversion SCM
1697
- # to a Job
1698
- #
1699
- # @param [Hash] params parameters to be used for building XML
1700
- # @param [XML] xml Nokogiri XML object
1701
- #
1702
- def scm_subversion(params, xml)
1703
- xml.scm(:class => "hudson.scm.SubversionSCM",
1704
- :plugin => "subversion@1.39") {
1705
- xml.locations {
1706
- xml.send("hudson.scm.SubversionSCM_-ModuleLocation") {
1707
- xml.remote "#{params[:scm_url]}"
1708
- xml.local "."
1709
- }
1710
- }
1711
- xml.excludedRegions
1712
- xml.includedRegions
1713
- xml.excludedUsers
1714
- xml.excludedRevprop
1715
- xml.excludedCommitMessages
1716
- xml.workspaceUpdater(:class =>
1717
- "hudson.scm.subversion.UpdateUpdater")
1718
- }
1719
- end
1720
-
1721
- # This private method builds portion of XML that adds CVS SCM to a Job
1722
- #
1723
- # @param [Hash] params parameters to be used for building XML
1724
- # @param [XML] xml Nokogiri XML object
1725
- #
1726
- def scm_cvs(params, xml)
1727
- xml.scm(:class => "hudson.scm.CVSSCM",
1728
- :plugin => "cvs@1.6") {
1729
- xml.cvsroot "#{params[:scm_url]}"
1730
- xml.module "#{params[:scm_module]}"
1731
- if params[:scm_branch]
1732
- xml.branch "#{params[:scm_branch]}"
1733
- else
1734
- xml.branch "#{params[:scm_tag]}"
1735
- end
1736
- xml.canUseUpdate true
1737
- xml.useHeadIfNotFound(
1738
- "#{params[:scm_use_head_if_tag_not_found]}")
1739
- xml.flatten true
1740
- if params[:scm_tag]
1741
- xml.isTag true
1742
- else
1743
- xml.isTag false
1744
- end
1745
- xml.excludedRegions
1746
- }
1747
- end
1748
-
1749
- # This private method adds portion of XML that adds Git SCM to a Job
1750
- #
1751
- # @param [Hash] params parameters to be used for building XML
1752
- # @param [XML] xml Nokogiri XML object
1753
- #
1754
- def scm_git(params, xml)
1755
- xml.scm(:class => "hudson.plugins.git.GitSCM") {
1756
- xml.configVersion "2"
1757
- xml.userRemoteConfigs {
1758
- xml.send("hudson.plugins.git.UserRemoteConfig") {
1759
- xml.name
1760
- xml.refspec
1761
- xml.credentialsId "#{params[:scm_credentials_id]}"
1762
- xml.url "#{params[:scm_url]}"
1763
- }
1764
- }
1765
- xml.branches {
1766
- xml.send("hudson.plugins.git.BranchSpec") {
1767
- xml.name "#{params[:scm_branch]}"
1768
- }
1769
- }
1770
- xml.disableSubmodules "false"
1771
- xml.recursiveSubmodules "false"
1772
- xml.doGenerateSubmoduleConfigurations "false"
1773
- xml.authorOrCommitter "false"
1774
- xml.clean "false"
1775
- xml.wipeOutWorkspace "false"
1776
- xml.pruneBranches "false"
1777
- xml.remotePoll "false"
1778
- xml.ignoreNotifyCommit "false"
1779
- xml.useShallowClone "false"
1780
- xml.buildChooser(:class =>
1781
- "hudson.plugins.git.util.DefaultBuildChooser")
1782
- xml.gitTool params.fetch(:scm_git_tool) { "Default" }
1783
- xml.submoduleCfg(:class => "list")
1784
- xml.relativeTargetDir
1785
- xml.reference
1786
- xml.excludedRegions
1787
- xml.excludedUsers
1788
- xml.gitConfigName
1789
- xml.gitConfigEmail
1790
- xml.skipTag "false"
1791
- xml.includedRegions
1792
- xml.scmName
1793
- }
1794
- end
1795
-
1796
- # Method for creating portion of xml that builds Skype notification
1797
- # Use this option only when you have the Skype plugin installed and
1798
- # everything is set up properly
1799
- #
1800
- # @param [Hash] params Parameters for adding skype notificaiton. For the
1801
- # options in this params Hash refer to create_freestyle
1802
- # @param [XML] xml Main xml to attach the skype portion.
1803
- #
1804
- def skype_notification(params, xml)
1805
- params[:skype_strategy] = case params[:skype_strategy]
1806
- when "all"
1807
- "ALL"
1808
- when "failure"
1809
- "ANY_FAILURE"
1810
- when "failure_and_fixed"
1811
- "FAILURE_AND_FIXED"
1812
- when "change"
1813
- "STATECHANGE_ONLY"
1814
- else
1815
- "STATECHANGE_ONLY"
1816
- end
1817
-
1818
- params[:skype_notify_on_build_start] = false if params[:skype_notify_on_build_start].nil?
1819
- params[:skype_notify_suspects] = false if params[:skype_notify_suspects].nil?
1820
- params[:skype_notify_culprits] = false if params[:skype_notify_culprits].nil?
1821
- params[:skype_notify_fixers] = false if params[:skype_notify_fixers].nil?
1822
- params[:skype_notify_upstream_committers] = false if params[:skype_notify_upstream_committers].nil?
1823
-
1824
- targets = params[:skype_targets].split(/\s+/)
1825
- xml.send("hudson.plugins.skype.im.transport.SkypePublisher") {
1826
- xml.targets {
1827
- targets.each { |target|
1828
- if target =~ /^\*/
1829
- # Group Chat
1830
- xml.send("hudson.plugins.im.GroupChatIMMessageTarget") {
1831
- # Skipe the first * character
1832
- xml.value target[1..-1]
1833
- xml.notificationOnly false
1834
- }
1835
- else
1836
- # Individual message
1837
- xml.send("hudson.plugins.im.DefaultIMMessageTarget") {
1838
- xml.value target
1839
- }
1840
- end
1841
- }
1842
- }
1843
- xml.strategy "#{params[:skype_strategy]}"
1844
- xml.notifyOnBuildStart params[:skype_notify_on_build_start]
1845
- xml.notifySuspects params[:skype_notify_suspects]
1846
- xml.notifyCulprits params[:skype_notify_culprits]
1847
- xml.notifyFixers params[:skype_notify_fixers]
1848
- xml.notifyUpstreamCommitters params[:skype_notify_upstream_committers]
1849
- notification_class = case params[:skype_message]
1850
- when "just_summary"
1851
- "hudson.plugins.im.build_notify.SummaryOnlyBuildToChatNotifier"
1852
- when "summary_and_scm_changes"
1853
- "hudson.plugins.im.build_notify.DefaultBuildToChatNotifier"
1854
- when "summary_and_build_parameters"
1855
- "hudson.plugins.im.build_notify.BuildParametersBuildToChatNotifier"
1856
- when "summary_scm_changes_and_failed_tests"
1857
- "hudson.plugins.im.build_notify.PrintFailingTestsBuildToChatNotifier"
1858
- else
1859
- "hudson.plugins.im.build_notify.DefaultBuildToChatNotifier"
1860
- end
1861
- xml.buildToChatNotifier(:class => notification_class)
1862
- xml.matrixMultiplier "ONLY_CONFIGURATIONS"
1863
- }
1864
- end
1865
-
1866
- # This private method builds portion of XML that adds notification email
1867
- # to a Job.
1868
- #
1869
- # @param [Hash] params parameters to be used for building XML
1870
- # @param [XML] xml Nokogiri XML object
1871
- #
1872
- def notification_email(params, xml)
1873
- if params[:notification_email]
1874
- xml.send("hudson.tasks.Mailer") {
1875
- xml.recipients "#{params[:notification_email]}"
1876
- xml.dontNotifyEveryUnstableBuild(
1877
- "#{params[:notification_email_for_every_unstable]}")
1878
- xml.sendToIndividuals(
1879
- "#{params[:notification_email_send_to_individuals]}")
1880
- }
1881
- end
1882
- end
1883
-
1884
- # This private method builds portion of XML that adds child projects
1885
- # to a Job.
1886
- #
1887
- # @param [Hash] params parameters to be used for building XML
1888
- # @param [XML] xml Nokogiri XML object
1889
- #
1890
- def child_projects(params, xml)
1891
- xml.send("hudson.tasks.BuildTrigger") {
1892
- xml.childProjects "#{params[:child_projects]}"
1893
- threshold = params[:child_threshold]
1894
- name, ordinal, color = get_threshold_params(threshold)
1895
- xml.threshold {
1896
- xml.name "#{name}"
1897
- xml.ordinal "#{ordinal}"
1898
- xml.color "#{color}"
1899
- }
1900
- }
1901
- end
1902
-
1903
- def tree_string tree_value
1904
- return nil unless tree_value
1905
- "tree=#{tree_value}"
1906
- end
1907
-
1908
- # This private method gets the artifact path or throws an exception
1909
- #
1910
- # @param [Hash] job_name, build_number or build_details object
1911
- #
1912
- def artifact_path(params)
1913
- job_name = params[:job_name]
1914
- build_number = params[:build_number] || 0
1915
- build_details = params[:build_details]
1916
-
1917
- build_details = get_build_details(job_name, build_number) if build_details.nil?
1918
- artifacts = build_details['artifacts']
1919
- artifact_paths = []
1920
-
1921
- if artifacts && artifacts.any?
1922
- artifact_paths = artifacts.find_all { |a| a.key?('relativePath') }
1923
- end
1924
-
1925
- if artifact_paths.empty?
1926
- raise "No artifacts found."
1927
- end
1928
- artifact_paths
1929
- end
1930
- end
1931
- end
1932
- end
1933
- end