lib/gitlab/qa/report/report_as_issue.rb



# 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

        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)
          gitlab.create_issue(
            title: title_from_test(test),
            description: new_issue_description(test),
            labels: new_issue_labels(test).to_a
          )
        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 'staging-orchestrated', '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