jenkins_api_client 0.14.1 → 1.0.0

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +5 -23
  4. data/CHANGELOG.md +104 -35
  5. data/CONTRIBUTORS.md +1 -11
  6. data/Gemfile +4 -2
  7. data/README.md +107 -7
  8. data/Rakefile +1 -0
  9. data/Vagrantfile +6 -8
  10. data/jenkins_api_client.gemspec +30 -12
  11. data/lib/jenkins_api_client/cli/helper.rb +1 -0
  12. data/lib/jenkins_api_client/client.rb +162 -69
  13. data/lib/jenkins_api_client/exceptions.rb +26 -17
  14. data/lib/jenkins_api_client/job.rb +321 -75
  15. data/lib/jenkins_api_client/node.rb +22 -10
  16. data/lib/jenkins_api_client/plugin_manager.rb +460 -0
  17. data/lib/jenkins_api_client/urihelper.rb +17 -0
  18. data/lib/jenkins_api_client/user.rb +4 -2
  19. data/lib/jenkins_api_client/version.rb +3 -3
  20. data/lib/jenkins_api_client/view.rb +10 -7
  21. data/lib/jenkins_api_client.rb +1 -0
  22. data/scripts/login_with_pry.rb +54 -0
  23. data/spec/func_tests/client_spec.rb +3 -3
  24. data/spec/func_tests/job_spec.rb +90 -7
  25. data/spec/func_tests/{node_spec.rb → node_spec.rb.pending} +9 -9
  26. data/spec/func_tests/plugin_spec.rb +148 -0
  27. data/spec/unit_tests/client_spec.rb +108 -27
  28. data/spec/unit_tests/fake_http_response.rb +9 -0
  29. data/spec/unit_tests/fixtures/files/available_plugins.json +1 -0
  30. data/spec/unit_tests/fixtures/files/installed_plugins.json +1 -0
  31. data/spec/unit_tests/fixtures/files/updatable_plugins.json +1 -0
  32. data/spec/unit_tests/job_spec.rb +109 -6
  33. data/spec/unit_tests/node_spec.rb +18 -6
  34. data/spec/unit_tests/plugin_spec.rb +165 -0
  35. data/spec/unit_tests/spec_helper.rb +11 -1
  36. data/spec/unit_tests/system_spec.rb +2 -1
  37. data/spec/unit_tests/user_spec.rb +1 -1
  38. data/travis/hudson.model.UpdateCenter.xml +7 -0
  39. data/travis/setup.sh +2 -1
  40. metadata +76 -64
@@ -20,12 +20,18 @@
20
20
  # THE SOFTWARE.
21
21
  #
22
22
 
23
+ require 'jenkins_api_client/urihelper'
24
+
23
25
  module JenkinsApi
24
26
  class Client
25
27
  # This class communicates with the Jenkins "/job" API to obtain details
26
28
  # about jobs, creating, deleting, building, and various other operations.
27
29
  #
28
30
  class Job
31
+ include JenkinsApi::UriHelper
32
+
33
+ # Version that jenkins started to include queued build info in build response
34
+ JENKINS_QUEUE_ID_SUPPORT_VERSION = '1.519'
29
35
 
30
36
  # Initialize the Job object and store the reference to Client object
31
37
  #
@@ -74,7 +80,7 @@ module JenkinsApi
74
80
  #
75
81
  def create(job_name, xml)
76
82
  @logger.info "Creating job '#{job_name}'"
77
- @client.post_config("/createItem?name=#{job_name}", xml)
83
+ @client.post_config("/createItem?name=#{form_encode job_name}", xml)
78
84
  end
79
85
 
80
86
  # Update a job with the name specified and the xml given
@@ -197,7 +203,7 @@ module JenkinsApi
197
203
  # Update a job with params given as a hash instead of the xml. For the
198
204
  # parameter description see #create_or_update_freestyle
199
205
  #
200
- # @param params [Hash]
206
+ # @param params [Hash] parameters to update a freestyle project
201
207
  #
