lhj-tools 0.2.13 → 0.2.14

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.
@@ -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