# frozen_string_literal: true require 'set' module Gitlab module QA module Report class ReportAsIssue MAX_TITLE_LENGTH = 255 def initialize(token:, input_files:, project: nil, dry_run: false, **kwargs) @project = project @gitlab = (dry_run ? GitlabIssueDryClient : GitlabIssueClient).new(token: token, project: project) @files = Array(input_files) end def invoke! validate_input! run! end private attr_reader :gitlab, :files, :project, :issue_type def run! raise NotImplementedError end def new_issue_title(test) "#{partial_file_path(test.file)} | #{search_safe(test.name)}".strip end def new_issue_description(test) "### Full description\n\n#{search_safe(test.name)}\n\n### File path\n\n#{test.file}" end def new_issue_labels(test) [] end def validate_input! assert_project! assert_input_files!(files) gitlab.assert_user_permission! end def assert_project! return if project abort "Please provide a valid project ID or path with the `-p/--project` option!" end def assert_input_files!(files) return if Dir.glob(files).any? abort "Please provide valid JUnit report files. No files were found matching `#{files.join(',')}`" end def test_results_per_file Dir.glob(files).each do |path| extension = File.extname(path) test_results = case extension when '.json' Report::JsonTestResults.new(path) when '.xml' Report::JUnitTestResults.new(path) else raise "Unknown extension #{extension}" end yield test_results end end def create_issue(test) issue = gitlab.create_issue( title: title_from_test(test), description: new_issue_description(test), labels: new_issue_labels(test).to_a, issue_type: issue_type ) new_link = issue_type == 'test_case' ? issue.web_url.sub('/issues/', '/quality/test_cases/') : issue.web_url puts "Created new #{issue_type}: #{new_link}" issue end def issue_labels(issue) issue&.labels&.to_set || Set.new end def update_labels(issue, test, new_labels = Set.new) labels = up_to_date_labels(test: test, issue: issue, new_labels: new_labels) return if issue_labels(issue) == labels gitlab.edit_issue(iid: issue.iid, options: { labels: labels.to_a }) end def up_to_date_labels(test:, issue: nil, new_labels: Set.new) labels = issue_labels(issue) labels |= new_labels ee_test?(test) ? labels << "Enterprise Edition" : labels.delete("Enterprise Edition") quarantine_job? ? labels << "quarantine" : labels.delete("quarantine") labels end def pipeline_name_label case pipeline when 'production' 'found:gitlab.com' when 'canary', 'staging' "found:#{pipeline}.gitlab.com" when 'staging-canary' "found:canary.staging.gitlab.com" when 'preprod' 'found:pre.gitlab.com' when 'nightly', QA::Runtime::Env.default_branch, 'staging-ref', 'release' "found:#{pipeline}" else raise "No `found:*` label for the `#{pipeline}` pipeline!" end end def ee_test?(test) test.file =~ %r{features/ee/(api|browser_ui)} end def quarantine_job? Runtime::Env.ci_job_name&.include?('quarantine') end def partial_file_path(path) path.match(/((api|browser_ui).*)/i)[1] end def title_from_test(test) title = new_issue_title(test) return title unless title.length > MAX_TITLE_LENGTH "#{title[0...MAX_TITLE_LENGTH - 3]}..." end def search_safe(value) value.delete('"') end def pipeline # Gets the name of the pipeline the test was run in, to be used as the key of a scoped label # # Tests can be run in several pipelines: # gitlab-qa, nightly, staging, canary, production, preprod, MRs, and the default branch (master/main) # # Some of those run in their own project, so CI_PROJECT_NAME is the name we need. Those are: # nightly, staging, canary, production, and preprod # # MR, master/main, and gitlab-qa tests run in gitlab-qa, but we only want to report tests run on # master/main because the other pipelines will be monitored by the author of the MR that triggered them. # So we assume that we're reporting a master/main pipeline if the project name is 'gitlab-qa'. @pipeline ||= Runtime::Env.pipeline_from_project_name end end end end end