202
208
  # @see #create_or_update_freestyle
203
209
  # @see #update
@@ -334,7 +340,7 @@ module JenkinsApi
334
340
  # @option params [String] :name Name of the job
335
341
  # @option params [String] :notification_email Email address to send
336
342
  # @option params [Boolean] :notification_email_for_every_unstable
337
- # Send email notification email for every unstable build
343
+ # Send email notification email for every unstable build
338
344
  #
339
345
  def add_email_notification(params)
340
346
  raise "No job name specified" unless params[:name]
@@ -400,16 +406,18 @@ module JenkinsApi
400
406
  #
401
407
  def rename(old_job, new_job)
402
408
  @logger.info "Renaming job '#{old_job}' to '#{new_job}'"
403
- @client.api_post_request("/job/#{old_job}/doRename?newName=#{new_job}")
409
+ @client.api_post_request("/job/#{path_encode old_job}/doRename?newName=#{form_encode new_job}")
404
410
  end
405
411
 
406
412
  # Delete a job given the name
407
413
  #
408
- # @param [String] job_name
414
+ # @param job_name [String] the name of the job to delete
415
+ #
416
+ # @return [String] the response from the HTTP POST request
409
417
  #
410
418
  def delete(job_name)
411
419
  @logger.info "Deleting job '#{job_name}'"
412
- @client.api_post_request("/job/#{job_name}/doDelete")
420
+ @client.api_post_request("/job/#{path_encode job_name}/doDelete")
413
421
  end
414
422
 
415
423
  # Deletes all jobs from Jenkins
@@ -424,11 +432,13 @@ module JenkinsApi
424
432
 
425
433
  # Wipe out the workspace for a job given the name
426
434
  #
427
- # @param [String] job_name
435
+ # @param job_name [String] the name of the job to wipe out the workspace
436
+ #
437
+ # @return [String] response from the HTTP POST request
428
438
  #
429
439
  def wipe_out_workspace(job_name)
430
440
  @logger.info "Wiping out the workspace of job '#{job_name}'"
431
- @client.api_post_request("/job/#{job_name}/doWipeOutWorkspace")
441
+ @client.api_post_request("/job/#{path_encode job_name}/doWipeOutWorkspace")
432
442
  end
433
443
 
434
444
  # Stops a running build of a job
@@ -436,8 +446,8 @@ module JenkinsApi
436
446
  # is specified. The build will be stopped only if it was
437
447
  # in 'running' state.
438
448
  #
439
- # @param [String] job_name
440
- # @param [Number] build_number
449
+ # @param job_name [String] the name of the job to stop the build
450
+ # @param build_number [Number] the build number to stop
441
451
  #
442
452
  def stop_build(job_name, build_number = 0)
443
453
  build_number = get_current_build_number(job_name) if build_number == 0
@@ -445,17 +455,21 @@ module JenkinsApi
445
455
  @logger.info "Stopping job '#{job_name}' Build ##{build_number}"
446
456
  # Check and see if the build is running
447
457
  is_building = @client.api_get_request(
448
- "/job/#{job_name}/#{build_number}"
458
+ "/job/#{path_encode job_name}/#{build_number}"
449
459
  )["building"]
450
460
  if is_building
451
- @client.api_post_request("/job/#{job_name}/#{build_number}/stop")
461
+ @client.api_post_request("/job/#{path_encode job_name}/#{build_number}/stop")
452
462
  end
453
463
  end
464
+ alias_method :stop, :stop_build
465
+ alias_method :abort, :stop_build
454
466
 
455
467
  # Re-create the same job
456
468
  # This is a hack to clear any existing builds
457
469
  #
458
- # @param [String] job_name
470
+ # @param job_name [String] the name of the job to recreate
471
+ #
472
+ # @return [String] the response from the HTTP POST request
459
473
  #
460
474
  def recreate(job_name)
461
475
  @logger.info "Recreating job '#{job_name}'"
