182 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			182 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Ruby
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/env ruby
 | |
| # frozen_string_literal: true
 | |
| 
 | |
| require 'optparse'
 | |
| require 'time'
 | |
| require 'fileutils'
 | |
| require 'uri'
 | |
| require 'net/http'
 | |
| require 'json'
 | |
| require_relative 'api/default_options'
 | |
| 
 | |
| # Request list of pipelines for MR
 | |
| # https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/69053/pipelines
 | |
| # Find latest failed pipeline
 | |
| # Retrieve list of failed builds for test stage in pipeline
 | |
| # https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/pipelines/363788864/jobs/?scope=failed
 | |
| # Retrieve test reports for these builds
 | |
| # https://gitlab.com/gitlab-org/gitlab/-/pipelines/363788864/tests/suite.json?build_ids[]=1555608749
 | |
| # Push into expected format for failed tests
 | |
| class PipelineTestReportBuilder
 | |
|   DEFAULT_OPTIONS = {
 | |
|     target_project: Host::DEFAULT_OPTIONS[:target_project] || API::DEFAULT_OPTIONS[:project],
 | |
|     current_pipeline_id: API::DEFAULT_OPTIONS[:pipeline_id],
 | |
|     mr_iid: Host::DEFAULT_OPTIONS[:mr_iid],
 | |
|     api_endpoint: API::DEFAULT_OPTIONS[:endpoint],
 | |
|     output_file_path: 'test_results/test_reports.json',
 | |
|     pipeline_index: :previous
 | |
|   }.freeze
 | |
| 
 | |
|   def initialize(options)
 | |
|     @target_project = options.delete(:target_project)
 | |
|     @current_pipeline_id = options.delete(:current_pipeline_id)
 | |
|     @mr_iid = options.delete(:mr_iid)
 | |
|     @api_endpoint = options.delete(:api_endpoint).to_s
 | |
|     @output_file_path = options.delete(:output_file_path).to_s
 | |
|     @pipeline_index = options.delete(:pipeline_index).to_sym
 | |
|   end
 | |
| 
 | |
|   def execute
 | |
|     FileUtils.mkdir_p(File.dirname(output_file_path))
 | |
| 
 | |
|     File.open(output_file_path, 'w') do |file|
 | |
|       file.write(test_report_for_pipeline)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def test_report_for_pipeline
 | |
|     build_test_report_json_for_pipeline
 | |
|   end
 | |
| 
 | |
|   def latest_pipeline
 | |
|     fetch("#{target_project_api_base_url}/pipelines/#{current_pipeline_id}")
 | |
|   end
 | |
| 
 | |
|   def previous_pipeline
 | |
|     # Top of the list will always be the latest pipeline
 | |
|     # Second from top will be the previous pipeline
 | |
|     pipelines_sorted_descending[1]
 | |
|   end
 | |
| 
 | |
|   private
 | |
| 
 | |
|   attr_reader :target_project, :current_pipeline_id, :mr_iid, :api_endpoint, :output_file_path, :pipeline_index
 | |
| 
 | |
|   def pipeline
 | |
|     @pipeline ||=
 | |
|       case pipeline_index
 | |
|       when :latest
 | |
|         latest_pipeline
 | |
|       when :previous
 | |
|         previous_pipeline
 | |
|       else
 | |
|         raise "[PipelineTestReportBuilder] Unsupported pipeline_index `#{pipeline_index}` (allowed index: `latest` and `previous`!"
 | |
|       end
 | |
|   end
 | |
| 
 | |
|   def pipelines_sorted_descending
 | |
|     # Top of the list will always be the current pipeline
 | |
|     # Second from top will be the previous pipeline
 | |
|     pipelines_for_mr.sort_by { |a| -a['id'] }
 | |
|   end
 | |
| 
 | |
|   def pipeline_project_api_base_url(pipeline)
 | |
|     "#{api_endpoint}/projects/#{pipeline['project_id']}"
 | |
|   end
 | |
| 
 | |
|   def target_project_api_base_url
 | |
|     "#{api_endpoint}/projects/#{target_project}"
 | |
|   end
 | |
| 
 | |
|   def pipelines_for_mr
 | |
