jenkins_api_client 1.0.0.alpha.2 → 1.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -400,7 +406,7 @@ 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
@@ -411,7 +417,7 @@ module JenkinsApi
411
417
  #
412
418
  def delete(job_name)
413
419
  @logger.info "Deleting job '#{job_name}'"
414
- @client.api_post_request("/job/#{job_name}/doDelete")
420
+ @client.api_post_request("/job/#{path_encode job_name}/doDelete")
415
421
  end
416
422
 
417
423
  # Deletes all jobs from Jenkins
@@ -432,7 +438,7 @@ module JenkinsApi
432
438
  #
433
439
  def wipe_out_workspace(job_name)
434
440
  @logger.info "Wiping out the workspace of job '#{job_name}'"
435
- @client.api_post_request("/job/#{job_name}/doWipeOutWorkspace")
441
+ @client.api_post_request("/job/#{path_encode job_name}/doWipeOutWorkspace")
436
442
  end
437
443
 
438
444
  # Stops a running build of a job
@@ -449,10 +455,10 @@ module JenkinsApi
449
455
  @logger.info "Stopping job '#{job_name}' Build ##{build_number}"
450
456
  # Check and see if the build is running
451
457
  is_building = @client.api_get_request(
452
- "/job/#{job_name}/#{build_number}"
458
+ "/job/#{path_encode job_name}/#{build_number}"
453
459
  )["building"]
454
460
  if is_building
455
- @client.api_post_request("/job/#{job_name}/#{build_number}/stop")
461
+ @client.api_post_request("/job/#{path_encode job_name}/#{build_number}/stop")
456
462
  end
457
463
  end
458
464
  alias_method :stop, :stop_build
@@ -515,7 +521,7 @@ module JenkinsApi
515
521
  else
516
522
  raise "Mode should either be 'text' or 'html'. You gave: #{mode}"
517
523
  end
518
- get_msg = "/job/#{job_name}/#{build_num}/logText/progressive#{mode}?"
524
+ get_msg = "/job/#{path_encode job_name}/#{build_num}/logText/progressive#{mode}?"
519
525
  get_msg << "start=#{start}"
520
526
  raw_response = true
521
527
  api_response = @client.api_get_request(get_msg, nil, nil, raw_response)
@@ -610,7 +616,7 @@ module JenkinsApi
610
616
  #
611
617
  def list_details(job_name)
612
618
  @logger.info "Obtaining the details of '#{job_name}'"
613
- @client.api_get_request("/job/#{job_name}")
619
+ @client.api_get_request("/job/#{path_encode job_name}")
614
620
  end
615
621
 
616
622
  # List upstream projects of a specific job
@@ -620,7 +626,7 @@ module JenkinsApi
620
626
  #
621
627
  def get_upstream_projects(job_name)
622
628
  @logger.info "Obtaining the upstream projects of '#{job_name}'"
623
- response_json = @client.api_get_request("/job/#{job_name}")
629
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
624
630
  response_json["upstreamProjects"]
625
631
  end
626
632
 
@@ -631,7 +637,7 @@ module JenkinsApi
631
637
  #
632
638
  def get_downstream_projects(job_name)
633
639
  @logger.info "Obtaining the down stream projects of '#{job_name}'"
634
- response_json = @client.api_get_request("/job/#{job_name}")
640
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
635
641
  response_json["downstreamProjects"]
636
642
  end
637
643
 
@@ -641,7 +647,7 @@ module JenkinsApi
641
647
  #
642
648
  def get_builds(job_name)
643
649
  @logger.info "Obtaining the build details of '#{job_name}'"
644
- response_json = @client.api_get_request("/job/#{job_name}")
650
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
645
651
  response_json["builds"]
646
652
  end
647
653
 
@@ -683,7 +689,7 @@ module JenkinsApi
683
689
  #
684
690
  def get_current_build_status(job_name)
685
691
  @logger.info "Obtaining the current build status of '#{job_name}'"
686
- response_json = @client.api_get_request("/job/#{job_name}")
692
+ response_json = @client.api_get_request("/job/#{path_encode job_name}")
687
693
  color_to_status(response_json["color"])
688
694
  end
689
695
  alias_method :status, :get_current_build_status
@@ -697,61 +703,248 @@ module JenkinsApi
697
703
  #
698
704
  def get_current_build_number(job_name)
699
705
  @logger.info "Obtaining the current build number of '#{job_name}'"
700
- @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
701
707
  end
702
708
  alias_method :build_number, :get_current_build_number
703
709
 
704
- # Build a job given the name of the job
705
- # You can optionally pass in a list of params for Jenkins to use for
706
- # parameterized builds
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
707
742
  #