@@ -466,14 +480,16 @@ module JenkinsApi
466
480
 
467
481
  # Copy a job
468
482
  #
469
- # @param [String] from_job_name
470
- # @param [String] to_job_name
483
+ # @param from_job_name [String] the name of the job to copy from
484
+ # @param to_job_name [String] the name of the job to copy to
485
+ #
486
+ # @return [String] the response from the HTTP POST request
471
487
  #
472
488
  def copy(from_job_name, to_job_name=nil)
473
489
  to_job_name = "copy_of_#{from_job_name}" if to_job_name.nil?
474
490
  @logger.info "Copying job '#{from_job_name}' to '#{to_job_name}'"
475
491
  @client.api_post_request(
476
- "/createItem?name=#{to_job_name}&mode=copy&from=#{from_job_name}"
492
+ "/createItem?name=#{path_encode to_job_name}&mode=copy&from=#{path_encode from_job_name}"
477
493
  )
478
494
  end
479
495
 
@@ -481,7 +497,7 @@ module JenkinsApi
481
497
  #
482
498
  # @param [String] job_name Name of the Jenkins job
483
499
  # @param [Number] build_num Specific build number to obtain the
484
- # console output from. Default is the recent build
500
+ # console output from. Default is the recent build
485
501
  # @param [Number] start start offset to get only a portion of the text
486
502
  # @param [String] mode Mode of text output. 'text' or 'html'
487
503
  #
@@ -505,7 +521,7 @@ module JenkinsApi
505
521
  else
506
522
  raise "Mode should either be 'text' or 'html'. You gave: #{mode}"
507
523
  end
508
- get_msg = "/job/#{job_name}/#{build_num}/logText/progressive#{mode}?"
524
+ get_msg = "/job/#{path_encode job_name}/#{build_num}/logText/progressive#{mode}?"
509
525
  get_msg << "start=#{start}"
510
526
  raw_response = true
511
527
  api_response = @client.api_get_request(get_msg, nil, nil, raw_response)
@@ -520,6 +536,8 @@ module JenkinsApi
520
536
 
521
537
  # List all jobs on the Jenkins CI server
522
538
  #
539
+ # @return [Array<String>] the names of all jobs in jenkins
540
+ #
523
541
  def list_all
524
542
  response_json = @client.api_get_request("", "tree=jobs[name]")["jobs"]
525
543
  response_json.map { |job| job["name"] }.sort
@@ -527,7 +545,9 @@ module JenkinsApi
527
545
 
528
546
  # Checks if the given job exists in Jenkins
529
547
  #
530
- # @param [String] job_name
548
+ # @param job_name [String] the name of the job to check
549
+ #
550
+ # @return [Boolean] whether the job exists in jenkins or not
531
551
  #
532
552
  def exists?(job_name)
533
553
  list(job_name).include?(job_name)
@@ -536,8 +556,12 @@ module JenkinsApi
536
556
  # List all Jobs matching the given status
537
557
  # You can optionally pass in jobs list to filter the status from
538
558
  #
539
- # @param [String] status
540
- # @param [Array] jobs
559
+ # @param status [String] the job status to filter
560
+ # @param jobs [Array<String>] if specified this array will be used for
561
+ # filtering by the status otherwise the filtering will be done using
562
+ # all jobs available in jenkins
563
+ #
564
+ # @return [Array<String>] filtered jobs
541
565
  #
542
566
  def list_by_status(status, jobs = [])
543
567
  jobs = list_all if jobs.empty?
@@ -555,8 +579,10 @@ module JenkinsApi
555
579
 
556
580
  # List all jobs that match the given regex
557
581
  #
558
- # @param [String] filter - a regex
559
- # @param [Boolean] ignorecase
582
+ # @param filter [String] a regular expression or a string to filter jobs
583
+ # @param ignorecase [Boolean] whether to ignore case or not
584
+ #
585
+ # @return [Array<String>] jobs matching the given pattern
560
586
  #
561
587
  def list(filter, ignorecase = true)
