lib/gitlab/qa/report/gitlab_issue_client.rb



# frozen_string_literal: true

require 'gitlab'

module Gitlab
  # Monkey patch the Gitlab client to use the correct API path and add required methods
  class Client
    def team_member(project, id)
      get("/projects/#{url_encode(project)}/members/all/#{id}")
    end

    def issue_discussions(project, issue_id, options = {})
      get("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions", query: options)
    end

    def add_note_to_issue_discussion_as_thread(project, issue_id, discussion_id, options = {})
      post("/projects/#{url_encode(project)}/issues/#{issue_id}/discussions/#{discussion_id}/notes", query: options)
    end
  end

  module QA
    module Report
      # The GitLab client is used for API access: https://github.com/NARKOZ/gitlab
      class GitlabIssueClient
        MAINTAINER_ACCESS_LEVEL = 40
        RETRY_BACK_OFF_DELAY = 60
        MAX_RETRY_ATTEMPTS = 3

        def initialize(token:, project:)
          @token = token
          @project = project
          @retry_backoff = 0

          configure_gitlab_client
        end

        def assert_user_permission!
          handle_gitlab_client_exceptions do
            user = Gitlab.user
            member = Gitlab.team_member(project, user.id)

            abort_not_permitted if member.access_level < MAINTAINER_ACCESS_LEVEL
          end
        rescue Gitlab::Error::NotFound
          abort_not_permitted
        end

        def find_issues(iid: nil, options: {}, &select)
          select ||= :itself

          handle_gitlab_client_exceptions do
            return [Gitlab.issue(project, iid)].select(&select) if iid

            Gitlab.issues(project, options)
              .auto_paginate
              .select(&select)
          end
        end

        def find_issue_discussions(iid:)
          handle_gitlab_client_exceptions do
            Gitlab.issue_discussions(project, iid, order_by: 'created_at', sort: 'asc').auto_paginate
          end
        end

        def create_issue(title:, description:, labels:, issue_type: 'issue')
          attrs = { issue_type: issue_type, description: description, labels: labels }

          handle_gitlab_client_exceptions do
            Gitlab.create_issue(project, title, attrs)
          end
        end

        def edit_issue(iid:, options: {})
          handle_gitlab_client_exceptions do
            Gitlab.edit_issue(project, iid, options)
          end
        end

        def find_issue_notes(iid:)
          handle_gitlab_client_exceptions do
            Gitlab.issue_notes(project, iid, order_by: 'created_at', sort: 'asc')&.auto_paginate
          end
        end

        def create_issue_note(iid:, note:)
          handle_gitlab_client_exceptions do
            Gitlab.create_issue_note(project, iid, note)
          end
        end

        def edit_issue_note(issue_iid:, note_id:, note:)
          handle_gitlab_client_exceptions do
            Gitlab.edit_issue_note(project, issue_iid, note_id, note)
          end
        end

        def add_note_to_issue_discussion_as_thread(iid:, discussion_id:, body:)
          handle_gitlab_client_exceptions do
            Gitlab.add_note_to_issue_discussion_as_thread(project, iid, discussion_id, body: body)
          end
        end

        def find_user_id(username:)
          handle_gitlab_client_exceptions do
            user = Gitlab.users(username: username)&.first
            user['id'] unless user.nil?
          end
        end

        def upload_file(file_fullpath:)
          ignore_gitlab_client_exceptions do
            Gitlab.upload_file(project, file_fullpath)
          end
        end

        def ignore_gitlab_client_exceptions
          yield
        rescue StandardError, SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout, Gitlab::Error::Error => e
          puts "Ignoring the following error: #{e}"
        end

        def handle_gitlab_client_exceptions
          yield
        rescue Gitlab::Error::NotFound
          # This error could be raised in assert_user_permission!
          # If so, we want it to terminate at that point
          raise
        rescue SystemCallError, OpenSSL::SSL::SSLError, Net::OpenTimeout, Net::ReadTimeout, Gitlab::Error::InternalServerError, Gitlab::Error::Parsing => e
          @retry_backoff += RETRY_BACK_OFF_DELAY

          raise if @retry_backoff > RETRY_BACK_OFF_DELAY * MAX_RETRY_ATTEMPTS

          warn_exception(e)
          warn("Sleeping for #{@retry_backoff} seconds before retrying...")
          sleep @retry_backoff

          retry
        rescue StandardError => e
          pipeline = QA::Runtime::Env.pipeline_from_project_name
          channel = case pipeline
                    when "canary"
                      "qa-production"
                    when "staging-canary"
                      "qa-staging"
                    else
                      "qa-#{pipeline}"
                    end
          error_msg = warn_exception(e)

          return unless QA::Runtime::Env.ci_commit_ref_name == QA::Runtime::Env.default_branch

          slack_options = {
            channel: channel,
            icon_emoji: ':ci_failing:',
            message: <<~MSG
              An unexpected error occurred while reporting test results in issues.
              The error occurred in job: #{QA::Runtime::Env.ci_job_url}
              `#{error_msg}`
            MSG
          }
          puts "Posting Slack message to channel: #{channel}"

          Gitlab::QA::Slack::PostToSlack.new(**slack_options).invoke!
        end

        private

        attr_reader :token, :project

        def configure_gitlab_client
          Gitlab.configure do |config|
            config.endpoint = Runtime::Env.gitlab_api_base
            config.private_token = token
          end
        end

        def abort_not_permitted
          abort "You must have at least Maintainer access to the project to use this feature."
        end

        def warn_exception(error)
          error_msg = "#{error.class.name} #{error.message}"
          warn(error_msg)
          error_msg
        end
      end
    end
  end
end