|     @pipelines_for_mr ||= fetch("#{target_project_api_base_url}/merge_requests/#{mr_iid}/pipelines")
 | |
|   end
 | |
| 
 | |
|   def failed_builds_for_pipeline
 | |
|     fetch("#{pipeline_project_api_base_url(pipeline)}/pipelines/#{pipeline['id']}/jobs?scope=failed&per_page=100")
 | |
|   end
 | |
| 
 | |
|   # Method uses the test suite endpoint to gather test results for a particular build.
 | |
|   # Here we request individual builds, even though it is possible to supply multiple build IDs.
 | |
|   # The reason for this; it is possible to lose the job context and name when requesting multiple builds.
 | |
|   # Please see for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69053#note_709939709
 | |
|   def test_report_for_build(pipeline_url, build_id)
 | |
|     fetch("#{pipeline_url}/tests/suite.json?build_ids[]=#{build_id}").tap do |suite|
 | |
|       suite['job_url'] = job_url(pipeline_url, build_id)
 | |
|     end
 | |
|   rescue Net::HTTPServerException => e
 | |
|     raise e unless e.response.code.to_i == 404
 | |
| 
 | |
|     puts "[PipelineTestReportBuilder] Artifacts not found. They may have expired. Skipping this build."
 | |
|   end
 | |
| 
 | |
|   def build_test_report_json_for_pipeline
 | |
|     # empty file if no previous failed pipeline
 | |
|     return {}.to_json if pipeline.nil?
 | |
| 
 | |
|     test_report = { 'suites' => [] }
 | |
| 
 | |
|     puts "[PipelineTestReportBuilder] Discovered #{pipeline_index} failed pipeline (##{pipeline['id']}) for MR!#{mr_iid}"
 | |
| 
 | |
|     failed_builds_for_pipeline.each do |failed_build|
 | |
|       next if failed_build['stage'] != 'test'
 | |
| 
 | |
|       test_report['suites'] << test_report_for_build(pipeline['web_url'], failed_build['id'])
 | |
|     end
 | |
| 
 | |
|     test_report['suites'].compact!
 | |
| 
 | |
|     puts "[PipelineTestReportBuilder] #{test_report['suites'].size} failed builds in test stage found..."
 | |
| 
 | |
|     test_report.to_json
 | |
|   end
 | |
| 
 | |
|   def job_url(pipeline_url, build_id)
 | |
|     pipeline_url.sub(%r{/pipelines/.+}, "/jobs/#{build_id}")
 | |
|   end
 | |
| 
 | |
|   def fetch(uri_str)
 | |
|     uri = URI(uri_str)
 | |
| 
 | |
|     puts "[PipelineTestReportBuilder] URL: #{uri}"
 | |
| 
 | |
|     request = Net::HTTP::Get.new(uri)
 | |
| 
 | |
|     body = ''
 | |
| 
 | |
|     Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
 | |
|       http.request(request) do |response|
 | |
|         case response
 | |
|         when Net::HTTPSuccess
 | |
|           body = response.read_body
 | |
|         else
 | |
|           raise "[PipelineTestReportBuilder] Unexpected response: #{response.value}"
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     JSON.parse(body)
 | |
|   end
 | |
| end
 | |
| 
 | |
| if $PROGRAM_NAME == __FILE__
 | |
|   options = PipelineTestReportBuilder::DEFAULT_OPTIONS.dup
 | |
| 
 | |
|   OptionParser.new do |opts|
 | |
|     opts.on("-o", "--output-file-path OUTPUT_PATH", String, "A path for output file") do |value|
 | |
|       options[:output_file_path] = value
 | |
|     end
 | |
| 
 | |
|     opts.on("-p", "--pipeline-index [latest|previous]", String, "What pipeline to retrieve (defaults to `#{PipelineTestReportBuilder::DEFAULT_OPTIONS[:pipeline_index]}`)") do |value|
 | |
|       options[:pipeline_index] = value
 | |
|     end
 | |
| 
 | |
|     opts.on("-h", "--help", "Prints this help") do
 | |
|       puts opts
 | |
|       exit
 | |
|     end
 | |
|   end.parse!
 | |
| 
 | |
|   PipelineTestReportBuilder.new(options).execute
 | |
| end
 |