562
588
  @logger.info "Obtaining jobs matching filter '#{filter}'"
@@ -574,6 +600,8 @@ module JenkinsApi
574
600
 
575
601
  # List all jobs on the Jenkins CI server along with their details
576
602
  #
603
+ # @return [Array<Hash>] the details of all jobs in jenkins
604
+ #
577
605
  def list_all_with_details
578
606
  @logger.info "Obtaining the details of all jobs"
579
607
  response_json = @client.api_get_request("")
@@ -582,30 +610,34 @@ module JenkinsApi
582
610
 
583
611
  # List details of a specific job
584
612
  #
585
- # @param [String] job_name
613
+ # @param job_name [String] the name of the job to obtain the details from
614
+ #
615
+ # @return [Hash] the details of the specified job
586
616
  #
587
617
  def list_details(job_name)
588
618
  @logger.info "Obtaining the details of '#{job_name}'"
589
- @client.api_get_request("/job/#{job_name}")
619
+ @client.api_get_request("/job/#{path_encode job_name}")
590
620
  end
591
621
 
592
622
  # List upstream projects of a specific job
593
623
  #
594
- # @param [String] job_name
624
+ # @param job_name [String] the name of the job to obtain upstream
625
+ # projects for
595
626
  #
596
627
  def get_upstream_projects(job_name)
597
628
  @logger.info "Obtaining the upstream projects of '#{job_name}'"
598
- response_json = @client.api_get_request("/job/#{job_name}")
629
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
599
630
  response_json["upstreamProjects"]
600
631
  end
601
632
 
602
633
  # List downstream projects of a specific job
603
634
  #
604
- # @param [String] job_name
635
+ # @param job_name [String] the name of the job to obtain downstream
636
+ # projects for
605
637
  #
606
638
  def get_downstream_projects(job_name)
607
639
  @logger.info "Obtaining the down stream projects of '#{job_name}'"
608
- response_json = @client.api_get_request("/job/#{job_name}")
640
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
609
641
  response_json["downstreamProjects"]
610
642
  end
611
643
 
@@ -615,7 +647,7 @@ module JenkinsApi
615
647
  #
616
648
  def get_builds(job_name)
617
649
  @logger.info "Obtaining the build details of '#{job_name}'"
618
- response_json = @client.api_get_request("/job/#{job_name}")
650
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
619
651
  response_json["builds"]
620
652
  end
621
653
 
@@ -657,9 +689,10 @@ module JenkinsApi
657
689
  #
658
690
  def get_current_build_status(job_name)
659
691
  @logger.info "Obtaining the current build status of '#{job_name}'"
660
- response_json = @client.api_get_request("/job/#{job_name}")
692
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
661
693
  color_to_status(response_json["color"])
662
694
  end
695
+ alias_method :status, :get_current_build_status
663
696
 
664
697
  # Obtain the current build number of the given job
665
698
  # This function returns nil if there were no builds for the given job.
@@ -670,60 +703,248 @@ module JenkinsApi
670
703
  #
671
704
  def get_current_build_number(job_name)
672
705
  @logger.info "Obtaining the current build number of '#{job_name}'"
673
- @client.api_get_request("/job/#{job_name}")['nextBuildNumber'].to_i - 1
706
+ @client.api_get_request("/job/#{path_encode job_name}")['nextBuildNumber'].to_i - 1
674
707
  end