708
743
  # @param [String] job_name the name of the job
709
- # @param [Hash] params the parameters for parameterized builds
710
- # @param [Boolean] return_build_number whether to wait and obtain the build
711
- # number
712
- #
713
- # @return [String, Integer] the response code from the build POST request
714
- # if return_build_number is not requested and the build number if the
715
- # return_build_number is requested. nil will be returned if the build
716
- # number is requested and not available. This can happen if there is
717
- # already a job in the queue and concurrent build is disabled.
718
- #
719
- 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
+
720
771
  msg = "Building job '#{job_name}'"
721
772
  msg << " with parameters: #{params.inspect}" unless params.empty?
722
773
  @logger.info msg
723
- build_endpoint = params.empty? ? "build" : "buildWithParameters"
724
- raw_response = return_build_number
725
- response =@client.api_post_request(
726
- "/job/#{job_name}/#{build_endpoint}",
727
- params,
728
- raw_response
729
- )
730
- # 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
731
806
  # header and wait till the build is moved to one of the executors and a
732
807
  # build number is assigned
733
- if return_build_number
734
- if response["location"]
735
- task_id_match = response["location"].match(/\/item\/(\d*)\//)
736
- task_id = task_id_match.nil? ? nil : task_id_match[1]
737
- unless task_id.nil?
738
- @logger.debug "Queue task ID for job '#{job_name}': #{task_id}"
739
- Timeout::timeout(@client.timeout) do
740
- while @client.queue.get_item_by_id(task_id)["executable"].nil?
741
- 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
742
851
  end
743
852
  end
744
- @client.queue.get_item_by_id(task_id)["executable"]["number"]
745
- else
746
- 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
747
880
  end
748
881
  else
749
- 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)
750
884
  end
751
885
  else
752
- 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)
753
888
  end
754
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_number, 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_number} to start. #{e.class} #{e}"
944
+ raise
945
+ end
946
+ end
947
+ private :get_build_id_the_old_way
755
948
 
756
949
  # Programatically schedule SCM polling for the specified job
757
950
  #
@@ -779,7 +972,7 @@ module JenkinsApi
779
972
  #
780
973
  def disable(job_name)
781
974
  @logger.info "Disabling job '#{job_name}'"
782
- @client.api_post_request("/job/#{job_name}/disable")
975
+ @client.api_post_request("/job/#{path_encode job_name}/disable")
783
976
  end
784
977
 
785
978
  # Obtain the configuration stored in config.xml of a specific job
@@ -790,7 +983,7 @@ module JenkinsApi
790
983
  #
791
984
  def get_config(job_name)
792
985
  @logger.info "Obtaining the config.xml of '#{job_name}'"
793
- @client.get_config("/job/#{job_name}")
986
+ @client.get_config("/job/#{path_encode job_name}")
794
987
  end
795
988
 
796
989
  # Post the configuration of a job given the job name and the config.xml
@@ -802,7 +995,7 @@ module JenkinsApi
802
995
  #
803
996
  def post_config(job_name, xml)
804
997
  @logger.info "Posting the config.xml of '#{job_name}'"
805
- @client.post_config("/job/#{job_name}/config.xml", xml)
998
+ @client.post_config("/job/#{path_encode job_name}/config.xml", xml)
806
999
  end
807
1000
 
808
1001
  # Obtain the test results for a specific build of a job
@@ -814,7 +1007,7 @@ module JenkinsApi
814
1007
  build_num = get_current_build_number(job_name) if build_num == 0
815
1008
  @logger.info "Obtaining the test results of '#{job_name}'" +
816
1009
  " Build ##{build_num}"
817
- @client.api_get_request("/job/#{job_name}/#{build_num}/testReport")
1010
+ @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/testReport")
818
1011
  rescue Exceptions::NotFound
819
1012
  # Not found is acceptable, as not all builds will have test results
820
1013
  # and this is what jenkins throws at us in that case
@@ -831,7 +1024,7 @@ module JenkinsApi
831
1024
  @logger.info "Obtaining the build details of '#{job_name}'" +
832
1025
  " Build ##{build_num}"
833
1026
 
834
- @client.api_get_request("/job/#{job_name}/#{build_num}/")
1027
+ @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/")
835
1028
  end
836
1029
 
837
1030
  # Change the description of a specific job
@@ -1207,6 +1400,31 @@ module JenkinsApi
1207
1400
  filtered_job_names[0..parallel-1]
1208
1401
  end
1209
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
+
1210
1428
  private
1211
1429
 
1212
1430
  # Obtains the threshold params used by jenkins in the XML file