675
-
676
- # Build a job given the name of the job
677
- # You can optionally pass in a list of params for Jenkins to use for
678
- # parameterized builds
708
+ alias_method :build_number, :get_current_build_number
709
+
710
+ # Build a Jenkins job, optionally waiting for build to start and
711
+ # returning the build number.
712
+ # Adds support for new/old Jenkins servers where build_queue id may
713
+ # not be available. Also adds support for periodic callbacks, and
714
+ # optional cancellation of queued_job if not started within allowable
715
+ # time window (if build_queue option available)
716
+ #
717
+ # Notes:
718
+ # 'opts' may be a 'true' or 'false' value to maintain
719
+ # compatibility with old method signature, where true indicates
720
+ # 'return_build_number'. In this case, true is translated to:
721
+ # { 'build_start_timeout' => @client_timeout }
722
+ # which simulates earlier behavior.
723
+ #
724
+ # progress_proc
725
+ # Optional proc that is called periodically while waiting for
726
+ # build to start.
727
+ # Initial call (with poll_count == 0) indicates build has been
728
+ # requested, and that polling is starting.
729
+ # Final call will indicate one of build_started or cancelled.
730
+ # params:
731
+ # max_wait [Integer] Same as opts['build_start_timeout']
732
+ # current_wait [Integer]
733
+ # poll_count [Integer] How many times has queue been polled
734
+ #
735
+ # completion_proc
736
+ # Optional proc that is called <just before> the 'build' method
737
+ # exits.
738
+ # params:
739
+ # build_number [Integer] Present if build started or nil
740
+ # build_cancelled [Boolean] True if build timed out and was
741
+ # successfully removed from build-queue
679
742
  #
680
743
  # @param [String] job_name the name of the job
681
- # @param [Hash] params the parameters for parameterized builds
682
- # @param [Boolean] return_build_number whether to wait and obtain the build
683
- # number
684
- #
685
- # @return [String, Integer] the response code from the build POST request
686
- # if return_build_number is not requested and the build number if the
687
- # return_build_number is requested. nil will be returned if the build
688
- # number is requested and not available. This can happen if there is
689
- # already a job in the queue and concurrent build is disabled.
690
- #
691
- def build(job_name, params={}, return_build_number = false)
744
+ # @param [Hash] params the parameters for parameterized build
745
+ # @param [Hash] opts options for this method
746
+ # * +build_start_timeout+ [Integer] How long to wait for queued
747
+ # build to start before giving up. Default: 0/nil
748
+ # * +cancel_on_build_start_timeout+ [Boolean] Should an attempt be
749
+ # made to cancel the queued build if it hasn't started within
750
+ # 'build_start_timeout' seconds? This only works on newer versions
751
+ # of Jenkins where JobQueue is exposed in build post response.
752
+ # Default: false
753
+ # * +poll_interval+ [Integer] How often should we check with CI
754
+ # Server while waiting for start. Default: 2 (seconds)
755
+ # * +progress_proc+ [Proc] A proc that will receive progress notitications. Default: nil
756
+ # * +completion_proc+ [Proc] A proc that is called <just before>
757
+ # this method (build) exits. Default: nil
758
+ #
759
+ # @return [Integer] build number, or nil if not started (IF TIMEOUT SPECIFIED)
760
+ # @return [Integer] HTTP response code (per prev. behavior) (NO TIMEOUT SPECIFIED)
761
+ #
762
+ def build(job_name, params={}, opts = {})
763
+ if opts.nil? || opts.class.is_a?(FalseClass)
764
+ opts = {}
765
+ elsif opts.class.is_a?(TrueClass)
766
+ opts = { 'build_start_timeout' => @client_timeout }
767
+ end
768
+
769
+ opts['job_name'] = job_name
770
+
692
771
  msg = "Building job '#{job_name}'"
693
772
  msg << " with parameters: #{params.inspect}" unless params.empty?
694
773
  @logger.info msg
695
- build_endpoint = params.empty? ? "build" : "buildWithParameters"
696
- raw_response = return_build_number
697
- response =@client.api_post_request(
698
- "/job/#{job_name}/#{build_endpoint}",
699
- params,
700
- raw_response
701
- )
702
- # If return_build_number is enabled, obtain the queue ID from the location
774
+
775
+ # Best-guess build-id
776
+ # This is only used if we go the old-way below... but we can use this number to detect if multiple
777
+ # builds were queued
778
+ current_build_id = get_current_build_number(job_name)
779
+ expected_build_id = current_build_id > 0 ? current_build_id + 1 : 1
780
+
781
+ if (params.nil? or params.empty?)
782
+ response = @client.api_post_request("/job/#{path_encode job_name}/build",
783
+ {},
784
+ true)
785
+ else
786
+ response = @client.api_post_request("/job/#{path_encode job_name}/buildWithParameters",
787
+ params,
788
+ true)
789
+ end
790
+
791
+ if (opts['build_start_timeout'] || 0) > 0
792
+ if @client.compare_versions(@client.get_jenkins_version, JENKINS_QUEUE_ID_SUPPORT_VERSION) >= 0
793
+ return get_build_id_from_queue(response, expected_build_id, opts)
794
+ else
795
+ return get_build_id_the_old_way(expected_build_id, opts)
796
+ end
797
+ else
798
+ return response.code
799
+ end
800
+ end
801
+
802
+ def get_build_id_from_queue(response, expected_build_id, opts)
803
+ # If we get this far the API hasn't detected an error response (it would raise Exception)
804
+ # So no need to check response code
805
+ # Obtain the queue ID from the location
703
806
  # header and wait till the build is moved to one of the executors and a
704
807
  # build number is assigned
705
- if return_build_number
706
- if response["location"]
707
- task_id_match = response["location"].match(/\/item\/(\d*)\//)
708
- task_id = task_id_match.nil? ? nil : task_id_match[1]
709
- unless task_id.nil?
710
- @logger.debug "Queue task ID for job '#{job_name}': #{task_id}"
711
- Timeout::timeout(@client.timeout) do
712
- while @client.queue.get_item_by_id(task_id)["executable"].nil?
713
- sleep 5
808
+ build_start_timeout = opts['build_start_timeout']
809
+ poll_interval = opts['poll_interval'] || 2
810
+ poll_interval = 1 if poll_interval < 1
811
+ progress_proc = opts['progress_proc']
812
+ completion_proc = opts['completion_proc']
813
+ job_name = opts['job_name']
814
+
815
+ if response["location"]
816
+ task_id_match = response["location"].match(/\/item\/(\d*)\//)
817
+ task_id = task_id_match.nil? ? nil : task_id_match[1]
818
+ unless task_id.nil?
819
+ @logger.info "Job queued for #{job_name}, will wait up to #{build_start_timeout} seconds for build to start..."
820
+
821
+ # Let progress proc know we've queued the build
822
+ progress_proc.call(build_start_timeout, 0, 0) if progress_proc
823
+
824
+ # Wait for the build to start
825
+ begin
826
+ start = Time.now.to_i
827
+ Timeout::timeout(build_start_timeout) do
828
+ started = false
829
+ attempts = 0
830
+
831
+ while !started
832
+ # Don't really care about the response... if we get thru here, then it must have worked.
833
+ # Jenkins will return 404's until the job starts
834
+ queue_item = @client.queue.get_item_by_id(task_id)
835
+
836
+ if queue_item['executable'].nil?
837
+ # Job not started yet
838
+ attempts += 1
839
+
840
+ progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
841
+ # Every 5 attempts (~10 seconds)
842
+ @logger.info "Still waiting..." if attempts % 5 == 0
843
+
844
+ sleep poll_interval
845
+ else
846
+ build_number = queue_item['executable']['number']
847
+ completion_proc.call(build_number, false) if completion_proc
848
+
849
+ return build_number
850
+ end
714
851
  end
715
852
  end
716
- @client.queue.get_item_by_id(task_id)["executable"]["number"]
717
- else
718
- nil
853
+ rescue Timeout::Error
854
+ # Well, we waited - and the job never started building
855
+ # Attempt to kill off queued job (if flag set)
856
+ if opts['cancel_on_build_start_timeout']
857
+ @logger.info "Job for '#{job_name}' did not start in a timely manner, attempting to cancel pending build..."
858
+
859
+ begin
860
+ @client.api_post_request("/queue/cancelItem?id=#{task_id}")
861
+ @logger.info "Job cancelled"
862
+ completion_proc.call(nil, true) if completion_proc
863
+ rescue JenkinsApi::Exceptions::ApiException => e
864
+ completion_proc.call(nil, false) if completion_proc
865
+ @logger.warn "Error while attempting to cancel pending job for '#{job_name}'. #{e.class} #{e}"
866
+ raise
867
+ end
868
+ else
869
+ @logger.info "Jenkins build for '#{job_name}' failed to start in a timely manner"
870
+ completion_proc.call(nil, false) if completion_proc
871
+ end
872
+
873
+ # Old version used to throw timeout error, so we should let that go thru now
874
+ raise
875
+ rescue JenkinsApi::Exceptions::ApiException => e
876
+ # Jenkins Api threw an error at us
877
+ completion_proc.call(nil, false) if completion_proc
878
+ @logger.warn "Problem while waiting for '#{job_name}' build to start. #{e.class} #{e}"
879
+ raise
719
880
  end
720
881
  else
721
- nil
882
+ @logger.warn "Jenkins did not return a queue_id for '#{job_name}' build (location: #{response['location']})"
883
+ return get_build_id_the_old_way(expected_build_id, opts)
722
884
  end
723
885
  else
724
- response
886
+ @logger.warn "Jenkins did not return a location header for '#{job_name}' build"
887
+ return get_build_id_the_old_way(expected_build_id, opts)
888
+ end
889
+ end
890
+ private :get_build_id_from_queue
891
+
892
+ def get_build_id_the_old_way(expected_build_id, opts)
893
+ # Try to wait until the build starts so we can mimic queue
894
+ # Wait for the build to start
895
+ build_start_timeout = opts['build_start_timeout']
896
+ poll_interval = opts['poll_interval'] || 2
897
+ poll_interval = 1 if poll_interval < 1
898
+ progress_proc = opts['progress_proc']
899
+ completion_proc = opts['completion_proc']
900
+ job_name = opts['job_name']
901
+
902
+ @logger.info "Build requested for '#{job_name}', will wait up to #{build_start_timeout} seconds for build to start..."
903
+
904
+ # Let progress proc know we've queued the build
905
+ progress_proc.call(build_start_timeout, 0, 0) if progress_proc
906
+
907
+ begin
908
+ start = Time.now.to_i
909
+ Timeout::timeout(build_start_timeout) do
910
+ attempts = 0
911
+
912
+ while true
913
+ attempts += 1
914
+
915
+ # Don't really care about the response... if we get thru here, then it must have worked.
916
+ # Jenkins will return 404's until the job starts
917
+ begin
918
+ get_build_details(job_name, expected_build_id)
919
+ completion_proc.call(expected_build_id, false) if completion_proc
920
+
921
+ return expected_build_id
922
+ rescue JenkinsApi::Exceptions::NotFound => e
923
+ progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
924
+
925
+ # Every 5 attempts (~10 seconds)
926
+ @logger.info "Still waiting..." if attempts % 5 == 0
927
+
928
+ sleep poll_interval
929
+ end
930
+ end
931
+ end
932
+ rescue Timeout::Error
933
+ # Well, we waited - and the job never started building
934
+ # Now we need to raise an exception so that the build can be officially failed
935
+ completion_proc.call(nil, false) if completion_proc
936
+ @logger.info "Jenkins '#{job_name}' build failed to start in a timely manner"
937
+
938
+ # Old version used to propagate timeout error
939
+ raise
940
+ rescue JenkinsApi::Exceptions::ApiException => e
941
+ completion_proc.call(nil, false) if completion_proc
942
+ # Jenkins Api threw an error at us
943
+ @logger.warn "Problem while waiting for '#{job_name}' build ##{expected_build_id} to start. #{e.class} #{e}"
944
+ raise
725
945
  end
726
946
  end
947
+ private :get_build_id_the_old_way
727
948
 
728
949
  # Programatically schedule SCM polling for the specified job
729
950
  #
@@ -742,7 +963,7 @@ module JenkinsApi
742
963
  #
743
964
  def enable(job_name)
744
965
  @logger.info "Enabling job '#{job_name}'"
745
- @client.api_post_request("/job/#{job_name}/enable")
966
+ @client.api_post_request("/job/#{path_encode job_name}/enable")
746
967
  end
747
968
 
748
969
  # Disable a job given the name of the job
@@ -751,7 +972,7 @@ module JenkinsApi
751
972
  #
752
973
  def disable(job_name)
753
974
  @logger.info "Disabling job '#{job_name}'"
754
- @client.api_post_request("/job/#{job_name}/disable")
975
+ @client.api_post_request("/job/#{path_encode job_name}/disable")
755
976
  end
756
977
 
757
978
  # Obtain the configuration stored in config.xml of a specific job
@@ -762,7 +983,7 @@ module JenkinsApi
762
983
  #
763
984
  def get_config(job_name)
764
985
  @logger.info "Obtaining the config.xml of '#{job_name}'"
765
- @client.get_config("/job/#{job_name}")
986
+ @client.get_config("/job/#{path_encode job_name}")
766
987
  end
767
988
 
768
989
  # Post the configuration of a job given the job name and the config.xml
@@ -774,7 +995,7 @@ module JenkinsApi
774
995
  #
775
996
  def post_config(job_name, xml)
776
997
  @logger.info "Posting the config.xml of '#{job_name}'"
777
- @client.post_config("/job/#{job_name}/config.xml", xml)
998
+ @client.post_config("/job/#{path_encode job_name}/config.xml", xml)
778
999
  end
779
1000
 
780
1001
  # Obtain the test results for a specific build of a job
@@ -786,7 +1007,7 @@ module JenkinsApi
786
1007
  build_num = get_current_build_number(job_name) if build_num == 0
787
1008
  @logger.info "Obtaining the test results of '#{job_name}'" +
788
1009
  " Build ##{build_num}"
789
- @client.api_get_request("/job/#{job_name}/#{build_num}/testReport")
1010
+ @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/testReport")
790
1011
  rescue Exceptions::NotFound
791
1012
  # Not found is acceptable, as not all builds will have test results
792
1013
  # and this is what jenkins throws at us in that case
@@ -803,7 +1024,7 @@ module JenkinsApi
803
1024
  @logger.info "Obtaining the build details of '#{job_name}'" +
804
1025
  " Build ##{build_num}"
805
1026
 
806
- @client.api_get_request("/job/#{job_name}/#{build_num}/")
1027
+ @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/")
807
1028
  end
808
1029
 
809
1030
  # Change the description of a specific job
@@ -1179,6 +1400,31 @@ module JenkinsApi
1179
1400
  filtered_job_names[0..parallel-1]
1180
1401
  end
1181
1402
 
1403
+ # Get a list of promoted builds for given job
1404
+ #
1405
+ # @param [String] job_name
1406
+ # @return [Hash] Hash map of promitions and the promoted builds. Promotions that didn't took place yet
1407
+ # return nil
1408
+ def get_promotions(job_name)
1409
+ result = {}
1410
+
1411
+ @logger.info "Obtaining the promotions of '#{job_name}'"
1412
+ response_json = @client.api_get_request("/job/#{job_name}/promotion")
1413
+
1414
+ response_json["processes"].each do |promotion|
1415
+ @logger.info "Getting promotion details of '#{promotion['name']}'"
1416
+
1417
+ if promotion['color'] == 'notbuilt'
1418
+ result[promotion['name']] = nil
1419
+ else
1420
+ promo_json = @client.api_get_request("/job/#{job_name}/promotion/latest/#{promotion['name']}")
1421
+ result[promotion['name']] = promo_json['target']['number']
1422
+ end
1423
+ end
1424
+
1425
+ result
1426
+ end
1427
+
1182
1428
  private
1183
1429
 
1184
1430
  # Obtains the threshold params used by jenkins in the XML file