diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 5dc156fbcaa6d63f74339357b769c05f4073ed80..edacbee8cabc90da707ddf51fa1122d49b5dcf22 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -555,6 +555,7 @@
       when: never
     - <<: *if-merge-request
       changes: *db-patterns
+      when: manual
 
 .rails:rules:ee-and-foss-unit:
   rules:
diff --git a/.gitlab/ci/workhorse.gitlab-ci.yml b/.gitlab/ci/workhorse.gitlab-ci.yml
index 8361d20d2b79c68a4d53dd749650ef3b556bec04..ba4523f3bf75cd8b39d08552a736681bc8cd38b3 100644
--- a/.gitlab/ci/workhorse.gitlab-ci.yml
+++ b/.gitlab/ci/workhorse.gitlab-ci.yml
@@ -1,6 +1,6 @@
 workhorse:verify:
   extends: .workhorse:rules:workhorse
-  image: ${GITLAB_DEPENDENCY_PROXY}golang:1.15
+  image: ${GITLAB_DEPENDENCY_PROXY}golang:1.16
   stage: test
   needs: []
   script:
@@ -23,14 +23,10 @@ workhorse:verify:
     - apt-get update && apt-get -y install libimage-exiftool-perl
     - make -C workhorse test
 
-workhorse:test using go 1.13:
-  extends: .workhorse:test
-  image: ${GITLAB_DEPENDENCY_PROXY}golang:1.13
-
-workhorse:test using go 1.14:
-  extends: .workhorse:test
-  image: ${GITLAB_DEPENDENCY_PROXY}golang:1.14
-
 workhorse:test using go 1.15:
   extends: .workhorse:test
   image: ${GITLAB_DEPENDENCY_PROXY}golang:1.15
+
+workhorse:test using go 1.16:
+  extends: .workhorse:test
+  image: ${GITLAB_DEPENDENCY_PROXY}golang:1.16
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index 8964f55d9e90002feae229e016b634ff9e3c056f..462896509eefdc6d44db3338ed192eee97de2bcf 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -431,8 +431,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
     - ee/spec/controllers/projects/merge_requests_controller_spec.rb
     - ee/spec/controllers/projects/mirrors_controller_spec.rb
     - ee/spec/controllers/projects/threat_monitoring_controller_spec.rb
-    - ee/spec/controllers/registrations/groups_controller_spec.rb
-    - ee/spec/controllers/registrations/projects_controller_spec.rb
     - ee/spec/controllers/subscriptions_controller_spec.rb
     - ee/spec/features/boards/group_boards/multiple_boards_spec.rb
     - ee/spec/features/ci_shared_runner_warnings_spec.rb
@@ -932,9 +930,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
     - spec/services/audit_event_service_spec.rb
     - spec/services/auth/dependency_proxy_authentication_service_spec.rb
     - spec/services/auto_merge_service_spec.rb
-    - spec/services/award_emojis/add_service_spec.rb
-    - spec/services/award_emojis/destroy_service_spec.rb
-    - spec/services/award_emojis/toggle_service_spec.rb
     - spec/services/boards/destroy_service_spec.rb
     - spec/services/boards/issues/move_service_spec.rb
     - spec/services/bulk_create_integration_service_spec.rb
@@ -968,9 +963,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
     - spec/services/design_management/save_designs_service_spec.rb
     - spec/services/discussions/resolve_service_spec.rb
     - spec/services/discussions/unresolve_service_spec.rb
-    - spec/services/environments/auto_stop_service_spec.rb
-    - spec/services/environments/canary_ingress/update_service_spec.rb
-    - spec/services/environments/reset_auto_stop_service_spec.rb
     - spec/services/feature_flags/create_service_spec.rb
     - spec/services/feature_flags/destroy_service_spec.rb
     - spec/services/feature_flags/disable_service_spec.rb
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 8170a1f8443670609842247303c665a5910bf87c..cadcab16f16afbee600d0e780443d25f7951b7a8 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -7,6 +7,7 @@ const PERSISTENT_USER_CALLOUTS = [
   '.js-buy-pipeline-minutes-notification-callout',
   '.js-token-expiry-callout',
   '.js-registration-enabled-callout',
+  '.js-service-templates-deprecated-callout',
   '.js-new-user-signups-cap-reached',
   '.js-eoa-bronze-plan-banner',
 ];
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 10c7b4032cf938d64b9f542007cc3e2283b57423..7bf3cb6230b7ea91539fdd45a7e823638b382f0d 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -190,12 +190,8 @@ def diff_file_changed_icon_color(diff_file)
   def render_overflow_warning?(diffs_collection)
     diff_files = diffs_collection.raw_diff_files
 
-    if diff_files.any?(&:too_large?)
-      Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits)
-    end
-
     diff_files.overflow?.tap do |overflown|
-      Gitlab::Metrics.add_event(:diffs_overflow_collection_limits) if overflown
+      log_overflow_limits(diff_files)
     end
   end
 
@@ -286,4 +282,18 @@ def conflicts
 
     conflicts_service.conflicts.files.index_by(&:our_path)
   end
+
+  def log_overflow_limits(diff_files)
+    if diff_files.any?(&:too_large?)
+      Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits)
+    end
+
+    Gitlab::Metrics.add_event(:diffs_overflow_collection_limits) if diff_files.overflow?
+    Gitlab::Metrics.add_event(:diffs_overflow_max_bytes_limits) if diff_files.overflow_max_bytes?
+    Gitlab::Metrics.add_event(:diffs_overflow_max_files_limits) if diff_files.overflow_max_files?
+    Gitlab::Metrics.add_event(:diffs_overflow_max_lines_limits) if diff_files.overflow_max_lines?
+    Gitlab::Metrics.add_event(:diffs_overflow_collapsed_bytes_limits) if diff_files.collapsed_safe_bytes?
+    Gitlab::Metrics.add_event(:diffs_overflow_collapsed_files_limits) if diff_files.collapsed_safe_files?
+    Gitlab::Metrics.add_event(:diffs_overflow_collapsed_lines_limits) if diff_files.collapsed_safe_lines?
+  end
 end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index d1db624432b45eb944393f0854b407679602ccab..7a90984cd777ee29f296100aa0f9a0723b8ea16f 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -5,7 +5,7 @@ module UserCalloutsHelper
   GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
   GCP_SIGNUP_OFFER = 'gcp_signup_offer'
   SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
-  SERVICE_TEMPLATES_DEPRECATED = 'service_templates_deprecated'
+  SERVICE_TEMPLATES_DEPRECATED_CALLOUT = 'service_templates_deprecated_callout'
   TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
   WEBHOOKS_MOVED = 'webhooks_moved'
   CUSTOMIZE_HOMEPAGE = 'customize_homepage'
@@ -41,8 +41,11 @@ def show_suggest_popover?
     !user_dismissed?(SUGGEST_POPOVER_DISMISSED)
   end
 
-  def show_service_templates_deprecated?
-    !user_dismissed?(SERVICE_TEMPLATES_DEPRECATED)
+  def show_service_templates_deprecated_callout?
+    !Gitlab.com? &&
+    current_user&.admin? &&
+    Service.for_template.active.exists? &&
+    !user_dismissed?(SERVICE_TEMPLATES_DEPRECATED_CALLOUT)
   end
 
   def show_webhooks_moved_alert?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 292d249cb23174b5dd295d03457be25322863833..c89a132bdd6d6d03f1c45136c565f0d1d8b4863f 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -179,18 +179,6 @@ class CommitStatus < ApplicationRecord
         ExpireJobCacheWorker.perform_async(id)
       end
     end
-
-    after_transition any => :failed do |commit_status|
-      next if Feature.enabled?(:async_add_build_failure_todo, commit_status.project, default_enabled: :yaml)
-      next unless commit_status.project
-
-      # rubocop: disable CodeReuse/ServiceClass
-      commit_status.run_after_commit do
-        MergeRequests::AddTodoWhenBuildFailsService
-          .new(project, nil).execute(self)
-      end
-      # rubocop: enable CodeReuse/ServiceClass
-    end
   end
 
   def self.names
diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb
index 37bbb9b875239956ffbc57b30a2304954dc939e5..d7adf63fde4cc6dcfa9ef6590e8f7c5457509d60 100644
--- a/app/models/project_services/discord_service.rb
+++ b/app/models/project_services/discord_service.rb
@@ -3,6 +3,8 @@
 require "discordrb/webhooks"
 
 class DiscordService < ChatNotificationService
+  include ActionView::Helpers::UrlHelper
+
   ATTACHMENT_REGEX = /: (?<entry>.*?)\n - (?<name>.*)\n*/.freeze
 
   def title
@@ -10,7 +12,7 @@ def title
   end
 
   def description
-    s_("DiscordService|Receive event notifications in Discord")
+    s_("DiscordService|Send notifications about project events to a Discord channel.")
   end
 
   def self.to_param
@@ -18,13 +20,8 @@ def self.to_param
   end
 
   def help
-    "This service sends notifications about project events to Discord channels.<br />
-    To set up this service:
-    <ol>
-      <li><a href='https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks'>Setup a custom Incoming Webhook</a>.</li>
-      <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
-      <li>Select events below to enable notifications.</li>
-    </ol>"
+    docs_link = link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
+    s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
   end
 
   def event_field(event)
@@ -36,13 +33,12 @@ def default_channel_placeholder
   end
 
   def self.supported_events
-    %w[push issue confidential_issue merge_request note confidential_note tag_push
-       pipeline wiki_page]
+    %w[push issue confidential_issue merge_request note confidential_note tag_push pipeline wiki_page]
   end
 
   def default_fields
     [
-      { type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" },
+      { type: "text", name: "webhook", placeholder: "https://discordapp.com/api/webhooks/…", help: "URL to the webhook for the Discord channel." },
       { type: "checkbox", name: "notify_only_broken_pipelines" },
       { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
     ]
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index a1192ceaeda13ee24f53b6b56309faad12266e5f..34996b771a083d0f425659824e8e15d978dd3d23 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -17,7 +17,7 @@ class UserCallout < ApplicationRecord
     threat_monitoring_info: 11,                # EE-only
     account_recovery_regular_check: 12,        # EE-only
     webhooks_moved: 13,
-    service_templates_deprecated: 14,
+    service_templates_deprecated_callout: 14,
     admin_integrations_moved: 15,
     web_ide_alert_dismissed: 16,               # no longer in use
     active_user_count_threshold: 18,           # EE-only
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1f2fcd1c70bf2cfc338b6031cb32bdf6970d9f23..c91d27e3ed1fa0c7ae74040f3a2dc3b6596d6293 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -11,6 +11,7 @@
       = render "layouts/broadcast"
       = render "layouts/header/read_only_banner"
       = render "layouts/header/registration_enabled_callout"
+      = render "layouts/header/service_templates_deprecation_callout"
       = render "layouts/nav/classification_level_banner"
       = yield :flash_message
       = render "shared/ping_consent"
diff --git a/app/views/layouts/header/_service_templates_deprecation_callout.html.haml b/app/views/layouts/header/_service_templates_deprecation_callout.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..056d4426d5a4bb7799187dbc1bb6ab76c28d15c9
--- /dev/null
+++ b/app/views/layouts/header/_service_templates_deprecation_callout.html.haml
@@ -0,0 +1,21 @@
+- return unless show_service_templates_deprecated_callout?
+
+- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe
+- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
+
+%div{ class: [container_class, @content_class, 'gl-pt-5!'] }
+  .gl-alert.gl-alert-warning.js-service-templates-deprecated-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::SERVICE_TEMPLATES_DEPRECATED_CALLOUT, dismiss_endpoint: user_callouts_path } }
+    = sprite_icon('warning', size: 16, css_class: 'gl-alert-icon')
+    %button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-service-templates-deprecated-callout' } }
+      = sprite_icon('close', size: 16)
+    .gl-alert-title
+      = s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
+    .gl-alert-body
+      = html_escape_once(s_('AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings &gt; Integrations.%{link_end}')).html_safe % { doc_link_start: doc_link_start, settings_link_start: settings_link_start, link_end: '</a>'.html_safe }
+    .gl-alert-actions
+      = link_to admin_application_settings_services_path, class: 'btn gl-alert-action btn-info btn-md gl-button' do
+        %span.gl-button-text
+          = s_('AdminSettings|See affected service templates')
+      = link_to "https://gitlab.com/gitlab-org/gitlab/-/issues/325905", class: 'btn gl-alert-action btn-default btn-md gl-button', target: '_blank', rel: 'noopener noreferrer' do
+        %span.gl-button-text
+          = _('Leave feedback')
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index c9cfb1da10bc13f7ca91fa08431d7712afef2ee6..aeda8d113aca91d262a128236590fe22d0c66f38 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -37,7 +37,7 @@ def process_build(build)
     ExpirePipelineCacheWorker.perform_async(build.pipeline_id)
     ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat?
 
-    if build.failed? && Feature.enabled?(:async_add_build_failure_todo, build.project, default_enabled: :yaml)
+    if build.failed?
       ::Ci::MergeRequests::AddTodoWhenBuildFailsWorker.perform_async(build.id)
     end
 
diff --git a/changelogs/unreleased/31063-ensure-diff-collection-limits-are-configurable.yml b/changelogs/unreleased/31063-ensure-diff-collection-limits-are-configurable.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9e8918810c41d98544b2cfebd2d049d7e7b0fa36
--- /dev/null
+++ b/changelogs/unreleased/31063-ensure-diff-collection-limits-are-configurable.yml
@@ -0,0 +1,5 @@
+---
+title: Track the different overflows for diff collections
+merge_request: 57790
+author:
+type: other
diff --git a/changelogs/unreleased/325196-service-template-deprecation-update.yml b/changelogs/unreleased/325196-service-template-deprecation-update.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4aefb4a083abd35cc50f27e9321624a897e9ef71
--- /dev/null
+++ b/changelogs/unreleased/325196-service-template-deprecation-update.yml
@@ -0,0 +1,5 @@
+---
+title: Add global callout for Service template deprecation
+merge_request: 58613
+author:
+type: changed
diff --git a/changelogs/unreleased/325691-add-sync_code_onwers_approval_rules_worker.yml b/changelogs/unreleased/325691-add-sync_code_onwers_approval_rules_worker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..48f6211f02cf5daa624da3204659d408ad7652f8
--- /dev/null
+++ b/changelogs/unreleased/325691-add-sync_code_onwers_approval_rules_worker.yml
@@ -0,0 +1,5 @@
+---
+title: Add new MergeRequests::SyncCodeOwnerApprovalRulesWorker
+merge_request: 58512
+author:
+type: performance
diff --git a/changelogs/unreleased/ab-remove-async_add_build_failure_todo-feature-flag.yml b/changelogs/unreleased/ab-remove-async_add_build_failure_todo-feature-flag.yml
new file mode 100644
index 0000000000000000000000000000000000000000..76b5214d2782c2d48b293035189b1714f58c55f1
--- /dev/null
+++ b/changelogs/unreleased/ab-remove-async_add_build_failure_todo-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance by moving TODO creation out of the jobs/request path
+merge_request: 59022
+author:
+type: performance
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-award-emojis.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-award-emojis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5b846c00c08ab6a461e08258763e61b4f2e2e271
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-award-emojis.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/award_emojis
+merge_request: 58407
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-environments.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-environments.yml
new file mode 100644
index 0000000000000000000000000000000000000000..33c8ea69627352ea95fa538102e3f543c5ac9107
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-services-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/environments
+merge_request: 58418
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/pks-workhorse-go-1-16.yml b/changelogs/unreleased/pks-workhorse-go-1-16.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8404dd721b1d0339780abd203861ef2db7d09ac6
--- /dev/null
+++ b/changelogs/unreleased/pks-workhorse-go-1-16.yml
@@ -0,0 +1,5 @@
+---
+title: Bump minimum required Go version for workhorse to 1.15
+merge_request: 59347
+author:
+type: other
diff --git a/changelogs/unreleased/ui-text-discord-integration.yml b/changelogs/unreleased/ui-text-discord-integration.yml
new file mode 100644
index 0000000000000000000000000000000000000000..df25bf624b9837ceb75191403967ce1396af419e
--- /dev/null
+++ b/changelogs/unreleased/ui-text-discord-integration.yml
@@ -0,0 +1,5 @@
+---
+title: Update Discord integration UI text
+merge_request: 58842
+author:
+type: other
diff --git a/config/feature_flags/development/async_add_build_failure_todo.yml b/config/feature_flags/development/async_add_build_failure_todo.yml
deleted file mode 100644
index 3c1e056a50ecef277d34e1a5c83aeacf6307e055..0000000000000000000000000000000000000000
--- a/config/feature_flags/development/async_add_build_failure_todo.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: async_add_build_failure_todo
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57490/diffs
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326726
-milestone: '13.11'
-type: development
-group: group::continuous integration
-default_enabled: true
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7febb3770d1234d9f7d5506b3c1cba91633b0e4a..c0aab89fd4639ce2fc4c7ddae22299784f12bad0 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -220,6 +220,8 @@
   - 1
 - - merge_requests_resolve_todos
   - 1
+- - merge_requests_sync_code_owner_approval_rules
+  - 1
 - - metrics_dashboard_prune_old_annotations
   - 1
 - - metrics_dashboard_sync_dashboards
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 4ba1a85babcb1a3ec231b3d889423dd2b374de67..9f87192aab0862820fe0ceb52b6d6e4cae999b59 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -250,10 +250,11 @@ configuration option in `gitlab.yml`. These metrics are served from the
 
 The following metrics are available:
 
-| Metric                            | Type      | Since                                                         | Description                            |
-|:--------------------------------- |:--------- |:------------------------------------------------------------- |:-------------------------------------- |
-| `db_load_balancing_hosts`         | Gauge     | [12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/13630)     | Current number of load balancing hosts |
-
+| Metric                            | Type      | Since                                                         | Description                            | Labels                                                    |
+|:--------------------------------- |:--------- |:------------------------------------------------------------- |:-------------------------------------- |:--------------------------------------------------------- |
+| `db_load_balancing_hosts`         | Gauge     | [12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/13630)   | Current number of load balancing hosts | |
+| `sidekiq_load_balancing_count`    | Counter   | 13.11                                                         | Sidekiq jobs using load balancing with data consistency set to :sticky or :delayed | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency`, `data_consistency`, `database_chosen` |
+ 
 ## Database partitioning metrics **(PREMIUM SELF)**
 
 The following metrics are available:
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 8abeacd0d0e1225556f815043fff9d807d3e8f1c..4a3348e9609700c506b2666cca07d6be96e0d245 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -63,6 +63,12 @@ Returns [`ContainerRepositoryDetails`](#containerrepositorydetails).
 | ---- | ---- | ----------- |
 | `id` | [`ContainerRepositoryID!`](#containerrepositoryid) | The global ID of the container repository. |
 
+### `currentLicense`
+
+Fields related to the current license.
+
+Returns [`CurrentLicense`](#currentlicense).
+
 ### `currentUser`
 
 Get information about current user.
@@ -181,6 +187,21 @@ Returns [`Iteration`](#iteration).
 | ---- | ---- | ----------- |
 | `id` | [`IterationID!`](#iterationid) | Find an iteration by its ID. |
 
+### `licenseHistoryEntries`
+
+Fields related to entries in the license history.
+
+Returns [`LicenseHistoryEntryConnection`](#licensehistoryentryconnection).
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. |
+| `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. |
+| `first` | [`Int`](#int) | Returns the first _n_ elements from the list. |
+| `last` | [`Int`](#int) | Returns the last _n_ elements from the list. |
+
 ### `metadata`
 
 Metadata about GitLab.
@@ -1872,6 +1893,27 @@ Autogenerated return type of CreateTestCase.
 | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 | `testCase` | [`Issue`](#issue) | The test case created. |
 
+### `CurrentLicense`
+
+Represents the current license.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `activatedAt` | [`Date`](#date) | Date when the license was activated. |
+| `billableUsersCount` | [`Int`](#int) | Number of billable users on the system. |
+| `company` | [`String`](#string) | Company of the licensee. |
+| `email` | [`String`](#string) | Email of the licensee. |
+| `expiresAt` | [`Date`](#date) | Date when the license expires. |
+| `id` | [`ID!`](#id) | ID of the license. |
+| `lastSync` | [`Time`](#time) | Date when the license was last synced. |
+| `maximumUserCount` | [`Int`](#int) | Highest number of billable users on the system during the term of the current license. |
+| `name` | [`String`](#string) | Name of the licensee. |
+| `plan` | [`String!`](#string) | Name of the subscription plan. |
+| `startsAt` | [`Date`](#date) | Date when the license started. |
+| `type` | [`String!`](#string) | Type of the license. |
+| `usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. |
+| `usersOverLicenseCount` | [`Int`](#int) | Number of users over the paid users in the license. |
+
 ### `CustomEmoji`
 
 A custom emoji uploaded by user.
@@ -3874,6 +3916,42 @@ An edge in a connection.
 | `cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | `node` | [`Label`](#label) | The item at the end of the edge. |
 
+### `LicenseHistoryEntry`
+
+Represents an entry from the Cloud License history.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `activatedAt` | [`Date`](#date) | Date when the license was activated. |
+| `company` | [`String`](#string) | Company of the licensee. |
+| `email` | [`String`](#string) | Email of the licensee. |
+| `expiresAt` | [`Date`](#date) | Date when the license expires. |
+| `id` | [`ID!`](#id) | ID of the license. |
+| `name` | [`String`](#string) | Name of the licensee. |
+| `plan` | [`String!`](#string) | Name of the subscription plan. |
+| `startsAt` | [`Date`](#date) | Date when the license started. |
+| `type` | [`String!`](#string) | Type of the license. |
+| `usersInLicenseCount` | [`Int`](#int) | Number of paid users in the license. |
+
+### `LicenseHistoryEntryConnection`
+
+The connection type for LicenseHistoryEntry.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `edges` | [`[LicenseHistoryEntryEdge]`](#licensehistoryentryedge) | A list of edges. |
+| `nodes` | [`[LicenseHistoryEntry]`](#licensehistoryentry) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+### `LicenseHistoryEntryEdge`
+
+An edge in a connection.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`LicenseHistoryEntry`](#licensehistoryentry) | The item at the end of the edge. |
+
 ### `MarkAsSpamSnippetPayload`
 
 Autogenerated return type of MarkAsSpamSnippet.
@@ -8416,7 +8494,7 @@ Name of the feature that the callout is for.
 | `NEW_USER_SIGNUPS_CAP_REACHED` | Callout feature name for new_user_signups_cap_reached. |
 | `PERSONAL_ACCESS_TOKEN_EXPIRY` | Callout feature name for personal_access_token_expiry. |
 | `REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. |
-| `SERVICE_TEMPLATES_DEPRECATED` | Callout feature name for service_templates_deprecated. |
+| `SERVICE_TEMPLATES_DEPRECATED_CALLOUT` | Callout feature name for service_templates_deprecated_callout. |
 | `SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. |
 | `SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. |
 | `TABS_POSITION_HIGHLIGHT` | Callout feature name for tabs_position_highlight. |
diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md
index 624c0252f23635cf63d6c7f7b7fbd4ddb5bcd00f..2ec657eec22e0edc5d30672a333039153ef73d02 100644
--- a/doc/user/project/integrations/discord_notifications.md
+++ b/doc/user/project/integrations/discord_notifications.md
@@ -10,7 +10,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
 
 The Discord Notifications service sends event notifications from GitLab to the channel for which the webhook was created.
 
-To send GitLab event notifications to a Discord channel, create a webhook in Discord and configure it in GitLab.
+To send GitLab event notifications to a Discord channel, [create a webhook in Discord](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks)
+and configure it in GitLab.
 
 ## Create webhook
 
diff --git a/ee/app/assets/javascripts/approvals/components/approval_gate_icon.vue b/ee/app/assets/javascripts/approvals/components/approval_gate_icon.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cd8c479b60969c5f18c10691db66d7203952ab4f
--- /dev/null
+++ b/ee/app/assets/javascripts/approvals/components/approval_gate_icon.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlIcon, GlPopover } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+
+export default {
+  components: {
+    GlIcon,
+    GlPopover,
+  },
+  props: {
+    url: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    iconId() {
+      return uniqueId('approval-icon-');
+    },
+    containerId() {
+      return uniqueId('approva-icon-container-');
+    },
+  },
+  i18n: {
+    title: __('Approval Gate'),
+  },
+};
+</script>
+
+<template>
+  <div :id="containerId">
+    <gl-icon :id="iconId" name="api" />
+    <gl-popover
+      :target="iconId"
+      :container="containerId"
+      placement="top"
+      :title="$options.i18n.title"
+      triggers="hover focus"
+      :content="url"
+    />
+  </div>
+</template>
diff --git a/ee/app/assets/javascripts/approvals/components/approver_type_select.vue b/ee/app/assets/javascripts/approvals/components/approver_type_select.vue
new file mode 100644
index 0000000000000000000000000000000000000000..41f2155d58eefde43c208eafa8c8da8eec6668a5
--- /dev/null
+++ b/ee/app/assets/javascripts/approvals/components/approver_type_select.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+
+export default {
+  components: {
+    GlDropdown,
+    GlDropdownItem,
+  },
+  props: {
+    approverTypeOptions: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      selected: null,
+    };
+  },
+  computed: {
+    dropdownText() {
+      return this.selected.text;
+    },
+  },
+  created() {
+    const [firstOption] = this.approverTypeOptions;
+    this.onSelect(firstOption);
+  },
+  methods: {
+    isSelectedType(type) {
+      return this.selected.type === type;
+    },
+    onSelect(option) {
+      this.selected = option;
+      this.$emit('input', option.type);
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-dropdown class="gl-w-full gl-dropdown-menu-full-width" :text="dropdownText">
+    <gl-dropdown-item
+      v-for="option in approverTypeOptions"
+      :key="option.type"
+      :is-check-item="true"
+      :is-checked="isSelectedType(option.type)"
+      @click="onSelect(option)"
+    >
+      <span>{{ option.text }}</span>
+    </gl-dropdown-item>
+  </gl-dropdown>
+</template>
diff --git a/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue b/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue
index d1169d4004642642bc19a6941f5c069c3ea7a068..8e6f66f50864da92d3a0be3cc6074859252c942b 100644
--- a/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue
+++ b/ee/app/assets/javascripts/approvals/components/modal_rule_remove.vue
@@ -3,14 +3,24 @@ import { GlSprintf } from '@gitlab/ui';
 import { mapActions, mapState } from 'vuex';
 import { n__, s__, __ } from '~/locale';
 import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
+import { RULE_TYPE_EXTERNAL_APPROVAL } from '../constants';
 
 const i18n = {
   cancelButtonText: __('Cancel'),
-  primaryButtonText: __('Remove approvers'),
-  modalTitle: __('Remove approvers?'),
-  removeWarningText: s__(
-    'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}.',
-  ),
+  regularRule: {
+    primaryButtonText: __('Remove approvers'),
+    modalTitle: __('Remove approvers?'),
+    removeWarningText: s__(
+      'ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}.',
+    ),
+  },
+  externalRule: {
+    primaryButtonText: s__('ApprovalRuleRemove|Remove approval gate'),
+    modalTitle: s__('ApprovalRuleRemove|Remove approval gate?'),
+    removeWarningText: s__(
+      'ApprovalRuleRemove|You are about to remove the %{name} approval gate. Approval from this service is not revoked.',
+    ),
+  },
 };
 
 export default {
@@ -28,6 +38,9 @@ export default {
     ...mapState('deleteModal', {
       rule: 'data',
     }),
+    isExternalApprovalRule() {
+      return this.rule?.ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
+    },
     membersText() {
       return n__(
         'ApprovalRuleRemove|%d member',
@@ -42,24 +55,38 @@ export default {
         this.rule.approvers.length,
       );
     },
+    modalTitle() {
+      return this.isExternalApprovalRule
+        ? i18n.externalRule.modalTitle
+        : i18n.regularRule.modalTitle;
+    },
     modalText() {
-      return `${i18n.removeWarningText} ${this.revokeWarningText}`;
+      return this.isExternalApprovalRule
+        ? i18n.externalRule.removeWarningText
+        : `${i18n.regularRule.removeWarningText} ${this.revokeWarningText}`;
+    },
+    primaryButtonProps() {
+      const text = this.isExternalApprovalRule
+        ? i18n.externalRule.primaryButtonText
+        : i18n.regularRule.primaryButtonText;
+      return {
+        text,
+        attributes: [{ variant: 'danger' }],
+      };
     },
   },
   methods: {
-    ...mapActions(['deleteRule']),
+    ...mapActions(['deleteRule', 'deleteExternalApprovalRule']),
     submit() {
-      this.deleteRule(this.rule.id);
+      if (this.rule.externalUrl) {
+        this.deleteExternalApprovalRule(this.rule.id);
+      } else {
+        this.deleteRule(this.rule.id);
+      }
     },
   },
-  buttonActions: {
-    primary: {
-      text: i18n.primaryButtonText,
-      attributes: [{ variant: 'danger' }],
-    },
-    cancel: {
-      text: i18n.cancelButtonText,
-    },
+  cancelButtonProps: {
+    text: i18n.cancelButtonText,
   },
   i18n,
 };
@@ -69,9 +96,9 @@ export default {
   <gl-modal-vuex
     modal-module="deleteModal"
     :modal-id="modalId"
-    :title="$options.i18n.modalTitle"
-    :action-primary="$options.buttonActions.primary"
-    :action-cancel="$options.buttonActions.cancel"
+    :title="modalTitle"
+    :action-primary="primaryButtonProps"
+    :action-cancel="$options.cancelButtonProps"
     @ok.prevent="submit"
   >
     <p v-if="rule">
@@ -82,9 +109,6 @@ export default {
         <template #nMembers>
           <strong>{{ membersText }}</strong>
         </template>
-        <template #revokeWarning>
-          {{ revokeWarningText }}
-        </template>
       </gl-sprintf>
     </p>
   </gl-modal-vuex>
diff --git a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue
index 868a537a9ea9a597fa932e86a8ee0e10d0b50edf..bee900015a0dae8a0161be95adc3bcdde217609f 100644
--- a/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue
+++ b/ee/app/assets/javascripts/approvals/components/project_settings/project_rules.vue
@@ -4,8 +4,13 @@ import RuleName from 'ee/approvals/components/rule_name.vue';
 import { n__, sprintf } from '~/locale';
 import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
 import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { RULE_TYPE_ANY_APPROVER, RULE_TYPE_REGULAR } from '../../constants';
+import {
+  RULE_TYPE_EXTERNAL_APPROVAL,
+  RULE_TYPE_ANY_APPROVER,
+  RULE_TYPE_REGULAR,
+} from '../../constants';
 
+import ApprovalGateIcon from '../approval_gate_icon.vue';
 import EmptyRule from '../empty_rule.vue';
 import RuleInput from '../mr_edit/rule_input.vue';
 import RuleBranches from '../rule_branches.vue';
@@ -15,6 +20,7 @@ import UnconfiguredSecurityRules from '../security_configuration/unconfigured_se
 
 export default {
   components: {
+    ApprovalGateIcon,
     RuleControls,
     Rules,
     UserAvatarList,
@@ -95,6 +101,9 @@ export default {
 
       return canEdit && (!allowMultiRule || !rule.hasSource);
     },
+    isExternalApprovalRule({ ruleType }) {
+      return ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
+    },
   },
 };
 </script>
@@ -132,13 +141,14 @@ export default {
               class="js-members"
               :class="settings.allowMultiRule ? 'd-none d-sm-table-cell' : null"
             >
-              <user-avatar-list :items="rule.approvers" :img-size="24" empty-text="" />
+              <approval-gate-icon v-if="isExternalApprovalRule(rule)" :url="rule.externalUrl" />
+              <user-avatar-list v-else :items="rule.approvers" :img-size="24" empty-text="" />
             </td>
             <td v-if="settings.allowMultiRule" class="js-branches">
               <rule-branches :rule="rule" />
             </td>
             <td class="js-approvals-required">
-              <rule-input :rule="rule" />
+              <rule-input v-if="!isExternalApprovalRule(rule)" :rule="rule" />
             </td>
             <td class="text-nowrap px-2 w-0 js-controls">
               <rule-controls v-if="canEdit(rule)" :rule="rule" />
diff --git a/ee/app/assets/javascripts/approvals/components/rule_form.vue b/ee/app/assets/javascripts/approvals/components/rule_form.vue
index ca32636c5f63046ede71f66110f4588dc5cc5737..75039865820ecb4fea3602dabf1580a5be1cadfe 100644
--- a/ee/app/assets/javascripts/approvals/components/rule_form.vue
+++ b/ee/app/assets/javascripts/approvals/components/rule_form.vue
@@ -1,8 +1,17 @@
 <script>
 import { groupBy, isNumber } from 'lodash';
 import { mapState, mapActions } from 'vuex';
-import { sprintf, __ } from '~/locale';
-import { TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from '../constants';
+import { isSafeURL } from '~/lib/utils/url_utility';
+import { sprintf, __, s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+  TYPE_USER,
+  TYPE_GROUP,
+  TYPE_HIDDEN_GROUPS,
+  RULE_TYPE_EXTERNAL_APPROVAL,
+  RULE_TYPE_USER_OR_GROUP_APPROVER,
+} from '../constants';
+import ApproverTypeSelect from './approver_type_select.vue';
 import ApproversList from './approvers_list.vue';
 import ApproversSelect from './approvers_select.vue';
 import BranchesSelect from './branches_select.vue';
@@ -21,7 +30,9 @@ export default {
     ApproversList,
     ApproversSelect,
     BranchesSelect,
+    ApproverTypeSelect,
   },
+  mixins: [glFeatureFlagsMixin()],
   props: {
     initRule: {
       type: Object,
@@ -44,6 +55,7 @@ export default {
       name: this.defaultRuleName,
       approvalsRequired: 1,
       minApprovalsRequired: 0,
+      externalUrl: null,
       approvers: [],
       approversToAdd: [],
       branches: [],
@@ -52,6 +64,7 @@ export default {
       isFallback: false,
       containsHiddenGroups: false,
       serverValidationErrors: [],
+      ruleType: null,
       ...this.getInitialData(),
     };
 
@@ -59,6 +72,17 @@ export default {
   },
   computed: {
     ...mapState(['settings']),
+    showApproverTypeSelect() {
+      return (
+        this.glFeatures.ffComplianceApprovalGates &&
+        !this.isEditing &&
+        !this.isMrEdit &&
+        !READONLY_NAMES.includes(this.name)
+      );
+    },
+    isExternalApprovalRule() {
+      return this.ruleType === RULE_TYPE_EXTERNAL_APPROVAL;
+    },
     rule() {
       // If we are creating a new rule with a suggested approval name
       return this.defaultRuleName ? null : this.initRule;
@@ -85,16 +109,32 @@ export default {
 
       const invalidObject = {
         name: this.invalidName,
-        approvalsRequired: this.invalidApprovalsRequired,
-        approvers: this.invalidApprovers,
       };
 
       if (!this.isMrEdit) {
         invalidObject.branches = this.invalidBranches;
       }
 
+      if (this.isExternalApprovalRule) {
+        invalidObject.externalUrl = this.invalidApprovalGateUrl;
+      } else {
+        invalidObject.approvers = this.invalidApprovers;
+        invalidObject.approvalsRequired = this.invalidApprovalsRequired;
+      }
+
       return invalidObject;
     },
+    invalidApprovalGateUrl() {
+      let error = '';
+
+      if (this.serverValidationErrors.includes('External url has already been taken')) {
+        error = __('External url has already been taken');
+      } else if (!this.externalUrl || !isSafeURL(this.externalUrl)) {
+        error = __('Please provide a valid URL');
+      }
+
+      return error;
+    },
     invalidName() {
       let error = '';
 
@@ -175,9 +215,24 @@ export default {
         protectedBranchIds: this.branches,
       };
     },
+    isEditing() {
+      return Boolean(this.initRule);
+    },
+    externalRuleSubmissionData() {
+      const { id, name, protectedBranchIds } = this.submissionData;
+      return {
+        id,
+        name,
+        protectedBranchIds,
+        externalUrl: this.externalUrl,
+      };
+    },
     showProtectedBranch() {
       return !this.isMrEdit && this.settings.allowMultiRule;
     },
+    approvalGateLabel() {
+      return this.isEditing ? this.$options.i18n.approvalGate : this.$options.i18n.addApprovalGate;
+    },
   },
   watch: {
     approversToAdd(value) {
@@ -188,7 +243,15 @@ export default {
     },
   },
   methods: {
-    ...mapActions(['putFallbackRule', 'postRule', 'putRule', 'deleteRule', 'postRegularRule']),
+    ...mapActions([
+      'putFallbackRule',
+      'putExternalApprovalRule',
+      'postExternalApprovalRule',
+      'postRule',
+      'putRule',
+      'deleteRule',
+      'postRegularRule',
+    ]),
     addSelection() {
       if (!this.approversToAdd.length) {
         return;
@@ -219,9 +282,13 @@ export default {
       }
 
       submission.catch((failureResponse) => {
-        this.serverValidationErrors = mapServerResponseToValidationErrors(
-          failureResponse?.response?.data?.message || {},
-        );
+        if (this.isExternalApprovalRule) {
+          this.serverValidationErrors = failureResponse?.response?.data?.message || [];
+        } else {
+          this.serverValidationErrors = mapServerResponseToValidationErrors(
+            failureResponse?.response?.data?.message || {},
+          );
+        }
       });
 
       return submission;
@@ -230,12 +297,14 @@ export default {
      * Submit the rule, by either put-ing or post-ing.
      */
     submitRule() {
+      if (this.isExternalApprovalRule) {
+        const data = this.externalRuleSubmissionData;
+        return data.id ? this.putExternalApprovalRule(data) : this.postExternalApprovalRule(data);
+      }
       const data = this.submissionData;
-
       if (!this.settings.allowMultiRule && this.settings.prefix === 'mr-edit') {
         return data.id ? this.putRule(data) : this.postRegularRule(data);
       }
-
       return data.id ? this.putRule(data) : this.postRule(data);
     },
     /**
@@ -248,7 +317,7 @@ export default {
      * Submit as a single rule. This is determined by the settings.
      */
     submitSingleRule() {
-      if (!this.approvers.length) {
+      if (!this.approvers.length && !this.isExternalApprovalRule) {
         return this.submitEmptySingleRule();
       }
 
@@ -280,6 +349,16 @@ export default {
         };
       }
 
+      if (this.initRule.ruleType === RULE_TYPE_EXTERNAL_APPROVAL) {
+        return {
+          name: this.initRule.name || '',
+          externalUrl: this.initRule.externalUrl,
+          branches: this.initRule.protectedBranches?.map((x) => x.id) || [],
+          ruleType: this.initRule.ruleType,
+          approvers: [],
+        };
+      }
+
       const { containsHiddenGroups = false, removeHiddenGroups = false } = this.initRule;
 
       const users = this.initRule.users.map((x) => ({ ...x, type: TYPE_USER }));
@@ -290,6 +369,7 @@ export default {
         name: this.initRule.name || '',
         approvalsRequired: this.initRule.approvalsRequired || 0,
         minApprovalsRequired: this.initRule.minApprovalsRequired || 0,
+        ruleType: this.initRule.ruleType,
         containsHiddenGroups,
         approvers: groups
           .concat(users)
@@ -300,6 +380,14 @@ export default {
       };
     },
   },
+  i18n: {
+    approvalGate: s__('ApprovalRule|Approvel gate'),
+    addApprovalGate: s__('ApprovalRule|Add approvel gate'),
+  },
+  approverTypeOptions: [
+    { type: RULE_TYPE_USER_OR_GROUP_APPROVER, text: s__('ApprovalRule|Users or groups') },
+    { type: RULE_TYPE_EXTERNAL_APPROVAL, text: s__('ApprovalRule|Approval service API') },
+  ],
 };
 </script>
 
@@ -334,7 +422,14 @@ export default {
         {{ __('Apply this approval rule to any branch or a specific protected branch.') }}
       </small>
     </div>
-    <div class="form-group gl-form-group">
+    <div v-if="showApproverTypeSelect" class="form-group gl-form-group">
+      <label class="col-form-label">{{ s__('ApprovalRule|Approver Type') }}</label>
+      <approver-type-select
+        v-model="ruleType"
+        :approver-type-options="$options.approverTypeOptions"
+      />
+    </div>
+    <div v-if="!isExternalApprovalRule" class="form-group gl-form-group">
       <label class="col-form-label">{{ s__('ApprovalRule|Approvals required') }}</label>
       <input
         v-model.number="approvalsRequired"
@@ -347,7 +442,7 @@ export default {
       />
       <span class="invalid-feedback">{{ validation.approvalsRequired }}</span>
     </div>
-    <div class="form-group gl-form-group">
+    <div v-if="!isExternalApprovalRule" class="form-group gl-form-group">
       <label class="col-form-label">{{ s__('ApprovalRule|Add approvers') }}</label>
       <approvers-select
         v-model="approversToAdd"
@@ -359,7 +454,22 @@ export default {
       />
       <span class="invalid-feedback">{{ validation.approvers }}</span>
     </div>
-    <div class="bordered-box overflow-auto h-12em">
+    <div v-if="isExternalApprovalRule" class="form-group gl-form-group">
+      <label class="col-form-label">{{ approvalGateLabel }}</label>
+      <input
+        v-model="externalUrl"
+        :class="{ 'is-invalid': validation.externalUrl }"
+        class="gl-form-input form-control"
+        name="approval_gate_url"
+        type="url"
+        data-qa-selector="external_url_field"
+      />
+      <span class="invalid-feedback">{{ validation.externalUrl }}</span>
+      <small class="form-text text-gl-muted">
+        {{ s__('ApprovalRule|Invoke an external API as part of the approvals') }}
+      </small>
+    </div>
+    <div v-if="!isExternalApprovalRule" class="bordered-box overflow-auto h-12em">
       <approvers-list v-model="approvers" />
     </div>
   </form>
diff --git a/ee/app/assets/javascripts/approvals/constants.js b/ee/app/assets/javascripts/approvals/constants.js
index da1013904d8602d69f6509cf2017346263c843b9..e09704b68a2a62b61adecffefe828600cfbceb39 100644
--- a/ee/app/assets/javascripts/approvals/constants.js
+++ b/ee/app/assets/javascripts/approvals/constants.js
@@ -17,6 +17,7 @@ export const RULE_TYPE_CODE_OWNER = 'code_owner';
 export const RULE_TYPE_ANY_APPROVER = 'any_approver';
 export const RULE_TYPE_EXTERNAL_APPROVAL = 'external_approval';
 export const RULE_NAME_ANY_APPROVER = 'All Members';
+export const RULE_TYPE_USER_OR_GROUP_APPROVER = 'user_or_group';
 
 export const VULNERABILITY_CHECK_NAME = 'Vulnerability-Check';
 export const LICENSE_CHECK_NAME = 'License-Check';
diff --git a/ee/app/assets/javascripts/approvals/mappers.js b/ee/app/assets/javascripts/approvals/mappers.js
index 5390d1d1dd576899b17a090cddd5c7e9e1a9f90c..7006a21b93687540c70ee95f98fe736f12361bc7 100644
--- a/ee/app/assets/javascripts/approvals/mappers.js
+++ b/ee/app/assets/javascripts/approvals/mappers.js
@@ -70,7 +70,7 @@ export const mapExternalApprovalRuleResponse = (res) => ({
 });
 
 export const mapExternalApprovalResponse = (res) => ({
-  rules: withDefaultEmptyRule(res.map(mapExternalApprovalRuleResponse)),
+  rules: res.map(mapExternalApprovalRuleResponse),
 });
 
 export const mapApprovalSettingsResponse = (res) => ({
diff --git a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue
index ad3307adb61805dbc759cc84345ba8b2ecce9d10..6f0a7a36db3e49a85f93c62f5375f4a9e3f1b1cc 100644
--- a/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue
+++ b/ee/app/assets/javascripts/integrations/jira/issues_list/components/jira_issues_list_root.vue
@@ -200,7 +200,7 @@ export default {
     @filter="handleFilterIssues"
   >
     <template #nav-actions>
-      <gl-button :href="issueCreateUrl" target="_blank"
+      <gl-button :href="issueCreateUrl" target="_blank" class="gl-my-5"
         >{{ s__('Integrations|Create new issue in Jira') }}<gl-icon name="external-link"
       /></gl-button>
     </template>
diff --git a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_row.vue b/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_row.vue
deleted file mode 100644
index 91f3af8298ae19f77634d25d60ebe448b49aa9ec..0000000000000000000000000000000000000000
--- a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_row.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
-export default {
-  props: {
-    label: {
-      type: String,
-      required: true,
-    },
-  },
-};
-</script>
-<template>
-  <div
-    class="generic-report-row gl-display-grid gl-px-3 gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-gray-100"
-  >
-    <strong>{{ label }}</strong>
-    <div data-testid="reportContent">
-      <slot></slot>
-    </div>
-  </div>
-</template>
diff --git a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue b/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue
index dfcf3874cb8c133a5b67a12e232f404bc41af9d6..ef54f04a1b30125ef4c4e8b8de89947debeb2d68 100644
--- a/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue
+++ b/ee/app/assets/javascripts/vulnerabilities/components/generic_report/report_section.vue
@@ -2,7 +2,6 @@
 import { GlCollapse, GlIcon } from '@gitlab/ui';
 import { s__ } from '~/locale';
 import ReportItem from './report_item.vue';
-import ReportRow from './report_row.vue';
 import { filterTypesAndLimitListDepth } from './types/utils';
 
 const NESTED_LISTS_MAX_DEPTH = 4;
@@ -15,7 +14,6 @@ export default {
     GlCollapse,
     GlIcon,
     ReportItem,
-    ReportRow,
   },
   props: {
     details: {
@@ -57,11 +55,14 @@ export default {
       </h3>
     </header>
     <gl-collapse :visible="showSection">
-      <div data-testid="reports">
+      <div class="generic-report-container" data-testid="reports">
         <template v-for="[label, item] in detailsEntries">
-          <report-row :key="label" :label="item.name" :data-testid="`report-row-${label}`">
-            <report-item :item="item" />
-          </report-row>
+          <div :key="label" class="generic-report-row" :data-testid="`report-row-${label}`">
+            <strong class="generic-report-column">{{ item.name }}</strong>
+            <div class="generic-report-column" data-testid="reportContent">
+              <report-item :item="item" :data-testid="`report-item-${label}`" />
+            </div>
+          </div>
         </template>
       </div>
     </gl-collapse>
diff --git a/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss b/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss
index ab4e9e33e03626c039de5ad7b2aca9c6c3b3340b..1b7ffa12db0dd4685bc6505a37eea6a3c1f86af2 100644
--- a/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss
+++ b/ee/app/assets/stylesheets/page_bundles/security_dashboard.scss
@@ -120,15 +120,32 @@ $selection-summary-with-error-height: 118px;
   }
 }
 
+.generic-report-container {
+  @include gl-display-grid;
+
+  grid-template-columns: max-content auto;
+}
+
 .generic-report-row {
-  grid-template-columns: minmax(150px, 1fr) 3fr;
-  grid-column-gap: $gl-spacing-scale-5;
+  display: contents;
 
-  &:last-child {
+  &:last-child .generic-report-column {
     @include gl-border-b-0;
   }
 }
 
+.generic-report-column {
+  @include gl-px-3;
+  @include gl-py-5;
+  @include gl-border-b-1;
+  @include gl-border-b-solid;
+  @include gl-border-gray-100;
+
+  &:first-child {
+    max-width: 15rem;
+  }
+}
+
 .generic-report-list {
   li {
     @include gl-ml-0;
@@ -140,4 +157,3 @@ $selection-summary-with-error-height: 118px;
     list-style-type: disc;
   }
 }
-
diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb
index 8332113817dcbf3b35836bd10de1f739740489fb..419e2401dc75610684f93cebfaaf83e25c1d03c9 100644
--- a/ee/app/controllers/ee/projects_controller.rb
+++ b/ee/app/controllers/ee/projects_controller.rb
@@ -10,6 +10,10 @@ module ProjectsController
       before_action :log_archive_audit_event, only: [:archive]
       before_action :log_unarchive_audit_event, only: [:unarchive]
 
+      before_action only: :edit do
+        push_frontend_feature_flag(:ff_compliance_approval_gates, project, default_enabled: :yaml)
+      end
+
       before_action only: :show do
         push_frontend_feature_flag(:cve_id_request_button, project)
       end
diff --git a/ee/app/graphql/ee/types/query_type.rb b/ee/app/graphql/ee/types/query_type.rb
index 8cdc63c61fb433faef18fb61b4e997d9e9dee066..20db0eaf00356e649efd98f57b5d5464f6415ff6 100644
--- a/ee/app/graphql/ee/types/query_type.rb
+++ b/ee/app/graphql/ee/types/query_type.rb
@@ -61,6 +61,16 @@ module QueryType
               null: true,
               description: 'Get configured DevOps adoption segments on the instance.',
               resolver: ::Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver
+
+        field :current_license, ::Types::Admin::CloudLicenses::CurrentLicenseType,
+              null: true,
+              resolver: ::Resolvers::Admin::CloudLicenses::CurrentLicenseResolver,
+              description: 'Fields related to the current license.'
+
+        field :license_history_entries, ::Types::Admin::CloudLicenses::LicenseHistoryEntryType.connection_type,
+              null: true,
+              resolver: ::Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver,
+              description: 'Fields related to entries in the license history.'
       end
 
       def vulnerability(id:)
diff --git a/ee/app/graphql/resolvers/admin/cloud_licenses/current_license_resolver.rb b/ee/app/graphql/resolvers/admin/cloud_licenses/current_license_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..03ca1c8eb63ae7e36b2f6135e7ccde302b716ca2
--- /dev/null
+++ b/ee/app/graphql/resolvers/admin/cloud_licenses/current_license_resolver.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Admin
+    module CloudLicenses
+      class CurrentLicenseResolver < BaseResolver
+        include Gitlab::Graphql::Authorize::AuthorizeResource
+        include ::Admin::LicenseRequest
+
+        type ::Types::Admin::CloudLicenses::CurrentLicenseType, null: true
+
+        def resolve
+          return unless application_settings.cloud_license_enabled?
+
+          authorize!
+
+          license
+        end
+
+        private
+
+        def application_settings
+          Gitlab::CurrentSettings.current_application_settings
+        end
+
+        def authorize!
+          Ability.allowed?(context[:current_user], :read_licenses) || raise_resource_not_available_error!
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver.rb b/ee/app/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2d93823f633df6e77e517c256762b5bf23e3de7a
--- /dev/null
+++ b/ee/app/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Resolvers
+  module Admin
+    module CloudLicenses
+      class LicenseHistoryEntriesResolver < BaseResolver
+        include Gitlab::Graphql::Authorize::AuthorizeResource
+
+        type [::Types::Admin::CloudLicenses::LicenseHistoryEntryType], null: true
+
+        def resolve
+          return unless application_settings.cloud_license_enabled?
+
+          authorize!
+
+          License.history
+        end
+
+        private
+
+        def application_settings
+          Gitlab::CurrentSettings.current_application_settings
+        end
+
+        def authorize!
+          Ability.allowed?(context[:current_user], :read_licenses) || raise_resource_not_available_error!
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb b/ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..825c681648b7e6cc61c34fd441edb34b4ae577d7
--- /dev/null
+++ b/ee/app/graphql/types/admin/cloud_licenses/current_license_type.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Types
+  module Admin
+    module CloudLicenses
+      # rubocop: disable Graphql/AuthorizeTypes
+      class CurrentLicenseType < BaseObject
+        include ::Types::Admin::CloudLicenses::LicenseType
+
+        graphql_name 'CurrentLicense'
+        description 'Represents the current license'
+
+        field :last_sync, ::Types::TimeType, null: true,
+              description: 'Date when the license was last synced.',
+              method: :last_synced_at
+
+        field :billable_users_count, GraphQL::INT_TYPE, null: true,
+              description: 'Number of billable users on the system.',
+              method: :daily_billable_users_count
+
+        field :maximum_user_count, GraphQL::INT_TYPE, null: true,
+              description: 'Highest number of billable users on the system during the term of the current license.',
+              method: :maximum_user_count
+
+        field :users_over_license_count, GraphQL::INT_TYPE, null: true,
+              description: 'Number of users over the paid users in the license.'
+
+        def users_over_license_count
+          return 0 if object.trial?
+
+          [object.overage_with_historical_max, 0].max
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb b/ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85e50de344ab311d348856288df9050b6a6c65af
--- /dev/null
+++ b/ee/app/graphql/types/admin/cloud_licenses/license_history_entry_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+  module Admin
+    module CloudLicenses
+      # rubocop: disable Graphql/AuthorizeTypes
+      class LicenseHistoryEntryType < BaseObject
+        include ::Types::Admin::CloudLicenses::LicenseType
+
+        graphql_name 'LicenseHistoryEntry'
+        description 'Represents an entry from the Cloud License history'
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/types/admin/cloud_licenses/license_type.rb b/ee/app/graphql/types/admin/cloud_licenses/license_type.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9fd51b4723d2e4aa5fafacc391327a510f1521ce
--- /dev/null
+++ b/ee/app/graphql/types/admin/cloud_licenses/license_type.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Types
+  module Admin
+    module CloudLicenses
+      module LicenseType
+        extend ActiveSupport::Concern
+
+        included do
+          field :id, GraphQL::ID_TYPE, null: false,
+                description: 'ID of the license.',
+                method: :license_id
+
+          field :type, GraphQL::STRING_TYPE, null: false,
+                description: 'Type of the license.',
+                method: :license_type
+
+          field :plan, GraphQL::STRING_TYPE, null: false,
+                description: 'Name of the subscription plan.'
+
+          field :name, GraphQL::STRING_TYPE, null: true,
+                description: 'Name of the licensee.',
+                method: :licensee_name
+
+          field :email, GraphQL::STRING_TYPE, null: true,
+                description: 'Email of the licensee.',
+                method: :licensee_email
+
+          field :company, GraphQL::STRING_TYPE, null: true,
+                description: 'Company of the licensee.',
+                method: :licensee_company
+
+          field :starts_at, ::Types::DateType, null: true,
+                description: 'Date when the license started.'
+
+          field :expires_at, ::Types::DateType, null: true,
+                description: 'Date when the license expires.'
+
+          field :activated_at, ::Types::DateType, null: true,
+                description: 'Date when the license was activated.',
+                method: :created_at
+
+          field :users_in_license_count, GraphQL::INT_TYPE, null: true,
+                description: 'Number of paid users in the license.',
+                method: :restricted_user_count
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/models/license.rb b/ee/app/models/license.rb
index de94004a7a7e2c038e16b684295800aec48f3aae..e4e1d6de2dd778bc2179c083f35aaffe9b5e5f81 100644
--- a/ee/app/models/license.rb
+++ b/ee/app/models/license.rb
@@ -8,6 +8,7 @@ class License < ApplicationRecord
   PREMIUM_PLAN = 'premium'
   ULTIMATE_PLAN = 'ultimate'
   CLOUD_LICENSE_TYPE = 'cloud'
+  LEGACY_LICENSE_TYPE = 'legacy'
   ALLOWED_PERCENTAGE_OF_USERS_OVERAGE = (10 / 100.0)
 
   EE_ALL_PLANS = [STARTER_PLAN, PREMIUM_PLAN, ULTIMATE_PLAN].freeze
@@ -237,6 +238,8 @@ class License < ApplicationRecord
     { range: (1000..nil), percentage: true, value: 5 }
   ].freeze
 
+  LICENSEE_ATTRIBUTES = %w[Name Email Company].freeze
+
   validate :valid_license
   validate :check_users_limit, if: :new_record?, unless: :validate_with_trueup?
   validate :check_trueup, unless: :persisted?, if: :validate_with_trueup?
@@ -550,6 +553,10 @@ def cloud?
     license&.type == CLOUD_LICENSE_TYPE
   end
 
+  def license_type
+    cloud? ? CLOUD_LICENSE_TYPE : LEGACY_LICENSE_TYPE
+  end
+
   def auto_renew
     false
   end
@@ -576,6 +583,12 @@ def remaining_user_count
     restricted_user_count - daily_billable_users_count
   end
 
+  LICENSEE_ATTRIBUTES.each do |attribute|
+    define_method "licensee_#{attribute.downcase}" do
+      licensee[attribute]
+    end
+  end
+
   private
 
   def restricted_attr(name, default = nil)
diff --git a/ee/app/serializers/integrations/jira/issue_entity.rb b/ee/app/serializers/integrations/jira/issue_entity.rb
index c59137fe16144886dbc4e416498fbfff9f3ea81f..90f449dff2d5021587ac491c5b93ba9d3bd387a9 100644
--- a/ee/app/serializers/integrations/jira/issue_entity.rb
+++ b/ee/app/serializers/integrations/jira/issue_entity.rb
@@ -34,8 +34,8 @@ class IssueEntity < Grape::Entity
           {
             title: name,
             name: name,
-            color: '#EBECF0',
-            text_color: '#283856'
+            color: '#0052CC',
+            text_color: '#FFFFFF'
           }
         end
       end
diff --git a/ee/app/services/ee/merge_requests/update_service.rb b/ee/app/services/ee/merge_requests/update_service.rb
index e8f8d3af1964be7027c3ce112b842d9b07d31021..636b7b624eb02bc926d15e198ffe3a58936a7e58 100644
--- a/ee/app/services/ee/merge_requests/update_service.rb
+++ b/ee/app/services/ee/merge_requests/update_service.rb
@@ -39,7 +39,9 @@ def execute(merge_request)
       def after_update(merge_request)
         super
 
-        ::MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute
+        merge_request.run_after_commit do
+          ::MergeRequests::SyncCodeOwnerApprovalRulesWorker.perform_async(merge_request)
+        end
       end
 
       override :create_branch_change_note
diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml
index dbe7694f337931de5cab6e4fffe97f0212b6d2a4..7d08829262cc16e73d3a4956f9f9f0ca3fbc1fb2 100644
--- a/ee/app/workers/all_queues.yml
+++ b/ee/app/workers/all_queues.yml
@@ -867,6 +867,14 @@
   :weight: 1
   :idempotent:
   :tags: []
+- :name: merge_requests_sync_code_owner_approval_rules
+  :feature_category: :source_code_management
+  :has_external_dependencies:
+  :urgency: :high
+  :resource_boundary: :unknown
+  :weight: 1
+  :idempotent: true
+  :tags: []
 - :name: new_epic
   :feature_category: :epics
   :has_external_dependencies:
diff --git a/ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb b/ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2ea4ae21ad1165513b95819bafa7bedbdc706366
--- /dev/null
+++ b/ee/app/workers/merge_requests/sync_code_owner_approval_rules_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module MergeRequests
+  class SyncCodeOwnerApprovalRulesWorker
+    include ApplicationWorker
+
+    feature_category :source_code_management
+    urgency :high
+    deduplicate :until_executed
+    idempotent!
+
+    def perform(merge_request_id)
+      merge_request = MergeRequest.find_by_id(merge_request_id)
+      return unless merge_request
+
+      ::MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute
+    end
+  end
+end
diff --git a/ee/changelogs/unreleased/326225-add-load-balancing-metrics-on-request-level.yml b/ee/changelogs/unreleased/326225-add-load-balancing-metrics-on-request-level.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6324a789f6bea817d360c205bc53377a55703087
--- /dev/null
+++ b/ee/changelogs/unreleased/326225-add-load-balancing-metrics-on-request-level.yml
@@ -0,0 +1,5 @@
+---
+title: Add metric to track querying write ahead log on Request level
+merge_request: 58673
+author:
+type: other
diff --git a/ee/changelogs/unreleased/326225-add-sidekiq-lb-server-metrics.yml b/ee/changelogs/unreleased/326225-add-sidekiq-lb-server-metrics.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5721c60df5b13dc077bc4508ccdc87622502b2c6
--- /dev/null
+++ b/ee/changelogs/unreleased/326225-add-sidekiq-lb-server-metrics.yml
@@ -0,0 +1,5 @@
+---
+title: Add load balancing Sidekiq metrics
+merge_request: 58473
+author:
+type: other
diff --git a/ee/changelogs/unreleased/327553-jira-issue-label-colors-should-have-higher-contrast.yml b/ee/changelogs/unreleased/327553-jira-issue-label-colors-should-have-higher-contrast.yml
new file mode 100644
index 0000000000000000000000000000000000000000..47313fcfeaaef4b3ca46bb2b656e7b72f1fca29e
--- /dev/null
+++ b/ee/changelogs/unreleased/327553-jira-issue-label-colors-should-have-higher-contrast.yml
@@ -0,0 +1,5 @@
+---
+title: Update label color on Jira issues pages
+merge_request: 59226
+author:
+type: changed
diff --git a/ee/changelogs/unreleased/dpisek-generic-reports-grid-cleanup.yml b/ee/changelogs/unreleased/dpisek-generic-reports-grid-cleanup.yml
new file mode 100644
index 0000000000000000000000000000000000000000..814c535b49afb0fcc459eb720e40b105257837e4
--- /dev/null
+++ b/ee/changelogs/unreleased/dpisek-generic-reports-grid-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: 'Generic vulnerability reports: Remove extra margin between report columns'
+merge_request: 58720
+author:
+type: other
diff --git a/ee/changelogs/unreleased/rspec-empty-lines-after-letitbe-ee-spec-controllers-registractions.yml b/ee/changelogs/unreleased/rspec-empty-lines-after-letitbe-ee-spec-controllers-registractions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a0a0485dc6cd523e65d0a432fa9c98a170fd8f2e
--- /dev/null
+++ b/ee/changelogs/unreleased/rspec-empty-lines-after-letitbe-ee-spec-controllers-registractions.yml
@@ -0,0 +1,5 @@
+---
+title: Fix RSpec/EmptyLineAfterFinalLetItBe rubocop offenses in ee/spec/controllers/registrations
+merge_request: 58408
+author: Abdul Wadood @abdulwd
+type: fixed
diff --git a/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb b/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb
index c856221c48a744cc992f4bb34fd1ebde66c081dd..529323ab30c5367e720dff86f40c4937fd881016 100644
--- a/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb
+++ b/ee/lib/ee/gitlab/metrics/subscribers/active_record.rb
@@ -9,11 +9,13 @@ module ActiveRecord
           extend ::Gitlab::Utils::Override
 
           DB_LOAD_BALANCING_COUNTERS = %i{
-            db_replica_count db_replica_cached_count
-            db_primary_count db_primary_cached_count
+            db_replica_count db_replica_cached_count db_replica_wal_count
+            db_primary_count db_primary_cached_count db_primary_wal_count
           }.freeze
           DB_LOAD_BALANCING_DURATIONS = %i{db_primary_duration_s db_replica_duration_s}.freeze
 
+          SQL_WAL_LOCATION_REGEX = /(pg_current_wal_insert_lsn\(\)::text|pg_last_wal_replay_lsn\(\)::text)/.freeze
+
           class_methods do
             extend ::Gitlab::Utils::Override
 
@@ -55,9 +57,14 @@ def sql(event)
 
           private
 
+          def wal_command?(payload)
+            payload[:sql].match(SQL_WAL_LOCATION_REGEX)
+          end
+
           def increment_db_role_counters(db_role, payload)
             increment("db_#{db_role}_count".to_sym)
             increment("db_#{db_role}_cached_count".to_sym) if cached_query?(payload)
+            increment("db_#{db_role}_wal_count".to_sym) if !cached_query?(payload) && wal_command?(payload)
           end
 
           def observe_db_role_duration(db_role, event)
diff --git a/ee/lib/ee/gitlab/sidekiq_middleware/server_metrics.rb b/ee/lib/ee/gitlab/sidekiq_middleware/server_metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..03d5f2b6c64a94f338d18a16d60eeb7497f40884
--- /dev/null
+++ b/ee/lib/ee/gitlab/sidekiq_middleware/server_metrics.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+module EE
+  module Gitlab
+    module SidekiqMiddleware
+      module ServerMetrics
+        extend ::Gitlab::Utils::Override
+
+        protected
+
+        override :init_metrics
+        def init_metrics
+          super.merge(init_load_balancing_metrics)
+        end
+
+        override :instrument
+        def instrument(job, labels)
+          super
+        ensure
+          record_load_balancing(job, labels)
+        end
+
+        private
+
+        def init_load_balancing_metrics
+          return {} unless ::Gitlab::Database::LoadBalancing.enable?
+
+          {
+            sidekiq_load_balancing_count: ::Gitlab::Metrics.counter(:sidekiq_load_balancing_count, 'Sidekiq jobs with load balancing')
+          }
+        end
+
+        def record_load_balancing(job, labels)
+          return unless ::Gitlab::Database::LoadBalancing.enable?
+          return unless job[:database_chosen]
+
+          load_balancing_labels = {
+            database_chosen: job[:database_chosen],
+            data_consistency: job[:data_consistency]
+          }
+
+          metrics[:sidekiq_load_balancing_count].increment(labels.merge(load_balancing_labels), 1)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb b/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
index 3044585285f6a4bfef757c94e83ec1c21ad536d2..7136175a5bc817079a7c59f74314b0c82b6af666 100644
--- a/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
+++ b/ee/lib/gitlab/database/load_balancing/sidekiq_client_middleware.rb
@@ -19,6 +19,9 @@ def mark_data_consistency_location(worker_class, job)
           return unless worker_class
           return unless worker_class.include?(::ApplicationWorker)
           return unless worker_class.get_data_consistency_feature_flag_enabled?
+
+          job['worker_data_consistency'] = worker_class.get_data_consistency
+
           return if worker_class.get_data_consistency == :always
 
           if Session.current.performed_write?
diff --git a/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb b/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
index 125782927480185c65fb3b0a497aadf08bca9ce2..37cb4e35072ea5c06bcd96dd2839feaf98435992 100644
--- a/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
+++ b/ee/lib/gitlab/database/load_balancing/sidekiq_server_middleware.rb
@@ -25,18 +25,18 @@ def clear
 
         def requires_primary?(worker_class, job)
           return true unless worker_class.include?(::ApplicationWorker)
-
-          job[:worker_data_consistency] = worker_class.get_data_consistency
-
           return true if worker_class.get_data_consistency == :always
           return true unless worker_class.get_data_consistency_feature_flag_enabled?
 
-          if job['database_replica_location'] || replica_caught_up?(job['database_write_location'] )
+          if job['database_replica_location'] || replica_caught_up?(job['database_write_location'])
+            job[:database_chosen] = 'replica'
             false
           elsif worker_class.get_data_consistency == :delayed && job['retry_count'].to_i == 0
+            job[:database_chosen] = 'retry'
             raise JobReplicaNotUpToDate, "Sidekiq job #{worker_class} JID-#{job['jid']} couldn't use the replica."\
                "  Replica was not up to date."
           else
+            job[:database_chosen] = 'primary'
             true
           end
         end
diff --git a/ee/spec/controllers/registrations/groups_controller_spec.rb b/ee/spec/controllers/registrations/groups_controller_spec.rb
index 9be431afb180a81b93077a8b538d6a43ddf6abfd..512ef65cc89b316ad17859870a1c755c32d2c64c 100644
--- a/ee/spec/controllers/registrations/groups_controller_spec.rb
+++ b/ee/spec/controllers/registrations/groups_controller_spec.rb
@@ -78,6 +78,7 @@
     let_it_be(:trial_form_params) { { trial: 'false' } }
     let_it_be(:trial_onboarding_issues_enabled) { false }
     let_it_be(:trial_onboarding_flow_params) { {} }
+
     let(:signup_onboarding_enabled) { true }
     let(:group_params) { { name: 'Group name', path: 'group-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE, emails: ['', ''] } }
     let(:params) do
diff --git a/ee/spec/controllers/registrations/projects_controller_spec.rb b/ee/spec/controllers/registrations/projects_controller_spec.rb
index 229815df354d5bf3ba53da01a74a5db6ea0258dd..e2e744f96707eeeb89b8aa30ee628db7c0045237 100644
--- a/ee/spec/controllers/registrations/projects_controller_spec.rb
+++ b/ee/spec/controllers/registrations/projects_controller_spec.rb
@@ -54,6 +54,7 @@
     subject { post :create, params: { project: params }.merge(trial_onboarding_flow_params) }
 
     let_it_be(:trial_onboarding_flow_params) { {} }
+
     let(:params) { { namespace_id: namespace.id, name: 'New project', path: 'project-path', visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
     let(:signup_onboarding_enabled) { true }
 
diff --git a/ee/spec/features/projects/settings/merge_requests_settings_spec.rb b/ee/spec/features/projects/settings/merge_requests_settings_spec.rb
index 4efb445239f831fb1414bd9ded3753270f38828d..19d7641f990f704053ce32953dfcf47758111fec 100644
--- a/ee/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/ee/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -5,15 +5,16 @@
   include GitlabRoutingHelper
   include FeatureApprovalHelper
 
-  let(:user) { create(:user) }
-  let(:project) { create(:project) }
-  let(:group) { create(:group) }
-  let(:group_member) { create(:user) }
-  let(:non_member) { create(:user) }
-  let!(:config_selector) { '.js-approval-rules' }
-  let!(:modal_selector) { '#project-settings-approvals-create-modal' }
+  let_it_be(:user) { create(:user) }
+  let_it_be(:project) { create(:project) }
+  let_it_be(:group) { create(:group) }
+  let_it_be(:group_member) { create(:user) }
+  let_it_be(:non_member) { create(:user) }
+  let_it_be(:config_selector) { '.js-approval-rules' }
+  let_it_be(:modal_selector) { '#project-settings-approvals-create-modal' }
 
   before do
+    stub_licensed_features(compliance_approval_gates: true)
     sign_in(user)
     project.add_maintainer(user)
     group.add_developer(user)
@@ -69,8 +70,8 @@
   end
 
   context 'with an approver group' do
-    let(:non_group_approver) { create(:user) }
-    let!(:rule) { create(:approval_project_rule, project: project, groups: [group], users: [non_group_approver]) }
+    let_it_be(:non_group_approver) { create(:user) }
+    let_it_be(:rule) { create(:approval_project_rule, project: project, groups: [group], users: [non_group_approver]) }
 
     before do
       project.add_developer(non_group_approver)
@@ -90,6 +91,64 @@
     end
   end
 
+  it 'adds an approval gate' do
+    visit edit_project_path(project)
+
+    open_modal(text: 'Add approval rule', expand: false)
+
+    within('.modal-content') do
+      find('button', text: "Users or groups").click
+      find('button', text: "Approval service API").click
+
+      find('[data-qa-selector="rule_name_field"]').set('My new rule')
+      find('[data-qa-selector="external_url_field"]').set('https://api.gitlab.com')
+
+      click_button 'Add approval rule'
+    end
+
+    wait_for_requests
+
+    expect(first('.js-name')).to have_content('My new rule')
+  end
+
+  context 'with an approval gate' do
+    let_it_be(:rule) { create(:external_approval_rule, project: project) }
+
+    it 'updates the approval gate' do
+      visit edit_project_path(project)
+
+      expect(first('.js-name')).to have_content(rule.name)
+
+      open_modal(text: 'Edit', expand: false)
+
+      within('.modal-content') do
+        find('[data-qa-selector="rule_name_field"]').set('Something new')
+
+        click_button 'Update approval rule'
+      end
+
+      wait_for_requests
+
+      expect(first('.js-name')).to have_content('Something new')
+    end
+
+    it 'removes the approval gate' do
+      visit edit_project_path(project)
+
+      expect(first('.js-name')).to have_content(rule.name)
+
+      first('.js-controls').find('[data-testid="remove-icon"]').click
+
+      within('.modal-content') do
+        click_button 'Remove approval gate'
+      end
+
+      wait_for_requests
+
+      expect(first('.js-name')).not_to have_content(rule.name)
+    end
+  end
+
   context 'issuable default templates feature not available' do
     before do
       stub_licensed_features(issuable_default_templates: false)
diff --git a/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap b/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap
index 134907f59d039ca87b29b2d65326862295eee857..33c2b39440395c6806d93219b27ce07ad149815a 100644
--- a/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap
+++ b/ee/spec/frontend/approvals/components/__snapshots__/modal_rule_remove_spec.js.snap
@@ -1,6 +1,20 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Approvals ModalRuleRemove shows message 1`] = `
+exports[`Approvals ModalRuleRemove matches the snapshot for external approval 1`] = `
+<div
+  title="Remove approval gate?"
+>
+  <p>
+    You are about to remove the 
+    <strong>
+      API Gate
+    </strong>
+     approval gate. Approval from this service is not revoked.
+  </p>
+</div>
+`;
+
+exports[`Approvals ModalRuleRemove matches the snapshot for multiple approvers 1`] = `
 <div
   title="Remove approvers?"
 >
@@ -18,7 +32,7 @@ exports[`Approvals ModalRuleRemove shows message 1`] = `
 </div>
 `;
 
-exports[`Approvals ModalRuleRemove shows singular message 1`] = `
+exports[`Approvals ModalRuleRemove matches the snapshot for singular approver 1`] = `
 <div
   title="Remove approvers?"
 >
diff --git a/ee/spec/frontend/approvals/components/approval_gate_icon_spec.js b/ee/spec/frontend/approvals/components/approval_gate_icon_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..69c2db2c63007c785ee1fd781c850ab0f431c775
--- /dev/null
+++ b/ee/spec/frontend/approvals/components/approval_gate_icon_spec.js
@@ -0,0 +1,42 @@
+import { GlPopover, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ApprovalGateIcon from 'ee/approvals/components/approval_gate_icon.vue';
+
+jest.mock('lodash/uniqueId', () => (id) => `${id}mock`);
+
+describe('ApprovalGateIcon', () => {
+  let wrapper;
+
+  const findPopover = () => wrapper.findComponent(GlPopover);
+  const findIcon = () => wrapper.findComponent(GlIcon);
+
+  const createComponent = () => {
+    return shallowMount(ApprovalGateIcon, {
+      propsData: {
+        url: 'https://gitlab.com/',
+      },
+    });
+  };
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  beforeEach(() => {
+    wrapper = createComponent();
+  });
+
+  it('renders the icon', () => {
+    expect(findIcon().props('name')).toBe('api');
+    expect(findIcon().attributes('id')).toBe('approval-icon-mock');
+  });
+
+  it('renders the popover with the URL for the icon', () => {
+    expect(findPopover().exists()).toBe(true);
+    expect(findPopover().attributes()).toMatchObject({
+      content: 'https://gitlab.com/',
+      title: 'Approval Gate',
+      target: 'approval-icon-mock',
+    });
+  });
+});
diff --git a/ee/spec/frontend/approvals/components/approver_type_select_spec.js b/ee/spec/frontend/approvals/components/approver_type_select_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1583cbe35c542bc39bcc98fe8fc8bb7beb9d3315
--- /dev/null
+++ b/ee/spec/frontend/approvals/components/approver_type_select_spec.js
@@ -0,0 +1,60 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import ApprovalTypeSelect from 'ee/approvals/components/approver_type_select.vue';
+
+jest.mock('lodash/uniqueId', () => (id) => `${id}mock`);
+
+const OPTIONS = [
+  { type: 'x', text: 'foo' },
+  { type: 'y', text: 'bar' },
+];
+
+describe('ApprovalTypeSelect', () => {
+  let wrapper;
+
+  const findDropdown = () => wrapper.findComponent(GlDropdown);
+  const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+
+  const createComponent = () => {
+    return shallowMount(ApprovalTypeSelect, {
+      propsData: {
+        approverTypeOptions: OPTIONS,
+      },
+    });
+  };
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  beforeEach(() => {
+    wrapper = createComponent();
+  });
+
+  it('should select the first option by default', () => {
+    expect(findDropdownItems().at(0).props('isChecked')).toBe(true);
+  });
+
+  it('renders the dropdown with the selected text', () => {
+    expect(findDropdown().props('text')).toBe(OPTIONS[0].text);
+  });
+
+  it('renders a dropdown item for each option', () => {
+    OPTIONS.forEach((option, idx) => {
+      expect(findDropdownItems().at(idx).text()).toBe(option.text);
+    });
+  });
+
+  it('should select an item when clicked', async () => {
+    const item = findDropdownItems().at(1);
+
+    expect(item.props('isChecked')).toBe(false);
+
+    item.vm.$emit('click');
+
+    await nextTick();
+
+    expect(item.props('isChecked')).toBe(true);
+  });
+});
diff --git a/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js b/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js
index 7b1ec6063fba83e25fc07136a5a9950437ab2e7e..17d6e0ce27dc5590afa6eb3d5367eb6cc4867954 100644
--- a/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js
+++ b/ee/spec/frontend/approvals/components/modal_rule_remove_spec.js
@@ -4,6 +4,7 @@ import Vuex from 'vuex';
 import ModalRuleRemove from 'ee/approvals/components/modal_rule_remove.vue';
 import { stubComponent } from 'helpers/stub_component';
 import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
+import { createExternalRule } from '../mocks';
 
 const MODAL_MODULE = 'deleteModal';
 const TEST_MODAL_ID = 'test-delete-modal-id';
@@ -14,6 +15,11 @@ const TEST_RULE = {
     .fill(1)
     .map((x, id) => ({ id })),
 };
+const SINGLE_APPROVER = {
+  ...TEST_RULE,
+  approvers: [{ id: 1 }],
+};
+const EXTERNAL_RULE = createExternalRule();
 
 const localVue = createLocalVue();
 localVue.use(Vuex);
@@ -61,6 +67,7 @@ describe('Approvals ModalRuleRemove', () => {
     };
     actions = {
       deleteRule: jest.fn(),
+      deleteExternalApprovalRule: jest.fn(),
     };
   });
 
@@ -83,30 +90,31 @@ describe('Approvals ModalRuleRemove', () => {
     );
   });
 
-  it('shows message', () => {
-    factory();
-
-    expect(findModal().element).toMatchSnapshot();
-  });
-
-  it('shows singular message', () => {
-    deleteModalState.data = {
-      ...TEST_RULE,
-      approvers: [{ id: 1 }],
-    };
+  it.each`
+    type                    | rule
+    ${'multiple approvers'} | ${TEST_RULE}
+    ${'singular approver'}  | ${SINGLE_APPROVER}
+    ${'external approval'}  | ${EXTERNAL_RULE}
+  `('matches the snapshot for $type', ({ rule }) => {
+    deleteModalState.data = rule;
     factory();
 
     expect(findModal().element).toMatchSnapshot();
   });
 
-  it('deletes rule when modal is submitted', () => {
+  it.each`
+    typeType      | action                          | rule
+    ${'regular'}  | ${'deleteRule'}                 | ${TEST_RULE}
+    ${'external'} | ${'deleteExternalApprovalRule'} | ${EXTERNAL_RULE}
+  `('calls $action when the modal is submitted for a $typeType rule', ({ action, rule }) => {
+    deleteModalState.data = rule;
     factory();
 
-    expect(actions.deleteRule).not.toHaveBeenCalled();
+    expect(actions[action]).not.toHaveBeenCalled();
 
     const modal = findModal();
     modal.vm.$emit('ok', new Event('submit'));
 
-    expect(actions.deleteRule).toHaveBeenCalledWith(expect.anything(), TEST_RULE.id);
+    expect(actions[action]).toHaveBeenCalledWith(expect.anything(), rule.id);
   });
 });
diff --git a/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js b/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js
index 0ceb68ee67bcce1c34dfa3469fde14e1fd9b2ee6..ab3aaa87fdeb3a073036a986f6ed79024949312b 100644
--- a/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js
+++ b/ee/spec/frontend/approvals/components/project_settings/project_rules_spec.js
@@ -1,5 +1,6 @@
 import { mount, createLocalVue } from '@vue/test-utils';
 import Vuex from 'vuex';
+import ApprovalGateIcon from 'ee/approvals/components/approval_gate_icon.vue';
 import RuleInput from 'ee/approvals/components/mr_edit/rule_input.vue';
 import ProjectRules from 'ee/approvals/components/project_settings/project_rules.vue';
 import RuleName from 'ee/approvals/components/rule_name.vue';
@@ -8,7 +9,7 @@ import UnconfiguredSecurityRules from 'ee/approvals/components/security_configur
 import { createStoreOptions } from 'ee/approvals/stores';
 import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
 import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
-import { createProjectRules } from '../../mocks';
+import { createProjectRules, createExternalRule } from '../../mocks';
 
 const TEST_RULES = createProjectRules();
 
@@ -149,4 +150,26 @@ describe('Approvals ProjectRules', () => {
       expect(wrapper.find(UnconfiguredSecurityRules).exists()).toBe(true);
     });
   });
+
+  describe('when the rule is external', () => {
+    const rule = createExternalRule();
+
+    beforeEach(() => {
+      store.modules.approvals.state.rules = [rule];
+
+      factory();
+    });
+
+    it('renders the approval gate component with URL', () => {
+      expect(wrapper.findComponent(ApprovalGateIcon).props('url')).toBe(rule.externalUrl);
+    });
+
+    it('does not render a user avatar component', () => {
+      expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false);
+    });
+
+    it('does not render the approvals required input', () => {
+      expect(wrapper.findComponent(RuleInput).exists()).toBe(false);
+    });
+  });
 });
diff --git a/ee/spec/frontend/approvals/components/rule_form_spec.js b/ee/spec/frontend/approvals/components/rule_form_spec.js
index d0643e2776e3fcdb5729424c9d6f345246553f8a..5b36011f00078eb45b9a9c4695f8955f84d94b6a 100644
--- a/ee/spec/frontend/approvals/components/rule_form_spec.js
+++ b/ee/spec/frontend/approvals/components/rule_form_spec.js
@@ -1,12 +1,21 @@
 import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
 import Vuex from 'vuex';
+import ApproverTypeSelect from 'ee/approvals/components/approver_type_select.vue';
 import ApproversList from 'ee/approvals/components/approvers_list.vue';
 import ApproversSelect from 'ee/approvals/components/approvers_select.vue';
 import BranchesSelect from 'ee/approvals/components/branches_select.vue';
 import RuleForm from 'ee/approvals/components/rule_form.vue';
-import { TYPE_USER, TYPE_GROUP, TYPE_HIDDEN_GROUPS } from 'ee/approvals/constants';
+import {
+  TYPE_USER,
+  TYPE_GROUP,
+  TYPE_HIDDEN_GROUPS,
+  RULE_TYPE_EXTERNAL_APPROVAL,
+} from 'ee/approvals/constants';
 import { createStoreOptions } from 'ee/approvals/stores';
 import projectSettingsModule from 'ee/approvals/stores/modules/project_settings';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createExternalRule } from '../mocks';
 
 const TEST_PROJECT_ID = '7';
 const TEST_RULE = {
@@ -27,6 +36,10 @@ const TEST_FALLBACK_RULE = {
   approvalsRequired: 1,
   isFallback: true,
 };
+const TEST_EXTERNAL_APPROVAL_RULE = {
+  ...createExternalRule(),
+  protectedBranches: TEST_PROTECTED_BRANCHES,
+};
 const TEST_LOCKED_RULE_NAME = 'LOCKED_RULE';
 const nameTakenError = {
   response: {
@@ -37,6 +50,13 @@ const nameTakenError = {
     },
   },
 };
+const urlTakenError = {
+  response: {
+    data: {
+      message: ['External url has already been taken'],
+    },
+  },
+};
 
 const localVue = createLocalVue();
 localVue.use(Vuex);
@@ -54,7 +74,11 @@ describe('EE Approvals RuleForm', () => {
       store: new Vuex.Store(store),
       localVue,
       provide: {
-        glFeatures: { scopedApprovalRules: true, ...options.provide?.glFeatures },
+        glFeatures: {
+          ffComplianceApprovalGates: true,
+          scopedApprovalRules: true,
+          ...options.provide?.glFeatures,
+        },
       },
     });
   };
@@ -71,6 +95,9 @@ describe('EE Approvals RuleForm', () => {
   const findApproversValidation = () => findValidation(findApproversSelect(), true);
   const findApproversList = () => wrapper.find(ApproversList);
   const findBranchesSelect = () => wrapper.find(BranchesSelect);
+  const findApproverTypeSelect = () => wrapper.findComponent(ApproverTypeSelect);
+  const findExternalUrlInput = () => wrapper.find('input[name=approval_gate_url');
+  const findExternalUrlValidation = () => findValidation(findExternalUrlInput(), false);
   const findBranchesValidation = () => findValidation(findBranchesSelect(), true);
   const findValidations = () => [
     findNameValidation(),
@@ -85,12 +112,20 @@ describe('EE Approvals RuleForm', () => {
     findBranchesValidation(),
   ];
 
+  const findValidationForExternal = () => [
+    findNameValidation(),
+    findExternalUrlValidation(),
+    findBranchesValidation(),
+  ];
+
   beforeEach(() => {
     store = createStoreOptions(projectSettingsModule(), { projectId: TEST_PROJECT_ID });
 
-    ['postRule', 'putRule', 'deleteRule', 'putFallbackRule'].forEach((actionName) => {
-      jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {});
-    });
+    ['postRule', 'putRule', 'deleteRule', 'putFallbackRule', 'postExternalApprovalRule'].forEach(
+      (actionName) => {
+        jest.spyOn(store.modules.approvals.actions, actionName).mockImplementation(() => {});
+      },
+    );
 
     ({ actions } = store.modules.approvals);
   });
@@ -181,6 +216,119 @@ describe('EE Approvals RuleForm', () => {
       });
     });
 
+    describe('when the rule is an external rule', () => {
+      describe('with initial rule', () => {
+        beforeEach(() => {
+          createComponent({
+            isMrEdit: false,
+            initRule: TEST_EXTERNAL_APPROVAL_RULE,
+          });
+        });
+
+        it('does not render the approver type select input', () => {
+          expect(findApproverTypeSelect().exists()).toBe(false);
+        });
+
+        it('on load, it populates the external URL', () => {
+          expect(findExternalUrlInput().element.value).toBe(
+            TEST_EXTERNAL_APPROVAL_RULE.externalUrl,
+          );
+        });
+      });
+
+      describe('without an initial rule', () => {
+        beforeEach(() => {
+          createComponent({
+            isMrEdit: false,
+          });
+          findApproverTypeSelect().vm.$emit('input', RULE_TYPE_EXTERNAL_APPROVAL);
+        });
+
+        it('renders the approver type select input', () => {
+          expect(findApproverTypeSelect().exists()).toBe(true);
+        });
+
+        it('renders the inputs for external rules', () => {
+          expect(findNameInput().exists()).toBe(true);
+          expect(findExternalUrlInput().exists()).toBe(true);
+          expect(findBranchesSelect().exists()).toBe(true);
+        });
+
+        it('does not render the user and group input fields', () => {
+          expect(findApprovalsRequiredInput().exists()).toBe(false);
+          expect(findApproversList().exists()).toBe(false);
+          expect(findApproversSelect().exists()).toBe(false);
+        });
+
+        it('at first, shows no validation', () => {
+          const inputs = findValidationForExternal();
+          const invalidInputs = inputs.filter((x) => !x.isValid);
+          const feedbacks = inputs.map((x) => x.feedback);
+
+          expect(invalidInputs.length).toBe(0);
+          expect(feedbacks.every((str) => !str.length)).toBe(true);
+        });
+
+        it('on submit, does not dispatch action', () => {
+          wrapper.vm.submit();
+
+          expect(actions.postExternalApprovalRule).not.toHaveBeenCalled();
+        });
+
+        it('on submit, shows name validation', async () => {
+          findExternalUrlInput().setValue('');
+
+          wrapper.vm.submit();
+
+          await nextTick();
+
+          expect(findExternalUrlValidation()).toEqual({
+            isValid: false,
+            feedback: 'Please provide a valid URL',
+          });
+        });
+
+        describe('with valid data', () => {
+          const branches = TEST_PROTECTED_BRANCHES.map((x) => x.id);
+          const expected = {
+            id: null,
+            name: 'Lorem',
+            externalUrl: 'https://gitlab.com/',
+            protectedBranchIds: branches,
+          };
+
+          beforeEach(() => {
+            findNameInput().setValue(expected.name);
+            findExternalUrlInput().setValue(expected.externalUrl);
+            wrapper.vm.branches = expected.protectedBranchIds;
+          });
+
+          it('on submit, posts external approval rule', () => {
+            wrapper.vm.submit();
+
+            expect(actions.postExternalApprovalRule).toHaveBeenCalledWith(
+              expect.anything(),
+              expected,
+            );
+          });
+
+          it('when submitted with a duplicate external URL, shows the "url already taken" validation', async () => {
+            store.state.settings.prefix = 'project-settings';
+            jest.spyOn(wrapper.vm, 'postExternalApprovalRule').mockRejectedValueOnce(urlTakenError);
+
+            wrapper.vm.submit();
+
+            await waitForPromises();
+
+            expect(findExternalUrlValidation()).toEqual({
+              isValid: false,
+              feedback: 'External url has already been taken',
+            });
+          });
+        });
+      });
+    });
+
     describe('without initRule', () => {
       beforeEach(() => {
         createComponent();
@@ -536,16 +684,17 @@ describe('EE Approvals RuleForm', () => {
 
     describe('with approval suggestions', () => {
       describe.each`
-        defaultRuleName          | expectedDisabledAttribute
-        ${'Vulnerability-Check'} | ${'disabled'}
-        ${'License-Check'}       | ${'disabled'}
-        ${'Foo Bar Baz'}         | ${undefined}
+        defaultRuleName          | expectedDisabledAttribute | approverTypeSelect
+        ${'Vulnerability-Check'} | ${'disabled'}             | ${false}
+        ${'License-Check'}       | ${'disabled'}             | ${false}
+        ${'Foo Bar Baz'}         | ${undefined}              | ${true}
       `(
         'with defaultRuleName set to $defaultRuleName',
-        ({ defaultRuleName, expectedDisabledAttribute }) => {
+        ({ defaultRuleName, expectedDisabledAttribute, approverTypeSelect }) => {
           beforeEach(() => {
             createComponent({
               initRule: null,
+              isMrEdit: false,
               defaultRuleName,
             });
           });
@@ -555,6 +704,12 @@ describe('EE Approvals RuleForm', () => {
           } the name text field`, () => {
             expect(findNameInput().attributes('disabled')).toBe(expectedDisabledAttribute);
           });
+
+          it(`${
+            approverTypeSelect ? 'renders' : 'does not render'
+          } the approver type select`, () => {
+            expect(findApproverTypeSelect().exists()).toBe(approverTypeSelect);
+          });
         },
       );
     });
@@ -727,4 +882,23 @@ describe('EE Approvals RuleForm', () => {
       });
     });
   });
+
+  describe('when the approval gates feature is disabled', () => {
+    it('does not render the approver type select input', async () => {
+      createComponent(
+        { isMrEdit: false },
+        {
+          provide: {
+            glFeatures: {
+              ffComplianceApprovalGates: false,
+            },
+          },
+        },
+      );
+
+      await nextTick();
+
+      expect(findApproverTypeSelect().exists()).toBe(false);
+    });
+  });
 });
diff --git a/ee/spec/frontend/approvals/mocks.js b/ee/spec/frontend/approvals/mocks.js
index 073ba48f11c7645c07981faf4dd4614ea467f0d3..9366c88fe35d63ddcd5d7ac0ffcc53e31db8ae28 100644
--- a/ee/spec/frontend/approvals/mocks.js
+++ b/ee/spec/frontend/approvals/mocks.js
@@ -1,3 +1,10 @@
+export const createExternalRule = () => ({
+  id: 9,
+  name: 'API Gate',
+  externalUrl: 'https://gitlab.com',
+  ruleType: 'external_approval',
+});
+
 export const createProjectRules = () => [
   {
     id: 1,
diff --git a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js
index c980041ab0c59ba5d6c4c2e1a315fba467a73c98..fcfb521666235a305df23d8bbd0900095da2126e 100644
--- a/ee/spec/frontend/integrations/jira/issues_list/mock_data.js
+++ b/ee/spec/frontend/integrations/jira/issues_list/mock_data.js
@@ -18,8 +18,8 @@ export const mockJiraIssue1 = {
   labels: [
     {
       name: 'backend',
-      color: '#EBECF0',
-      text_color: '#283856',
+      color: '#0052CC',
+      text_color: '#FFFFFF',
     },
   ],
   author: {
diff --git a/ee/spec/frontend/integrations/jira/issues_show/mock_data.js b/ee/spec/frontend/integrations/jira/issues_show/mock_data.js
index 652e461ed96865b40d763ec0f655dc061d9ae3ca..b37ada539a9e7f058befd2f136d06bafb7d80a10 100644
--- a/ee/spec/frontend/integrations/jira/issues_show/mock_data.js
+++ b/ee/spec/frontend/integrations/jira/issues_show/mock_data.js
@@ -20,8 +20,8 @@ export const mockJiraIssue = {
     {
       title: 'In Progress',
       description: 'Work that is still in progress',
-      color: '#EBECF0',
-      text_color: '#283856',
+      color: '#0052CC',
+      text_color: '#FFFFFF',
     },
   ],
   references: {
diff --git a/ee/spec/frontend/vulnerabilities/generic_report/report_row_spec.js b/ee/spec/frontend/vulnerabilities/generic_report/report_row_spec.js
deleted file mode 100644
index df25d8ed3aea5d3816359e142906c8f32c16095d..0000000000000000000000000000000000000000
--- a/ee/spec/frontend/vulnerabilities/generic_report/report_row_spec.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import ReportRow from 'ee/vulnerabilities/components/generic_report/report_row.vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
-describe('ee/vulnerabilities/components/generic_report/report_row.vue', () => {
-  let wrapper;
-
-  const createWrapper = ({ ...options } = {}) =>
-    extendedWrapper(
-      shallowMount(ReportRow, {
-        propsData: {
-          label: 'Foo',
-        },
-        ...options,
-      }),
-    );
-
-  it('renders the default slot', () => {
-    const slotContent = 'foo bar';
-    wrapper = createWrapper({ slots: { default: slotContent } });
-
-    expect(wrapper.findByTestId('reportContent').text()).toBe(slotContent);
-  });
-});
diff --git a/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js b/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js
index db65b2d2b05fe4fd4880921835a668b9976681ff..9f4702f007c096a8004f2e5deec505892d337843 100644
--- a/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js
+++ b/ee/spec/frontend/vulnerabilities/generic_report/report_section_spec.js
@@ -1,8 +1,6 @@
 import { within, fireEvent } from '@testing-library/dom';
 import { mount } from '@vue/test-utils';
 import { nextTick } from 'vue';
-import ReportItem from 'ee/vulnerabilities/components/generic_report/report_item.vue';
-import ReportRow from 'ee/vulnerabilities/components/generic_report/report_row.vue';
 import ReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
 import { REPORT_TYPE_URL } from 'ee/vulnerabilities/components/generic_report/types/constants';
 import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -47,9 +45,9 @@ describe('ee/vulnerabilities/components/generic_report/report_section.vue', () =
       name: /evidence/i,
     });
   const findReportsSection = () => wrapper.findByTestId('reports');
-  const findAllReportRows = () => wrapper.findAllComponents(ReportRow);
+  const findAllReportRows = () => wrapper.findAll('[data-testid*="report-row"]');
   const findReportRowByLabel = (label) => wrapper.findByTestId(`report-row-${label}`);
-  const findItemWithinRow = (row) => row.findComponent(ReportItem);
+  const findReportItemByLabel = (label) => wrapper.findByTestId(`report-item-${label}`);
   const supportedReportTypesLabels = Object.keys(TEST_DATA.supportedTypes);
 
   describe('with supported report types', () => {
@@ -77,20 +75,21 @@ describe('ee/vulnerabilities/components/generic_report/report_section.vue', () =
         expect(findAllReportRows()).toHaveLength(supportedReportTypesLabels.length);
       });
 
-      it.each(supportedReportTypesLabels)('passes the correct props to report row: %s', (label) => {
-        expect(findReportRowByLabel(label).props()).toMatchObject({
-          label: TEST_DATA.supportedTypes[label].name,
-        });
-      });
+      it.each(supportedReportTypesLabels)(
+        'renders the correct label for report row: %s',
+        (label) => {
+          expect(within(findReportRowByLabel(label).element).getByText(label)).toBeInstanceOf(
+            HTMLElement,
+          );
+        },
+      );
     });
 
     describe('report items', () => {
       it.each(supportedReportTypesLabels)(
         'passes the correct props to item for row: %s',
         (label) => {
-          const row = findReportRowByLabel(label);
-
-          expect(findItemWithinRow(row).props()).toMatchObject({
+          expect(findReportItemByLabel(label).props()).toMatchObject({
             item: TEST_DATA.supportedTypes[label],
           });
         },
diff --git a/ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb b/ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d4a4591f62ec171507b98d60f0c13cd2317dddd8
--- /dev/null
+++ b/ee/spec/graphql/resolvers/admin/cloud_licenses/current_license_resolver_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Admin::CloudLicenses::CurrentLicenseResolver do
+  include GraphqlHelpers
+
+  specify do
+    expect(described_class).to have_nullable_graphql_type(::Types::Admin::CloudLicenses::CurrentLicenseType)
+  end
+
+  describe '#resolve' do
+    subject(:result) { resolve_current_license }
+
+    let_it_be(:admin) { create(:admin) }
+    let_it_be(:license) { create_current_license }
+
+    def resolve_current_license(current_user: admin)
+      resolve(described_class, ctx: { current_user: current_user })
+    end
+
+    before do
+      stub_application_setting(cloud_license_enabled: true)
+    end
+
+    context 'when application setting for cloud license is disabled', :enable_admin_mode do
+      it 'returns nil' do
+        stub_application_setting(cloud_license_enabled: false)
+
+        expect(result).to be_nil
+      end
+    end
+
+    context 'when current user is unauthorized' do
+      it 'raises error' do
+        unauthorized_user = create(:user)
+
+        expect do
+          resolve_current_license(current_user: unauthorized_user)
+        end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+      end
+    end
+
+    context 'when there is no current license', :enable_admin_mode do
+      it 'returns nil' do
+        License.delete_all # delete existing license
+
+        expect(result).to be_nil
+      end
+    end
+
+    it 'returns the current license', :enable_admin_mode do
+      expect(result).to eq(license)
+    end
+  end
+end
diff --git a/ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb b/ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..63b2229fd25c6db8e2cc3fa2e8d39b998cc1e1b1
--- /dev/null
+++ b/ee/spec/graphql/resolvers/admin/cloud_licenses/license_history_entries_resolver_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Admin::CloudLicenses::LicenseHistoryEntriesResolver do
+  include GraphqlHelpers
+
+  describe '#resolve' do
+    subject(:result) { resolve_license_history_entries }
+
+    let_it_be(:admin) { create(:admin) }
+
+    def create_license(data: {}, license_options: { created_at: Time.current })
+      gl_license = create(:gitlab_license, data)
+      create(:license, license_options.merge(data: gl_license.export))
+    end
+
+    def resolve_license_history_entries(current_user: admin)
+      resolve(described_class, ctx: { current_user: current_user })
+    end
+
+    before do
+      stub_application_setting(cloud_license_enabled: true)
+    end
+
+    context 'when application setting for cloud license is disabled', :enable_admin_mode do
+      it 'returns nil' do
+        stub_application_setting(cloud_license_enabled: false)
+
+        expect(result).to be_nil
+      end
+    end
+
+    context 'when current user is unauthorized' do
+      it 'raises error' do
+        unauthorized_user = create(:user)
+
+        expect do
+          resolve_license_history_entries(current_user: unauthorized_user)
+        end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+      end
+    end
+
+    context 'when no licenses exist' do
+      it 'returns an empty array', :enable_admin_mode do
+        License.delete_all # delete license created with ee/spec/support/test_license.rb
+
+        expect(result).to eq([])
+      end
+    end
+
+    it 'returns the license history entries', :enable_admin_mode do
+      today = Date.current
+      type = License::CLOUD_LICENSE_TYPE
+
+      past_license = create_license(
+        data: { starts_at: today - 1.month, expires_at: today + 11.months },
+        license_options: { created_at: Time.current - 1.month }
+      )
+      expired_license = create_license(data: { starts_at: today - 1.year, expires_at: today - 1.month })
+      another_license = create_license(data: { starts_at: today - 1.month, expires_at: today + 1.year })
+      future_license = create_license(data: { starts_at: today + 1.month, expires_at: today + 13.months, type: type })
+      current_license = create_license(data: { starts_at: today - 15.days, expires_at: today + 11.months, type: type })
+
+      expect(result).to eq(
+        [
+          future_license,
+          current_license,
+          another_license,
+          past_license,
+          expired_license,
+          License.first # created with ee/spec/support/test_license.rb
+        ]
+      )
+    end
+  end
+end
diff --git a/ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb b/ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cff1ae38b7556b1c577e6883ebaa04b256e520ab
--- /dev/null
+++ b/ee/spec/graphql/types/admin/cloud_licenses/current_license_type_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CurrentLicense'], :enable_admin_mode do
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:licensee) do
+    {
+      'Name' => 'User Example',
+      'Email' => 'user@example.com',
+      'Company' => 'Example Inc.'
+    }
+  end
+
+  let_it_be(:license) { create_current_license(licensee: licensee, type: License::CLOUD_LICENSE_TYPE) }
+
+  let(:fields) do
+    %w[last_sync billable_users_count maximum_user_count users_over_license_count]
+  end
+
+  def query(field_name)
+    %(
+      {
+        currentLicense {
+          #{field_name}
+        }
+      }
+    )
+  end
+
+  def query_field(field_name)
+    GitlabSchema.execute(query(field_name), context: { current_user: admin }).as_json
+  end
+
+  before do
+    stub_application_setting(cloud_license_enabled: true)
+  end
+
+  it { expect(described_class.graphql_name).to eq('CurrentLicense') }
+  it { expect(described_class).to include_graphql_fields(*fields) }
+
+  include_examples 'license type fields', %w[data currentLicense]
+
+  describe "#users_over_license_count" do
+    context 'when license is for a trial' do
+      it 'returns 0' do
+        create_current_license(licensee: licensee, restrictions: { trial: true })
+
+        result_as_json = query_field('usersOverLicenseCount')
+
+        expect(result_as_json['data']['currentLicense']['usersOverLicenseCount']).to eq(0)
+      end
+    end
+
+    it 'returns the number of users over the paid users in the license' do
+      create(:historical_data, active_user_count: 15)
+      create_current_license(licensee: licensee, restrictions: { active_user_count: 10 })
+
+      result_as_json = query_field('usersOverLicenseCount')
+
+      expect(result_as_json['data']['currentLicense']['usersOverLicenseCount']).to eq(5)
+    end
+  end
+end
diff --git a/ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb b/ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..03552ebb428913cbd873112c560ca4c15a212468
--- /dev/null
+++ b/ee/spec/graphql/types/admin/cloud_licenses/license_history_entry_type_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['LicenseHistoryEntry'], :enable_admin_mode do
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:licensee) do
+    {
+      'Name' => 'User Example',
+      'Email' => 'user@example.com',
+      'Company' => 'Example Inc.'
+    }
+  end
+
+  let_it_be(:license) { create_current_license(licensee: licensee, type: License::CLOUD_LICENSE_TYPE) }
+
+  def query(field_name)
+    %(
+      {
+        licenseHistoryEntries {
+          nodes {
+            #{field_name}
+          }
+        }
+      }
+    )
+  end
+
+  def query_field(field_name)
+    GitlabSchema.execute(query(field_name), context: { current_user: admin }).as_json
+  end
+
+  before do
+    stub_application_setting(cloud_license_enabled: true)
+  end
+
+  it { expect(described_class.graphql_name).to eq('LicenseHistoryEntry') }
+
+  include_examples 'license type fields', ['data', 'licenseHistoryEntries', 'nodes', -1]
+end
diff --git a/ee/spec/graphql/types/query_type_spec.rb b/ee/spec/graphql/types/query_type_spec.rb
index 06a4ed474a5ca0ea8c00e0063e203a5c854a7594..767a5e9e01b9a71e61cfff98b78662415d521219 100644
--- a/ee/spec/graphql/types/query_type_spec.rb
+++ b/ee/spec/graphql/types/query_type_spec.rb
@@ -10,7 +10,9 @@
       :vulnerabilities,
       :vulnerability,
       :instance_security_dashboard,
-      :vulnerabilities_count_by_day_and_severity
+      :vulnerabilities_count_by_day_and_severity,
+      :current_license,
+      :license_history_entries
     ).at_least
   end
 end
diff --git a/ee/spec/initializers/lograge_spec.rb b/ee/spec/initializers/lograge_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7bbbdb184297f5707c7a57b328fbb906acbc0aac
--- /dev/null
+++ b/ee/spec/initializers/lograge_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'lograge', type: :request do
+  context 'with a log subscriber' do
+    include_context 'parsed logs'
+    include_context 'clear DB Load Balancing configuration'
+
+    let(:subscriber) { Lograge::LogSubscribers::ActionController.new }
+
+    let(:event) do
+      ActiveSupport::Notifications::Event.new(
+        'process_action.action_controller',
+        Time.now,
+        Time.now,
+        2,
+        status: 200,
+        controller: 'HomeController',
+        action: 'index',
+        format: 'application/json',
+        method: 'GET',
+        path: '/home?foo=bar',
+        params: {},
+        db_runtime: 0.02,
+        view_runtime: 0.01
+      )
+    end
+
+    let(:logging_keys) do
+      %w[db_primary_wal_count
+         db_replica_wal_count
+         db_replica_count
+         db_replica_cached_count
+         db_primary_count
+         db_primary_cached_count
+         db_primary_duration_s
+         db_replica_duration_s]
+    end
+
+    context 'when load balancing is enabled' do
+      before do
+        allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+      end
+
+      context 'with db payload' do
+        context 'when RequestStore is enabled', :request_store do
+          it 'includes db counters' do
+            subscriber.process_action(event)
+            expect(log_data).to include(*logging_keys)
+          end
+        end
+
+        context 'when RequestStore is disabled' do
+          it 'does not include db counters' do
+            subscriber.process_action(event)
+
+            expect(log_data).not_to include(*logging_keys)
+          end
+        end
+      end
+    end
+
+    context 'when load balancing is disabled' do
+      before do
+        allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+      end
+
+      it 'does not include db counters' do
+        subscriber.process_action(event)
+
+        expect(log_data).not_to include(*logging_keys)
+      end
+    end
+  end
+end
diff --git a/ee/spec/lib/ee/gitlab/sidekiq_middleware/server_metrics_spec.rb b/ee/spec/lib/ee/gitlab/sidekiq_middleware/server_metrics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..54f6905047905f125741917b3d4be73fbb292318
--- /dev/null
+++ b/ee/spec/lib/ee/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# rubocop: disable RSpec/MultipleMemoizedHelpers
+RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
+  using RSpec::Parameterized::TableSyntax
+
+  subject { described_class.new }
+
+  let(:queue) { :test }
+  let(:worker_class) { worker.class }
+  let(:job) { {} }
+  let(:job_status) { :done }
+  let(:labels_with_job_status) { default_labels.merge(job_status: job_status.to_s) }
+  let(:default_labels) do
+    { queue: queue.to_s,
+      worker: worker_class.to_s,
+      boundary: "",
+      external_dependencies: "no",
+      feature_category: "",
+      urgency: "low" }
+  end
+
+  before do
+    stub_const('TestWorker', Class.new)
+    TestWorker.class_eval do
+      include Sidekiq::Worker
+      include WorkerAttributes
+    end
+  end
+
+  let(:worker) { TestWorker.new }
+
+  include_context 'server metrics with mocked prometheus'
+
+  context 'when load_balancing is enabled' do
+    let(:load_balancing_metric) { double('load balancing metric') }
+
+    include_context 'clear DB Load Balancing configuration'
+
+    before do
+      allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+      allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
+    end
+
+    describe '#initialize' do
+      it 'sets load_balancing metrics' do
+        expect(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
+
+        subject
+      end
+    end
+
+    describe '#call' do
+      include_context 'server metrics call'
+
+      context 'when :database_chosen is provided' do
+        where(:database_chosen) do
+          %w[primary retry replica]
+        end
+
+        with_them do
+          context "when #{params[:database_chosen]} is used" do
+            let(:labels_with_load_balancing) do
+              labels_with_job_status.merge(database_chosen: database_chosen, data_consistency: 'delayed')
+            end
+
+            before do
+              job[:database_chosen] = database_chosen
+              job[:data_consistency] = 'delayed'
+              allow(load_balancing_metric).to receive(:increment)
+            end
+
+            it 'increment sidekiq_load_balancing_count' do
+              expect(load_balancing_metric).to receive(:increment).with(labels_with_load_balancing, 1)
+
+              described_class.new.call(worker, job, :test) { nil }
+            end
+          end
+        end
+      end
+
+      context 'when :database_chosen is not provided' do
+        it 'does not increment sidekiq_load_balancing_count' do
+          expect(load_balancing_metric).not_to receive(:increment)
+
+          described_class.new.call(worker, job, :test) { nil }
+        end
+      end
+    end
+  end
+
+  context 'when load_balancing is disabled' do
+    include_context 'clear DB Load Balancing configuration'
+
+    before do
+      allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+    end
+
+    describe '#initialize' do
+      it 'doesnt set load_balancing metrics' do
+        expect(Gitlab::Metrics).not_to receive(:counter).with(:sidekiq_load_balancing_count, anything)
+
+        subject
+      end
+    end
+  end
+end
diff --git a/ee/spec/lib/gitlab/instrumentation_helper_spec.rb b/ee/spec/lib/gitlab/instrumentation_helper_spec.rb
index 6b61425b0615b23cfef5750d38b0edfec1201135..6b096d9ec5b8124a85e27d56029303dac81fffe6 100644
--- a/ee/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/ee/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -21,7 +21,9 @@
         expect(payload).to include(db_replica_count: 0,
                                    db_replica_cached_count: 0,
                                    db_primary_count: 0,
-                                   db_primary_cached_count: 0)
+                                   db_primary_cached_count: 0,
+                                   db_primary_wal_count: 0,
+                                   db_replica_wal_count: 0)
       end
     end
 
@@ -30,13 +32,15 @@
         allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
       end
 
-      it 'includes DB counts' do
+      it 'does not include DB counts' do
         subject
 
         expect(payload).not_to include(db_replica_count: 0,
                                    db_replica_cached_count: 0,
                                    db_primary_count: 0,
-                                   db_primary_cached_count: 0)
+                                   db_primary_cached_count: 0,
+                                   db_primary_wal_count: 0,
+                                   db_replica_wal_count: 0)
       end
     end
 
diff --git a/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index d83ddc96d1d52609ef32736b732338e822b866ab..e74f56aad00f81cce2d3d3cdf14f1564b4b2a065 100644
--- a/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/ee/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -29,18 +29,20 @@ def sql(query, comments: true)
   end
 
   shared_examples 'track sql events for each role' do
-    where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query) do
-      'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false
-      'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false
-      'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false
-      'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false
-      'SQL' | 'DELETE FROM users where id = 10' | true | true | false
-      'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false
-      'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false
-      'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true
-      'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false
-      nil | 'BEGIN' | false | false | false
-      nil | 'COMMIT' | false | false | false
+    where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query, :record_wal_query) do
+      'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false | false
+      'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false | false
+      'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false | false
+      'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false | false
+      'SQL' | 'DELETE FROM users where id = 10' | true | true | false | false
+      'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false | false
+      'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false | false
+      'SQL' | 'SELECT pg_current_wal_insert_lsn()::text AS location' | true | false | false | true
+      'SQL' | 'SELECT pg_last_wal_replay_lsn()::text AS location' | true | false | false | true
+      'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true | false
+      'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false | false
+      nil | 'BEGIN' | false | false | false | false
+      nil | 'COMMIT' | false | false | false | false
     end
 
     with_them do
diff --git a/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 791b1ebedc0d4354661b8094c1200111e4e27d02..d94d70641560c971fc43f97dd9c71e844a2b06fa 100644
--- a/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/ee/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -76,7 +76,9 @@
             'db_replica_count' => 0,
             'db_replica_cached_count' => 0,
             'db_primary_count' => a_value >= 1,
-            'db_primary_cached_count' => 0
+            'db_primary_cached_count' => 0,
+            'db_primary_wal_count' => 0,
+            'db_replica_wal_count' => 0
           )
         end
 
@@ -94,7 +96,9 @@
             'db_replica_count' => 0,
             'db_replica_cached_count' => 0,
             'db_primary_count' => 0,
-            'db_primary_cached_count' => 0
+            'db_primary_cached_count' => 0,
+            'db_primary_wal_count' => 0,
+            'db_replica_wal_count' => 0
           )
         end
 
diff --git a/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
index daead45ade74ca00d83c6e3d184e296f35d605c0..9832e9eceea71f412f510d6b22b42fcec135bc37 100644
--- a/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
+++ b/ee/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
@@ -9,7 +9,9 @@
         :db_replica_count,
         :db_replica_cached_count,
         :db_primary_count,
-        :db_primary_cached_count
+        :db_primary_cached_count,
+        :db_primary_wal_count,
+        :db_replica_wal_count
       ]
 
       expect(described_class.keys).to include(*expected_keys)
diff --git a/ee/spec/models/license_spec.rb b/ee/spec/models/license_spec.rb
index 5b1bc73f2e680089e4c4fadabb0acbd5c27c3c43..ea82896f72843f11b2e1b8bf297f25c0c98328c2 100644
--- a/ee/spec/models/license_spec.rb
+++ b/ee/spec/models/license_spec.rb
@@ -1411,6 +1411,20 @@ def set_restrictions(opts)
     end
   end
 
+  describe '#license_type' do
+    subject { license.license_type }
+
+    context 'when the license is not a cloud license' do
+      it { is_expected.to eq(described_class::LEGACY_LICENSE_TYPE) }
+    end
+
+    context 'when the license is a cloud license' do
+      let(:gl_license) { build(:gitlab_license, type: described_class::CLOUD_LICENSE_TYPE) }
+
+      it { is_expected.to eq(described_class::CLOUD_LICENSE_TYPE) }
+    end
+  end
+
   describe '#auto_renew' do
     it 'is false' do
       expect(license.auto_renew).to be false
@@ -1485,4 +1499,28 @@ def set_restrictions(opts)
       it { is_expected.to eq(result) }
     end
   end
+
+  describe '#licensee_name' do
+    subject { license.licensee_name }
+
+    let(:gl_license) { build(:gitlab_license, licensee: { 'Name' => 'User Example' }) }
+
+    it { is_expected.to eq('User Example') }
+  end
+
+  describe '#licensee_email' do
+    subject { license.licensee_email }
+
+    let(:gl_license) { build(:gitlab_license, licensee: { 'Email' => 'user@example.com' }) }
+
+    it { is_expected.to eq('user@example.com') }
+  end
+
+  describe '#licensee_company' do
+    subject { license.licensee_company }
+
+    let(:gl_license) { build(:gitlab_license, licensee: { 'Company' => 'Example Inc.' }) }
+
+    it { is_expected.to eq('Example Inc.') }
+  end
 end
diff --git a/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb b/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb
index 616048882b4ccfca57e4aae25e84e6bca75f8944..00dca7094e953c8c2106c17a1420744b6a9837d8 100644
--- a/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb
+++ b/ee/spec/serializers/integrations/jira/issue_detail_entity_spec.rb
@@ -88,8 +88,8 @@
         {
           title: 'backend',
           name: 'backend',
-          color: '#EBECF0',
-          text_color: '#283856'
+          color: '#0052CC',
+          text_color: '#FFFFFF'
         }
       ],
       author: hash_including(
diff --git a/ee/spec/serializers/integrations/jira/issue_entity_spec.rb b/ee/spec/serializers/integrations/jira/issue_entity_spec.rb
index daa85b475be0a62a63810a255ff64396af010dcb..ac457b8979120cb36ff71a0e36bd27c82fc6ae21 100644
--- a/ee/spec/serializers/integrations/jira/issue_entity_spec.rb
+++ b/ee/spec/serializers/integrations/jira/issue_entity_spec.rb
@@ -55,8 +55,8 @@
         {
           title: 'backend',
           name: 'backend',
-          color: '#EBECF0',
-          text_color: '#283856'
+          color: '#0052CC',
+          text_color: '#FFFFFF'
         }
       ],
       author: hash_including(
diff --git a/ee/spec/services/ee/merge_requests/update_service_spec.rb b/ee/spec/services/ee/merge_requests/update_service_spec.rb
index 44f079fc72078f498401eaf2a4962aa41a7b2338..9665aa888a5b18d5457a13e4f5bf3a7c72a5fbec 100644
--- a/ee/spec/services/ee/merge_requests/update_service_spec.rb
+++ b/ee/spec/services/ee/merge_requests/update_service_spec.rb
@@ -302,12 +302,13 @@ def update_merge_request(opts)
       end
     end
 
-    it 'updates code owner approval rules' do
-      expect_next_instance_of(::MergeRequests::SyncCodeOwnerApprovalRules) do |instance|
-        expect(instance).to receive(:execute)
-      end
+    context 'when called inside an ActiveRecord transaction' do
+      it 'does not attempt to update code owner approval rules' do
+        allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
+        expect(::MergeRequests::SyncCodeOwnerApprovalRulesWorker).not_to receive(:perform_async)
 
-      update_merge_request(title: 'Title')
+        update_merge_request(title: 'Title')
+      end
     end
 
     context 'updating reviewers_ids' do
diff --git a/ee/spec/services/milestones/destroy_service_spec.rb b/ee/spec/services/milestones/destroy_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..35872d1c16d00383d5d1d3ccf568602b313129b9
--- /dev/null
+++ b/ee/spec/services/milestones/destroy_service_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Milestones::DestroyService do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :repository) }
+  let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) }
+
+  before do
+    project.add_maintainer(user)
+  end
+
+  def service
+    described_class.new(project, user, {})
+  end
+
+  describe '#execute' do
+    context 'with an existing merge request' do
+      let!(:issue) { create(:issue, project: project, milestone: milestone) }
+      let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
+
+      it 'manually queues MergeRequests::SyncCodeOwnerApprovalRulesWorker jobs' do
+        expect(::MergeRequests::SyncCodeOwnerApprovalRulesWorker).to receive(:perform_async)
+
+        service.execute(milestone)
+      end
+    end
+  end
+end
diff --git a/ee/spec/support/shared_examples/graphql/types/admin/cloud_licenses/license_type_shared_examples.rb b/ee/spec/support/shared_examples/graphql/types/admin/cloud_licenses/license_type_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..88afbcb1d4fdd3856bb4ccaefde9b519e74c2faf
--- /dev/null
+++ b/ee/spec/support/shared_examples/graphql/types/admin/cloud_licenses/license_type_shared_examples.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'license type fields' do |keys|
+  context 'with license type fields' do
+    let(:license_fields) do
+      %w[id type plan name email company starts_at expires_at activated_at users_in_license_count]
+    end
+
+    it { expect(described_class).to include_graphql_fields(*license_fields) }
+  end
+end
diff --git a/ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb b/ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2d6a6c365d8f2e9ff20e28352325fa4330607309
--- /dev/null
+++ b/ee/spec/workers/merge_requests/sync_code_owner_approval_rules_worker_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe MergeRequests::SyncCodeOwnerApprovalRulesWorker do
+  let_it_be(:merge_request) { create(:merge_request) }
+
+  subject { described_class.new }
+
+  describe "#perform" do
+    it_behaves_like 'an idempotent worker' do
+      let(:job_args) { [merge_request.id] }
+    end
+
+    context "when merge request is not found" do
+      it "returns without attempting to sync code owner rules" do
+        expect(MergeRequests::SyncCodeOwnerApprovalRules).not_to receive(:new)
+
+        subject.perform(non_existing_record_id)
+      end
+    end
+
+    context "when merge request is found" do
+      it "attempts to sync code owner rules" do
+        expect_next_instance_of(::MergeRequests::SyncCodeOwnerApprovalRules) do |instance|
+          expect(instance).to receive(:execute)
+        end
+
+        subject.perform(merge_request.id)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 19462e6cb023d9dae211f57875bfa213fd3116c5..b7b6343aec041226d6b7b4297aeadaf26e072b1b 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -82,6 +82,30 @@ def overflow?
         !!@overflow
       end
 
+      def overflow_max_lines?
+        !!@overflow_max_lines
+      end
+
+      def overflow_max_bytes?
+        !!@overflow_max_bytes
+      end
+
+      def overflow_max_files?
+        !!@overflow_max_files
+      end
+
+      def collapsed_safe_lines?
+        !!@collapsed_safe_lines
+      end
+
+      def collapsed_safe_files?
+        !!@collapsed_safe_files
+      end
+
+      def collapsed_safe_bytes?
+        !!@collapsed_safe_bytes
+      end
+
       def size
         @size ||= count # forces a loop using each method
       end
@@ -121,7 +145,15 @@ def populate!
       end
 
       def over_safe_limits?(files)
-        files >= safe_max_files || @line_count > safe_max_lines || @byte_count >= safe_max_bytes
+        if files >= safe_max_files
+          @collapsed_safe_files = true
+        elsif @line_count > safe_max_lines
+          @collapsed_safe_lines = true
+        elsif @byte_count >= safe_max_bytes
+          @collapsed_safe_bytes = true
+        end
+
+        @collapsed_safe_files || @collapsed_safe_lines || @collapsed_safe_bytes
       end
 
       def expand_diff?
@@ -154,6 +186,7 @@ def each_serialized_patch
 
           if @enforce_limits && i >= max_files
             @overflow = true
+            @overflow_max_files = true
             break
           end
 
@@ -166,10 +199,19 @@ def each_serialized_patch
           @line_count += diff.line_count
           @byte_count += diff.diff.bytesize
 
-          if @enforce_limits && (@line_count >= max_lines || @byte_count >= max_bytes)
+          if @enforce_limits && @line_count >= max_lines
+            # This last Diff instance pushes us over the lines limit. We stop and
+            # discard it.
+            @overflow = true
+            @overflow_max_lines = true
+            break
+          end
+
+          if @enforce_limits && @byte_count >= max_bytes
             # This last Diff instance pushes us over the lines limit. We stop and
             # discard it.
             @overflow = true
+            @overflow_max_bytes = true
             break
           end
 
diff --git a/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb b/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb
index db7cd6a8679bdb01f81c3bb2a78f0208dac0571f..b542aa4fe4c701c77b5ec82c85bdab1e0e81ea17 100644
--- a/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb
+++ b/lib/gitlab/sidekiq_middleware/instrumentation_logger.rb
@@ -13,7 +13,6 @@ def self.keys
           :elasticsearch_calls,
           :elasticsearch_duration_s,
           :elasticsearch_timed_out_count,
-          :worker_data_consistency,
           *::Gitlab::Memory::Instrumentation::KEY_MAPPING.values,
           *::Gitlab::Instrumentation::Redis.known_payload_keys,
           *::Gitlab::Metrics::Subscribers::ActiveRecord.known_payload_keys,
diff --git a/lib/gitlab/sidekiq_middleware/server_metrics.rb b/lib/gitlab/sidekiq_middleware/server_metrics.rb
index 3d16cc70bf849c87481a5cd86e9859d2270b6f44..f5fee8050ac2afd1654b1508316db68db6e31b27 100644
--- a/lib/gitlab/sidekiq_middleware/server_metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/server_metrics.rb
@@ -21,6 +21,16 @@ def call(worker, job, queue)
         Thread.current.name ||= Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME
 
         labels = create_labels(worker.class, queue, job)
+        instrument(job, labels) do
+          yield
+        end
+      end
+
+      protected
+
+      attr_reader :metrics
+
+      def instrument(job, labels)
         queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
 
         @metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration
@@ -62,8 +72,6 @@ def call(worker, job, queue)
         end
       end
 
-      private
-
       def init_metrics
         {
           sidekiq_jobs_cpu_seconds:                ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
@@ -82,6 +90,8 @@ def init_metrics
         }
       end
 
+      private
+
       def get_thread_cputime
         defined?(Process::CLOCK_THREAD_CPUTIME_ID) ? Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) : 0
       end
@@ -108,3 +118,5 @@ def get_gitaly_time(payload)
     end
   end
 end
+
+Gitlab::SidekiqMiddleware::ServerMetrics.prepend_if_ee('EE::Gitlab::SidekiqMiddleware::ServerMetrics')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c5bb6cb8737fe9fe1101996905640f0bf6c4aa9e..cfd057403db03d51cac2cbe034752d9605bb8fe2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2327,6 +2327,9 @@ msgstr ""
 msgid "AdminSettings|Required pipeline configuration"
 msgstr ""
 
+msgid "AdminSettings|See affected service templates"
+msgstr ""
+
 msgid "AdminSettings|Select a pipeline configuration file"
 msgstr ""
 
@@ -2363,6 +2366,9 @@ msgstr ""
 msgid "AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}."
 msgstr ""
 
+msgid "AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings &gt; Integrations.%{link_end}"
+msgstr ""
+
 msgid "AdminStatistics|Active Users"
 msgstr ""
 
@@ -3982,6 +3988,9 @@ msgstr ""
 msgid "Applying suggestions..."
 msgstr ""
 
+msgid "Approval Gate"
+msgstr ""
+
 msgid "Approval Status"
 msgstr ""
 
@@ -4004,6 +4013,15 @@ msgid_plural "ApprovalRuleRemove|Approvals from these members are not revoked."
 msgstr[0] ""
 msgstr[1] ""
 
+msgid "ApprovalRuleRemove|Remove approval gate"
+msgstr ""
+
+msgid "ApprovalRuleRemove|Remove approval gate?"
+msgstr ""
+
+msgid "ApprovalRuleRemove|You are about to remove the %{name} approval gate. Approval from this service is not revoked."
+msgstr ""
+
 msgid "ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}."
 msgstr ""
 
@@ -4017,21 +4035,36 @@ msgid_plural "ApprovalRuleSummary|%{count} approvals required from %{membersCoun
 msgstr[0] ""
 msgstr[1] ""
 
+msgid "ApprovalRule|Add approvel gate"
+msgstr ""
+
 msgid "ApprovalRule|Add approvers"
 msgstr ""
 
 msgid "ApprovalRule|Approval rules"
 msgstr ""
 
+msgid "ApprovalRule|Approval service API"
+msgstr ""
+
 msgid "ApprovalRule|Approvals required"
 msgstr ""
 
+msgid "ApprovalRule|Approvel gate"
+msgstr ""
+
+msgid "ApprovalRule|Approver Type"
+msgstr ""
+
 msgid "ApprovalRule|Approvers"
 msgstr ""
 
 msgid "ApprovalRule|Examples: QA, Security."
 msgstr ""
 
+msgid "ApprovalRule|Invoke an external API as part of the approvals"
+msgstr ""
+
 msgid "ApprovalRule|Name"
 msgstr ""
 
@@ -4041,6 +4074,9 @@ msgstr ""
 msgid "ApprovalRule|Target branch"
 msgstr ""
 
+msgid "ApprovalRule|Users or groups"
+msgstr ""
+
 msgid "ApprovalStatusTooltip|Adheres to separation of duties"
 msgstr ""
 
@@ -11256,7 +11292,7 @@ msgstr ""
 msgid "DiscordService|Discord Notifications"
 msgstr ""
 
-msgid "DiscordService|Receive event notifications in Discord"
+msgid "DiscordService|Send notifications about project events to a Discord channel."
 msgstr ""
 
 msgid "Discover GitLab Geo"
@@ -13013,6 +13049,9 @@ msgstr ""
 msgid "External storage authentication token"
 msgstr ""
 
+msgid "External url has already been taken"
+msgstr ""
+
 msgid "ExternalAuthorizationService|Classification label"
 msgstr ""
 
@@ -18672,6 +18711,9 @@ msgstr ""
 msgid "Leave edit mode? All unsaved changes will be lost."
 msgstr ""
 
+msgid "Leave feedback"
+msgstr ""
+
 msgid "Leave group"
 msgstr ""
 
@@ -28298,6 +28340,9 @@ msgstr ""
 msgid "Send notifications about project events to Mattermost channels. %{docs_link}"
 msgstr ""
 
+msgid "Send notifications about project events to a Discord channel. %{docs_link}"
+msgstr ""
+
 msgid "Send report"
 msgstr ""
 
diff --git a/spec/features/callouts/service_templates_deprecation_spec.rb b/spec/features/callouts/service_templates_deprecation_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6403b54e290d4594b8341ead766b3bc4e674b2e
--- /dev/null
+++ b/spec/features/callouts/service_templates_deprecation_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Service templates deprecation callout' do
+  let_it_be(:admin) { create(:admin) }
+  let_it_be(:non_admin) { create(:user) }
+  let_it_be(:callout_content) { 'Service templates are deprecated and will be removed in GitLab 14.0.' }
+
+  context 'when a non-admin is logged in' do
+    before do
+      sign_in(non_admin)
+      visit root_dashboard_path
+    end
+
+    it 'does not display callout' do
+      expect(page).not_to have_content callout_content
+    end
+  end
+
+  context 'when an admin is logged in' do
+    before do
+      sign_in(admin)
+      gitlab_enable_admin_mode_sign_in(admin)
+
+      visit root_dashboard_path
+    end
+
+    context 'with no active service templates' do
+      it 'does not display callout' do
+        expect(page).not_to have_content callout_content
+      end
+    end
+
+    context 'with active service template' do
+      before do
+        create(:service, :template, type: 'MattermostService', active: true)
+        visit root_dashboard_path
+      end
+
+      it 'displays callout' do
+        expect(page).to have_content callout_content
+        expect(page).to have_link 'See affected service templates', href: admin_application_settings_services_path
+      end
+
+      context 'when callout is dismissed', :js do
+        before do
+          find('[data-testid="close-service-templates-deprecated-callout"]').click
+
+          visit root_dashboard_path
+        end
+
+        it 'does not display callout' do
+          expect(page).not_to have_content callout_content
+        end
+      end
+    end
+  end
+end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 20fa8d628841aa793b5e353fcafdc09c2ca62a2a..dfea1020c5294fbcf440423d5db4a0de8f11fd17 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -291,6 +291,8 @@
   end
 
   describe '#render_overflow_warning?' do
+    using RSpec::Parameterized::TableSyntax
+
     let(:diffs_collection) { instance_double(Gitlab::Diff::FileCollection::MergeRequestDiff, raw_diff_files: diff_files) }
     let(:diff_files) { Gitlab::Git::DiffCollection.new(files) }
     let(:safe_file) { { too_large: false, diff: '' } }
@@ -299,13 +301,42 @@
 
     before do
       allow(diff_files).to receive(:overflow?).and_return(false)
+      allow(diff_files).to receive(:overflow_max_bytes?).and_return(false)
+      allow(diff_files).to receive(:overflow_max_files?).and_return(false)
+      allow(diff_files).to receive(:overflow_max_lines?).and_return(false)
+      allow(diff_files).to receive(:collapsed_safe_bytes?).and_return(false)
+      allow(diff_files).to receive(:collapsed_safe_files?).and_return(false)
+      allow(diff_files).to receive(:collapsed_safe_lines?).and_return(false)
     end
 
-    context 'when neither collection nor individual file hit the limit' do
+    context 'when no limits are hit' do
       it 'returns false and does not log any overflow events' do
         expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collection_limits)
         expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_single_file_limits)
+        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_max_bytes_limits)
+        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_max_files_limits)
+        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_max_lines_limits)
+        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collapsed_bytes_limits)
+        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collapsed_files_limits)
+        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_collapsed_lines_limits)
+
+        expect(render_overflow_warning?(diffs_collection)).to be false
+      end
+    end
+
+    where(:overflow_method, :event_name) do
+      :overflow_max_bytes?      | :diffs_overflow_max_bytes_limits
+      :overflow_max_files?      | :diffs_overflow_max_files_limits
+      :overflow_max_lines?      | :diffs_overflow_max_lines_limits
+      :collapsed_safe_bytes?    | :diffs_overflow_collapsed_bytes_limits
+      :collapsed_safe_files?    | :diffs_overflow_collapsed_files_limits
+      :collapsed_safe_lines?    | :diffs_overflow_collapsed_lines_limits
+    end
 
+    with_them do
+      it 'returns false and only logs the correct collection overflow event' do
+        allow(diff_files).to receive(overflow_method).and_return(true)
+        expect(Gitlab::Metrics).to receive(:add_event).with(event_name).once
         expect(render_overflow_warning?(diffs_collection)).to be false
       end
     end
@@ -315,9 +346,8 @@
         allow(diff_files).to receive(:overflow?).and_return(true)
       end
 
-      it 'returns false and only logs collection overflow event' do
-        expect(Gitlab::Metrics).to receive(:add_event).with(:diffs_overflow_collection_limits).exactly(:once)
-        expect(Gitlab::Metrics).not_to receive(:add_event).with(:diffs_overflow_single_file_limits)
+      it 'returns true and only logs all the correct collection overflow event' do
+        expect(Gitlab::Metrics).to receive(:add_event).with(:diffs_overflow_collection_limits).once
 
         expect(render_overflow_warning?(diffs_collection)).to be true
       end
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index a2d9495ce6c280db7fd94dc32ba3744faf515c9f..3dbaa655aebc909c7d74c95c39ef4adcc0915329 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -81,23 +81,31 @@
     end
   end
 
-  describe '.show_service_templates_deprecated?' do
-    subject { helper.show_service_templates_deprecated? }
+  describe '.show_service_templates_deprecated_callout?' do
+    using RSpec::Parameterized::TableSyntax
 
-    context 'when user has not dismissed' do
-      before do
-        allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED) { false }
-      end
+    let_it_be(:admin) { create(:user, :admin) }
+    let_it_be(:non_admin) { create(:user) }
 
-      it { is_expected.to be true }
+    subject { helper.show_service_templates_deprecated_callout? }
+
+    where(:self_managed, :is_admin_user, :has_active_service_template, :callout_dismissed, :should_show_callout) do
+      true  | true  | true  | false | true
+      true  | true  | true  | true  | false
+      true  | false | true  | false | false
+      false | true  | true  | false | false
+      true  | true  | false | false | false
     end
 
-    context 'when user dismissed' do
+    with_them do
       before do
-        allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED) { true }
+        allow(::Gitlab).to receive(:com?).and_return(!self_managed)
+        allow(helper).to receive(:current_user).and_return(is_admin_user ? admin : non_admin)
+        allow(helper).to receive(:user_dismissed?).with(described_class::SERVICE_TEMPLATES_DEPRECATED_CALLOUT) { callout_dismissed }
+        create(:service, :template, type: 'MattermostService', active: has_active_service_template)
       end
 
-      it { is_expected.to be false }
+      it { is_expected.to be should_show_callout }
     end
   end
 
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 1a3c332a21bc5b89fb50ac5351e920027f7f60ea..114b3d01952e314c3c058aa8efb123e2a70c4cbb 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -31,6 +31,19 @@ def each
     end
   end
 
+  let(:overflow_max_bytes) { false }
+  let(:overflow_max_files) { false }
+  let(:overflow_max_lines) { false }
+
+  shared_examples 'overflow stuff' do
+    it 'returns the expected overflow values' do
+      subject.overflow?
+      expect(subject.overflow_max_bytes?).to eq(overflow_max_bytes)
+      expect(subject.overflow_max_files?).to eq(overflow_max_files)
+      expect(subject.overflow_max_lines?).to eq(overflow_max_lines)
+    end
+  end
+
   subject do
     Gitlab::Git::DiffCollection.new(
       iterator,
@@ -76,12 +89,19 @@ def each
   end
 
   context 'overflow handling' do
+    subject { super() }
+
+    let(:collapsed_safe_files) { false }
+    let(:collapsed_safe_lines) { false }
+
     context 'adding few enough files' do
       let(:file_count) { 3 }
 
       context 'and few enough lines' do
         let(:line_count) { 10 }
 
+        it_behaves_like 'overflow stuff'
+
         describe '#overflow?' do
           subject { super().overflow? }
 
@@ -117,6 +137,11 @@ def each
 
         context 'when limiting is disabled' do
           let(:limits) { false }
+          let(:overflow_max_bytes) { false }
+          let(:overflow_max_files) { false }
+          let(:overflow_max_lines) { false }
+
+          it_behaves_like 'overflow stuff'
 
           describe '#overflow?' do
             subject { super().overflow? }
@@ -155,6 +180,9 @@ def each
 
       context 'and too many lines' do
         let(:line_count) { 1000 }
+        let(:overflow_max_lines) { true }
+
+        it_behaves_like 'overflow stuff'
 
         describe '#overflow?' do
           subject { super().overflow? }
@@ -184,6 +212,11 @@ def each
 
         context 'when limiting is disabled' do
           let(:limits) { false }
+          let(:overflow_max_bytes) { false }
+          let(:overflow_max_files) { false }
+          let(:overflow_max_lines) { false }
+
+          it_behaves_like 'overflow stuff'
 
           describe '#overflow?' do
             subject { super().overflow? }
@@ -216,10 +249,13 @@ def each
 
     context 'adding too many files' do
       let(:file_count) { 11 }
+      let(:overflow_max_files) { true }
 
       context 'and few enough lines' do
         let(:line_count) { 1 }
 
+        it_behaves_like 'overflow stuff'
+
         describe '#overflow?' do
           subject { super().overflow? }
 
@@ -248,6 +284,11 @@ def each
 
         context 'when limiting is disabled' do
           let(:limits) { false }
+          let(:overflow_max_bytes) { false }
+          let(:overflow_max_files) { false }
+          let(:overflow_max_lines) { false }
+
+          it_behaves_like 'overflow stuff'
 
           describe '#overflow?' do
             subject { super().overflow? }
@@ -279,6 +320,10 @@ def each
 
       context 'and too many lines' do
         let(:line_count) { 30 }
+        let(:overflow_max_lines) { true }
+        let(:overflow_max_files) { false }
+
+        it_behaves_like 'overflow stuff'
 
         describe '#overflow?' do
           subject { super().overflow? }
@@ -308,6 +353,11 @@ def each
 
         context 'when limiting is disabled' do
           let(:limits) { false }
+          let(:overflow_max_bytes) { false }
+          let(:overflow_max_files) { false }
+          let(:overflow_max_lines) { false }
+
+          it_behaves_like 'overflow stuff'
 
           describe '#overflow?' do
             subject { super().overflow? }
@@ -344,6 +394,8 @@ def each
       context 'and few enough lines' do
         let(:line_count) { 1 }
 
+        it_behaves_like 'overflow stuff'
+
         describe '#overflow?' do
           subject { super().overflow? }
 
@@ -375,6 +427,9 @@ def each
     context 'adding too many bytes' do
       let(:file_count) { 10 }
       let(:line_length) { 5200 }
+      let(:overflow_max_bytes) { true }
+
+      it_behaves_like 'overflow stuff'
 
       describe '#overflow?' do
         subject { super().overflow? }
@@ -404,6 +459,11 @@ def each
 
       context 'when limiting is disabled' do
         let(:limits) { false }
+        let(:overflow_max_bytes) { false }
+        let(:overflow_max_files) { false }
+        let(:overflow_max_lines) { false }
+
+        it_behaves_like 'overflow stuff'
 
         describe '#overflow?' do
           subject { super().overflow? }
@@ -437,6 +497,8 @@ def each
   describe 'empty collection' do
     subject { Gitlab::Git::DiffCollection.new([]) }
 
+    it_behaves_like 'overflow stuff'
+
     describe '#overflow?' do
       subject { super().overflow? }
 
@@ -555,7 +617,7 @@ def each
               .and_return({ max_files: 2, max_lines: max_lines })
           end
 
-          it 'prunes diffs by default even little ones' do
+          it 'prunes diffs by default even little ones and sets collapsed_safe_files true' do
             subject.each_with_index do |d, i|
               if i < 2
                 expect(d.diff).not_to eq('')
@@ -563,6 +625,8 @@ def each
                 expect(d.diff).to eq('')
               end
             end
+
+            expect(subject.collapsed_safe_files?).to eq(true)
           end
         end
 
@@ -582,7 +646,7 @@ def each
               .and_return({ max_files: max_files, max_lines: 80 })
           end
 
-          it 'prunes diffs by default even little ones' do
+          it 'prunes diffs by default even little ones and sets collapsed_safe_lines true' do
             subject.each_with_index do |d, i|
               if i < 2
                 expect(d.diff).not_to eq('')
@@ -590,26 +654,30 @@ def each
                 expect(d.diff).to eq('')
               end
             end
+
+            expect(subject.collapsed_safe_lines?).to eq(true)
           end
         end
 
         context 'when go over safe limits on bytes' do
           let(:iterator) do
             [
-              fake_diff(1, 45),
-              fake_diff(1, 45),
-              fake_diff(1, 20480),
-              fake_diff(1, 1)
+              fake_diff(5, 10),
+              fake_diff(5000, 10),
+              fake_diff(5, 10),
+              fake_diff(5, 10)
             ]
           end
 
           before do
+            allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(1.megabyte)
+
             allow(Gitlab::Git::DiffCollection)
               .to receive(:default_limits)
-              .and_return({ max_files: max_files, max_lines: 80 })
+              .and_return({ max_files: 4, max_lines: 3000 })
           end
 
-          it 'prunes diffs by default even little ones' do
+          it 'prunes diffs by default even little ones and sets collapsed_safe_bytes true' do
             subject.each_with_index do |d, i|
               if i < 2
                 expect(d.diff).not_to eq('')
@@ -617,6 +685,8 @@ def each
                 expect(d.diff).to eq('')
               end
             end
+
+            expect(subject.collapsed_safe_bytes?).to eq(true)
           end
         end
       end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 9939e680fa982242836060cb1938d16c381fbc46..6bfcfa2128956563400ab35b23905bb6c840d196 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -124,6 +124,7 @@ def sql(query, comments: true)
 
       with_them do
         let(:payload) { { name: name, sql: sql(sql_query, comments: comments), connection: connection } }
+        let(:record_wal_query) { false }
 
         it 'marks the current thread as using the database' do
           # since it would already have been toggled by other specs
diff --git a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
index e2b36125b4ea3ebb356947649379524168a899b3..82ca84f06974464cb755cb33db4606d44cf20b7d 100644
--- a/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb
@@ -3,156 +3,33 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::SidekiqMiddleware::ClientMetrics do
-  context "with worker attribution" do
-    subject { described_class.new }
+  shared_examples "a metrics middleware" do
+    context "with mocked prometheus" do
+      let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) }
 
-    let(:queue) { :test }
-    let(:worker_class) { worker.class }
-    let(:job) { {} }
-    let(:default_labels) do
-      { queue: queue.to_s,
-        worker: worker_class.to_s,
-        boundary: "",
-        external_dependencies: "no",
-        feature_category: "",
-        urgency: "low" }
-    end
-
-    shared_examples "a metrics client middleware" do
-      context "with mocked prometheus" do
-        let(:enqueued_jobs_metric) { double('enqueued jobs metric', increment: true) }
-
-        before do
-          allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric)
-        end
-
-        describe '#call' do
-          it 'yields block' do
-            expect { |b| subject.call(worker_class, job, :test, double, &b) }.to yield_control.once
-          end
-
-          it 'increments enqueued jobs metric with correct labels when worker is a string of the class' do
-            expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1)
-
-            subject.call(worker_class.to_s, job, :test, double) { nil }
-          end
-
-          it 'increments enqueued jobs metric with correct labels' do
-            expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1)
-
-            subject.call(worker_class, job, :test, double) { nil }
-          end
-        end
-      end
-    end
-
-    context "when workers are not attributed" do
       before do
-        stub_const('TestNonAttributedWorker', Class.new)
-        TestNonAttributedWorker.class_eval do
-          include Sidekiq::Worker
-        end
-      end
-
-      it_behaves_like "a metrics client middleware" do
-        let(:worker) { TestNonAttributedWorker.new }
-        let(:labels) { default_labels.merge(urgency: "") }
-      end
-    end
-
-    context "when a worker is wrapped into ActiveJob" do
-      before do
-        stub_const('TestWrappedWorker', Class.new)
-        TestWrappedWorker.class_eval do
-          include Sidekiq::Worker
-        end
-      end
-
-      it_behaves_like "a metrics client middleware" do
-        let(:job) do
-          {
-            "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
-            "wrapped" => TestWrappedWorker
-          }
-        end
-
-        let(:worker) { TestWrappedWorker.new }
-        let(:labels) { default_labels.merge(urgency: "") }
-      end
-    end
-
-    context "when workers are attributed" do
-      def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
-        klass = Class.new do
-          include Sidekiq::Worker
-          include WorkerAttributes
-
-          urgency urgency if urgency
-          worker_has_external_dependencies! if external_dependencies
-          worker_resource_boundary resource_boundary unless resource_boundary == :unknown
-          feature_category category unless category.nil?
-        end
-        stub_const("TestAttributedWorker", klass)
-      end
-
-      let(:urgency) { nil }
-      let(:external_dependencies) { false }
-      let(:resource_boundary) { :unknown }
-      let(:feature_category) { nil }
-      let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) }
-      let(:worker) { worker_class.new }
-
-      context "high urgency" do
-        it_behaves_like "a metrics client middleware" do
-          let(:urgency) { :high }
-          let(:labels) { default_labels.merge(urgency: "high") }
-        end
+        allow(Gitlab::Metrics).to receive(:counter).with(described_class::ENQUEUED, anything).and_return(enqueued_jobs_metric)
       end
 
-      context "no urgency" do
-        it_behaves_like "a metrics client middleware" do
-          let(:urgency) { :throttled }
-          let(:labels) { default_labels.merge(urgency: "throttled") }
+      describe '#call' do
+        it 'yields block' do
+          expect { |b| subject.call(worker_class, job, :test, double, &b) }.to yield_control.once
         end
-      end
 
-      context "external dependencies" do
-        it_behaves_like "a metrics client middleware" do
-          let(:external_dependencies) { true }
-          let(:labels) { default_labels.merge(external_dependencies: "yes") }
-        end
-      end
+        it 'increments enqueued jobs metric with correct labels when worker is a string of the class' do
+          expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1)
 
-      context "cpu boundary" do
-        it_behaves_like "a metrics client middleware" do
-          let(:resource_boundary) { :cpu }
-          let(:labels) { default_labels.merge(boundary: "cpu") }
+          subject.call(worker_class.to_s, job, :test, double) { nil }
         end
-      end
 
-      context "memory boundary" do
-        it_behaves_like "a metrics client middleware" do
-          let(:resource_boundary) { :memory }
-          let(:labels) { default_labels.merge(boundary: "memory") }
-        end
-      end
+        it 'increments enqueued jobs metric with correct labels' do
+          expect(enqueued_jobs_metric).to receive(:increment).with(labels, 1)
 
-      context "feature category" do
-        it_behaves_like "a metrics client middleware" do
-          let(:feature_category) { :authentication }
-          let(:labels) { default_labels.merge(feature_category: "authentication") }
-        end
-      end
-
-      context "combined" do
-        it_behaves_like "a metrics client middleware" do
-          let(:urgency) { :high }
-          let(:external_dependencies) { true }
-          let(:resource_boundary) { :cpu }
-          let(:feature_category) { :authentication }
-          let(:labels) { default_labels.merge(urgency: "high", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") }
+          subject.call(worker_class, job, :test, double) { nil }
         end
       end
     end
   end
+
+  it_behaves_like 'metrics middleware with worker attribution'
 end
diff --git a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
index 2d493a414994a7e843fdf5e2cf6b250f1d354eed..eb9ba50cdcd7d4903ccb21465829a4d0b0be47e9 100644
--- a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
@@ -35,7 +35,6 @@ def perform(*args)
         :elasticsearch_calls,
         :elasticsearch_duration_s,
         :elasticsearch_timed_out_count,
-        :worker_data_consistency,
         :mem_objects,
         :mem_bytes,
         :mem_mallocs,
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index df273f2598dce579792cbc78a12733363b0475e4..95be76ce351a07a84c81d9615a7b977c9cbc98f1 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -4,309 +4,108 @@
 
 # rubocop: disable RSpec/MultipleMemoizedHelpers
 RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
-  context "with worker attribution" do
-    subject { described_class.new }
+  shared_examples "a metrics middleware" do
+    context "with mocked prometheus" do
+      include_context 'server metrics with mocked prometheus'
 
-    let(:queue) { :test }
-    let(:worker_class) { worker.class }
-    let(:job) { {} }
-    let(:job_status) { :done }
-    let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
-    let(:default_labels) do
-      { queue: queue.to_s,
-        worker: worker_class.to_s,
-        boundary: "",
-        external_dependencies: "no",
-        feature_category: "",
-        urgency: "low" }
-    end
-
-    shared_examples "a metrics middleware" do
-      context "with mocked prometheus" do
-        let(:concurrency_metric) { double('concurrency metric') }
-
-        let(:queue_duration_seconds) { double('queue duration seconds metric') }
-        let(:completion_seconds_metric) { double('completion seconds metric') }
-        let(:user_execution_seconds_metric) { double('user execution seconds metric') }
-        let(:db_seconds_metric) { double('db seconds metric') }
-        let(:gitaly_seconds_metric) { double('gitaly seconds metric') }
-        let(:failed_total_metric) { double('failed total metric') }
-        let(:retried_total_metric) { double('retried total metric') }
-        let(:redis_requests_total) { double('redis calls total metric') }
-        let(:running_jobs_metric) { double('running jobs metric') }
-        let(:redis_seconds_metric) { double('redis seconds metric') }
-        let(:elasticsearch_seconds_metric) { double('elasticsearch seconds metric') }
-        let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') }
+      describe '#initialize' do
+        it 'sets concurrency metrics' do
+          expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
 
-        before do
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds)
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_db_seconds, anything, anything, anything).and_return(db_seconds_metric)
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything).and_return(gitaly_seconds_metric)
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_redis_requests_duration_seconds, anything, anything, anything).and_return(redis_seconds_metric)
-          allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_elasticsearch_requests_duration_seconds, anything, anything, anything).and_return(elasticsearch_seconds_metric)
-          allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
-          allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
-          allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total)
-          allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total)
-          allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
-          allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
-
-          allow(concurrency_metric).to receive(:set)
+          subject
         end
+      end
 
-        describe '#initialize' do
-          it 'sets concurrency metrics' do
-            expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
+      describe '#call' do
+        include_context 'server metrics call'
 
-            subject
-          end
+        it 'yields block' do
+          expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
         end
 
-        describe '#call' do
-          let(:thread_cputime_before) { 1 }
-          let(:thread_cputime_after) { 2 }
-          let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before }
-
-          let(:monotonic_time_before) { 11 }
-          let(:monotonic_time_after) { 20 }
-          let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before }
-
-          let(:queue_duration_for_job) { 0.01 }
-
-          let(:db_duration) { 3 }
-          let(:gitaly_duration) { 4 }
-
-          let(:redis_calls) { 2 }
-          let(:redis_duration) { 0.01 }
-
-          let(:elasticsearch_calls) { 8 }
-          let(:elasticsearch_duration) { 0.54 }
-
-          let(:instrumentation) do
-            {
-              gitaly_duration_s: gitaly_duration,
-              redis_calls: redis_calls,
-              redis_duration_s: redis_duration,
-              elasticsearch_calls: elasticsearch_calls,
-              elasticsearch_duration_s: elasticsearch_duration
-            }
+        it 'calls BackgroundTransaction' do
+          expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |instance|
+            expect(instance).to receive(:run)
           end
 
-          before do
-            allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
-            allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
-            allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
-            allow(ActiveRecord::LogSubscriber).to receive(:runtime).and_return(db_duration * 1000)
-            job[:instrumentation] = instrumentation
-
-            allow(running_jobs_metric).to receive(:increment)
-            allow(redis_requests_total).to receive(:increment)
-            allow(elasticsearch_requests_total).to receive(:increment)
-            allow(queue_duration_seconds).to receive(:observe)
-            allow(user_execution_seconds_metric).to receive(:observe)
-            allow(db_seconds_metric).to receive(:observe)
-            allow(gitaly_seconds_metric).to receive(:observe)
-            allow(completion_seconds_metric).to receive(:observe)
-            allow(redis_seconds_metric).to receive(:observe)
-            allow(elasticsearch_seconds_metric).to receive(:observe)
-          end
-
-          it 'yields block' do
-            expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
-          end
+          subject.call(worker, job, :test) {}
+        end
 
-          it 'calls BackgroundTransaction' do
-            expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |instance|
-              expect(instance).to receive(:run)
-            end
+        it 'sets queue specific metrics' do
+          expect(running_jobs_metric).to receive(:increment).with(labels, -1)
+          expect(running_jobs_metric).to receive(:increment).with(labels, 1)
+          expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job
+          expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration)
+          expect(db_seconds_metric).to receive(:observe).with(labels_with_job_status, db_duration)
+          expect(gitaly_seconds_metric).to receive(:observe).with(labels_with_job_status, gitaly_duration)
+          expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration)
+          expect(redis_seconds_metric).to receive(:observe).with(labels_with_job_status, redis_duration)
+          expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration)
+          expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls)
+          expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls)
+
+          subject.call(worker, job, :test) { nil }
+        end
 
-            subject.call(worker, job, :test) {}
-          end
+        it 'sets the thread name if it was nil' do
+          allow(Thread.current).to receive(:name).and_return(nil)
+          expect(Thread.current).to receive(:name=).with(Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME)
 
-          it 'sets queue specific metrics' do
-            expect(running_jobs_metric).to receive(:increment).with(labels, -1)
-            expect(running_jobs_metric).to receive(:increment).with(labels, 1)
-            expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job
-            expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration)
-            expect(db_seconds_metric).to receive(:observe).with(labels_with_job_status, db_duration)
-            expect(gitaly_seconds_metric).to receive(:observe).with(labels_with_job_status, gitaly_duration)
-            expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration)
-            expect(redis_seconds_metric).to receive(:observe).with(labels_with_job_status, redis_duration)
-            expect(elasticsearch_seconds_metric).to receive(:observe).with(labels_with_job_status, elasticsearch_duration)
-            expect(redis_requests_total).to receive(:increment).with(labels_with_job_status, redis_calls)
-            expect(elasticsearch_requests_total).to receive(:increment).with(labels_with_job_status, elasticsearch_calls)
+          subject.call(worker, job, :test) { nil }
+        end
 
-            subject.call(worker, job, :test) { nil }
-          end
+        context 'when job_duration is not available' do
+          let(:queue_duration_for_job) { nil }
 
-          it 'sets the thread name if it was nil' do
-            allow(Thread.current).to receive(:name).and_return(nil)
-            expect(Thread.current).to receive(:name=).with(Gitlab::Metrics::Samplers::ThreadsSampler::SIDEKIQ_WORKER_THREAD_NAME)
+          it 'does not set the queue_duration_seconds histogram' do
+            expect(queue_duration_seconds).not_to receive(:observe)
 
             subject.call(worker, job, :test) { nil }
           end
+        end
 
-          context 'when job_duration is not available' do
-            let(:queue_duration_for_job) { nil }
-
-            it 'does not set the queue_duration_seconds histogram' do
-              expect(queue_duration_seconds).not_to receive(:observe)
-
-              subject.call(worker, job, :test) { nil }
-            end
-          end
-
-          context 'when error is raised' do
-            let(:job_status) { :fail }
-
-            it 'sets sidekiq_jobs_failed_total and reraises' do
-              expect(failed_total_metric).to receive(:increment).with(labels, 1)
-
-              expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
-            end
-          end
-
-          context 'when job is retried' do
-            let(:job) { { 'retry_count' => 1 } }
+        context 'when error is raised' do
+          let(:job_status) { :fail }
 
-            it 'sets sidekiq_jobs_retried_total metric' do
-              expect(retried_total_metric).to receive(:increment)
+          it 'sets sidekiq_jobs_failed_total and reraises' do
+            expect(failed_total_metric).to receive(:increment).with(labels, 1)
 
-              subject.call(worker, job, :test) { nil }
-            end
+            expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
           end
         end
-      end
 
-      context "with prometheus integrated" do
-        describe '#call' do
-          it 'yields block' do
-            expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
-          end
+        context 'when job is retried' do
+          let(:job) { { 'retry_count' => 1 } }
 
-          context 'when error is raised' do
-            let(:job_status) { :fail }
+          it 'sets sidekiq_jobs_retried_total metric' do
+            expect(retried_total_metric).to receive(:increment)
 
-            it 'sets sidekiq_jobs_failed_total and reraises' do
-              expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
-            end
+            subject.call(worker, job, :test) { nil }
           end
         end
       end
     end
 
-    context "when workers are not attributed" do
-      before do
-        stub_const('TestNonAttributedWorker', Class.new)
-        TestNonAttributedWorker.class_eval do
-          include Sidekiq::Worker
+    context "with prometheus integrated" do
+      describe '#call' do
+        it 'yields block' do
+          expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
         end
-      end
 
-      let(:worker) { TestNonAttributedWorker.new }
-      let(:labels) { default_labels.merge(urgency: "") }
+        context 'when error is raised' do
+          let(:job_status) { :fail }
 
-      it_behaves_like "a metrics middleware"
-    end
-
-    context "when a worker is wrapped into ActiveJob" do
-      before do
-        stub_const('TestWrappedWorker', Class.new)
-        TestWrappedWorker.class_eval do
-          include Sidekiq::Worker
+          it 'sets sidekiq_jobs_failed_total and reraises' do
+            expect { subject.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
+          end
         end
       end
-
-      let(:job) do
-        {
-          "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
-          "wrapped" => TestWrappedWorker
-        }
-      end
-
-      let(:worker) { TestWrappedWorker.new }
-      let(:worker_class) { TestWrappedWorker }
-      let(:labels) { default_labels.merge(urgency: "") }
-
-      it_behaves_like "a metrics middleware"
-    end
-
-    context 'for ActionMailer::MailDeliveryJob' do
-      let(:job) { { 'class' => ActionMailer::MailDeliveryJob } }
-      let(:worker) { ActionMailer::MailDeliveryJob.new }
-      let(:worker_class) { ActionMailer::MailDeliveryJob }
-      let(:labels) { default_labels.merge(feature_category: 'issue_tracking') }
-
-      it_behaves_like 'a metrics middleware'
     end
+  end
 
-    context "when workers are attributed" do
-      def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
-        Class.new do
-          include Sidekiq::Worker
-          include WorkerAttributes
-
-          urgency urgency if urgency
-          worker_has_external_dependencies! if external_dependencies
-          worker_resource_boundary resource_boundary unless resource_boundary == :unknown
-          feature_category category unless category.nil?
-        end
-      end
-
-      let(:urgency) { nil }
-      let(:external_dependencies) { false }
-      let(:resource_boundary) { :unknown }
-      let(:feature_category) { nil }
-      let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) }
-      let(:worker) { worker_class.new }
-
-      context "high urgency" do
-        let(:urgency) { :high }
-        let(:labels) { default_labels.merge(urgency: "high") }
-
-        it_behaves_like "a metrics middleware"
-      end
-
-      context "external dependencies" do
-        let(:external_dependencies) { true }
-        let(:labels) { default_labels.merge(external_dependencies: "yes") }
-
-        it_behaves_like "a metrics middleware"
-      end
-
-      context "cpu boundary" do
-        let(:resource_boundary) { :cpu }
-        let(:labels) { default_labels.merge(boundary: "cpu") }
-
-        it_behaves_like "a metrics middleware"
-      end
-
-      context "memory boundary" do
-        let(:resource_boundary) { :memory }
-        let(:labels) { default_labels.merge(boundary: "memory") }
-
-        it_behaves_like "a metrics middleware"
-      end
-
-      context "feature category" do
-        let(:feature_category) { :authentication }
-        let(:labels) { default_labels.merge(feature_category: "authentication") }
-
-        it_behaves_like "a metrics middleware"
-      end
-
-      context "combined" do
-        let(:urgency) { :throttled }
-        let(:external_dependencies) { true }
-        let(:resource_boundary) { :cpu }
-        let(:feature_category) { :authentication }
-        let(:labels) { default_labels.merge(urgency: "throttled", external_dependencies: "yes", boundary: "cpu", feature_category: "authentication") }
-
-        it_behaves_like "a metrics middleware"
-      end
-    end
+  it_behaves_like 'metrics middleware with worker attribution' do
+    let(:job_status) { :done }
+    let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
   end
 end
 # rubocop: enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index 755f6004e52ee0b833d576032236df2917cd1a0b..0efdef0c999fac36883f5bd1b46a4f8987dceb0d 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -69,11 +69,13 @@ def perform(_arg)
     shared_examples "a server middleware chain" do
       it "passes through the right server middlewares" do
         enabled_sidekiq_middlewares.each do |middleware|
-          expect_any_instance_of(middleware).to receive(:call).with(*middleware_expected_args).once.and_call_original
+          expect_next_instance_of(middleware) do |middleware_instance|
+            expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original
+          end
         end
 
         disabled_sidekiq_middlewares.each do |middleware|
-          expect_any_instance_of(middleware).not_to receive(:call)
+          expect(middleware).not_to receive(:new)
         end
 
         worker_class.perform_async(*job_args)
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a736d001138848315fc2ed16845c3895f34f4fbb..339dffa507f9f1e5b7d43a7b18d6fb6fdfb371e8 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3758,42 +3758,15 @@ def run_job_without_exception
         subject.drop!
       end
 
-      context 'when async_add_build_failure_todo flag enabled' do
-        it 'creates a todo async', :sidekiq_inline do
-          project.add_developer(user)
-
-          expect_next_instance_of(TodoService) do |todo_service|
-            expect(todo_service)
-              .to receive(:merge_request_build_failed).with(merge_request)
-          end
-
-          subject.drop!
-        end
-
-        it 'does not create a sync todo' do
-          project.add_developer(user)
-
-          expect(TodoService).not_to receive(:new)
-
-          subject.drop!
-        end
-      end
+      it 'creates a todo async', :sidekiq_inline do
+        project.add_developer(user)
 
-      context 'when async_add_build_failure_todo flag disabled' do
-        before do
-          stub_feature_flags(async_add_build_failure_todo: false)
+        expect_next_instance_of(TodoService) do |todo_service|
+          expect(todo_service)
+            .to receive(:merge_request_build_failed).with(merge_request)
         end
 
-        it 'creates a todo sync' do
-          project.add_developer(user)
-
-          expect_next_instance_of(TodoService) do |todo_service|
-            expect(todo_service)
-              .to receive(:merge_request_build_failed).with(merge_request)
-          end
-
-          subject.drop!
-        end
+        subject.drop!
       end
     end
 
diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb
index 85c390156146845cb242c39925f98b1e1af06b7d..0fbb785e2d69fd791552d3e00079f16ffa4bc3bc 100644
--- a/spec/services/award_emojis/add_service_spec.rb
+++ b/spec/services/award_emojis/add_service_spec.rb
@@ -6,6 +6,7 @@
   let_it_be(:user) { create(:user) }
   let_it_be(:project) { create(:project) }
   let_it_be(:awardable) { create(:note, project: project) }
+
   let(:name) { 'thumbsup' }
 
   subject(:service) { described_class.new(awardable, name, user) }
diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb
index 2aba078b63885e98f745a21d9d4d3cdec8868b9d..f743de7c59e9811526b319b6a5d0ff425e44dbfe 100644
--- a/spec/services/award_emojis/destroy_service_spec.rb
+++ b/spec/services/award_emojis/destroy_service_spec.rb
@@ -6,6 +6,7 @@
   let_it_be(:user) { create(:user) }
   let_it_be(:awardable) { create(:note) }
   let_it_be(:project) { awardable.project }
+
   let(:name) { 'thumbsup' }
   let!(:award_from_other_user) do
     create(:award_emoji, name: name, awardable: awardable, user: create(:user))
diff --git a/spec/services/award_emojis/toggle_service_spec.rb b/spec/services/award_emojis/toggle_service_spec.rb
index a7feeed50c6de260d0d27a083311c5877eb2da56..74e97c66193d48e7023d79527f01cbc28f4a118d 100644
--- a/spec/services/award_emojis/toggle_service_spec.rb
+++ b/spec/services/award_emojis/toggle_service_spec.rb
@@ -6,6 +6,7 @@
   let_it_be(:user) { create(:user) }
   let_it_be(:project) { create(:project, :public) }
   let_it_be(:awardable) { create(:note, project: project) }
+
   let(:name) { 'thumbsup' }
 
   subject(:service) { described_class.new(awardable, name, user) }
diff --git a/spec/services/environments/auto_stop_service_spec.rb b/spec/services/environments/auto_stop_service_spec.rb
index 8e56c7e642cc692ee980de80f0623f572bee0af9..93b1596586f1407daeff64b57eed02a1f45af217 100644
--- a/spec/services/environments/auto_stop_service_spec.rb
+++ b/spec/services/environments/auto_stop_service_spec.rb
@@ -8,6 +8,7 @@
 
   let_it_be(:project) { create(:project, :repository) }
   let_it_be(:user) { create(:user) }
+
   let(:service) { described_class.new }
 
   before_all do
@@ -19,6 +20,7 @@
 
     let_it_be(:project) { create(:project, :repository) }
     let_it_be(:user) { create(:user) }
+
     let(:environments) { Environment.all }
 
     before_all do
diff --git a/spec/services/environments/canary_ingress/update_service_spec.rb b/spec/services/environments/canary_ingress/update_service_spec.rb
index 5ba62e7104cfe59bf7f951df74f2581c516d92c6..0e72fff1ed2dec3034cc9fa3eed2ee16c529e61b 100644
--- a/spec/services/environments/canary_ingress/update_service_spec.rb
+++ b/spec/services/environments/canary_ingress/update_service_spec.rb
@@ -8,6 +8,7 @@
   let_it_be(:project, refind: true) { create(:project) }
   let_it_be(:maintainer) { create(:user) }
   let_it_be(:reporter) { create(:user) }
+
   let(:user) { maintainer }
   let(:params) { {} }
   let(:service) { described_class.new(project, user, params) }
diff --git a/spec/services/environments/reset_auto_stop_service_spec.rb b/spec/services/environments/reset_auto_stop_service_spec.rb
index cab1bf2cc26b32c8e4588db53ddd4329800abbde..4a0b091c12d5e05e406909fc05fce7b1bcc1b8f4 100644
--- a/spec/services/environments/reset_auto_stop_service_spec.rb
+++ b/spec/services/environments/reset_auto_stop_service_spec.rb
@@ -6,6 +6,7 @@
   let_it_be(:project) { create(:project) }
   let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
   let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
+
   let(:user) { developer }
   let(:service) { described_class.new(project, user) }
 
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index dd68471d9274e93b17446e66b2d8c69c955eca69..6c08b7db43a0d05d053f76e7f72f703bed8dd5b8 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -22,14 +22,16 @@ def service
       expect { milestone.reload }.to raise_error ActiveRecord::RecordNotFound
     end
 
-    it 'deletes milestone id from issuables' do
-      issue = create(:issue, project: project, milestone: milestone)
-      merge_request = create(:merge_request, source_project: project, milestone: milestone)
+    context 'with an existing merge request' do
+      let!(:issue) { create(:issue, project: project, milestone: milestone) }
+      let!(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
 
-      service.execute(milestone)
+      it 'deletes milestone id from issuables' do
+        service.execute(milestone)
 
-      expect(issue.reload.milestone).to be_nil
-      expect(merge_request.reload.milestone).to be_nil
+        expect(issue.reload.milestone).to be_nil
+        expect(merge_request.reload.milestone).to be_nil
+      end
     end
 
     it 'logs destroy event' do
diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
new file mode 100644
index 0000000000000000000000000000000000000000..73de631e2939f93c951b3701b28767fc5d76fff5
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'server metrics with mocked prometheus' do
+  let(:concurrency_metric) { double('concurrency metric') }
+
+  let(:queue_duration_seconds) { double('queue duration seconds metric') }
+  let(:completion_seconds_metric) { double('completion seconds metric') }
+  let(:user_execution_seconds_metric) { double('user execution seconds metric') }
+  let(:db_seconds_metric) { double('db seconds metric') }
+  let(:gitaly_seconds_metric) { double('gitaly seconds metric') }
+  let(:failed_total_metric) { double('failed total metric') }
+  let(:retried_total_metric) { double('retried total metric') }
+  let(:redis_requests_total) { double('redis calls total metric') }
+  let(:running_jobs_metric) { double('running jobs metric') }
+  let(:redis_seconds_metric) { double('redis seconds metric') }
+  let(:elasticsearch_seconds_metric) { double('elasticsearch seconds metric') }
+  let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') }
+
+  before do
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds)
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_db_seconds, anything, anything, anything).and_return(db_seconds_metric)
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything).and_return(gitaly_seconds_metric)
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_redis_requests_duration_seconds, anything, anything, anything).and_return(redis_seconds_metric)
+    allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_elasticsearch_requests_duration_seconds, anything, anything, anything).and_return(elasticsearch_seconds_metric)
+    allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
+    allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
+    allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total)
+    allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total)
+    allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
+    allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
+
+    allow(concurrency_metric).to receive(:set)
+  end
+end
+
+RSpec.shared_context 'server metrics call' do
+  let(:thread_cputime_before) { 1 }
+  let(:thread_cputime_after) { 2 }
+  let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before }
+
+  let(:monotonic_time_before) { 11 }
+  let(:monotonic_time_after) { 20 }
+  let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before }
+
+  let(:queue_duration_for_job) { 0.01 }
+
+  let(:db_duration) { 3 }
+  let(:gitaly_duration) { 4 }
+
+  let(:redis_calls) { 2 }
+  let(:redis_duration) { 0.01 }
+
+  let(:elasticsearch_calls) { 8 }
+  let(:elasticsearch_duration) { 0.54 }
+  let(:instrumentation) do
+    {
+      gitaly_duration_s: gitaly_duration,
+      redis_calls: redis_calls,
+      redis_duration_s: redis_duration,
+      elasticsearch_calls: elasticsearch_calls,
+      elasticsearch_duration_s: elasticsearch_duration
+    }
+  end
+
+  before do
+    allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
+    allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
+    allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
+    allow(ActiveRecord::LogSubscriber).to receive(:runtime).and_return(db_duration * 1000)
+
+    job[:instrumentation] = instrumentation
+    job[:gitaly_duration_s] = gitaly_duration
+    job[:redis_calls] = redis_calls
+    job[:redis_duration_s] = redis_duration
+
+    job[:elasticsearch_calls] = elasticsearch_calls
+    job[:elasticsearch_duration_s] = elasticsearch_duration
+
+    allow(running_jobs_metric).to receive(:increment)
+    allow(redis_requests_total).to receive(:increment)
+    allow(elasticsearch_requests_total).to receive(:increment)
+    allow(queue_duration_seconds).to receive(:observe)
+    allow(user_execution_seconds_metric).to receive(:observe)
+    allow(db_seconds_metric).to receive(:observe)
+    allow(gitaly_seconds_metric).to receive(:observe)
+    allow(completion_seconds_metric).to receive(:observe)
+    allow(redis_seconds_metric).to receive(:observe)
+    allow(elasticsearch_seconds_metric).to receive(:observe)
+  end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..48dc47e8e9b8e993040b294d6d121d4757ef7b38
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/metrics_middleware_with_worker_attribution_shared_examples.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'metrics middleware with worker attribution' do
+  subject { described_class.new }
+
+  let(:queue) { :test }
+  let(:worker_class) { worker.class }
+  let(:job) { {} }
+  let(:default_labels) do
+    { queue: queue.to_s,
+      worker: worker_class.to_s,
+      boundary: "",
+      external_dependencies: "no",
+      feature_category: "",
+      urgency: "low" }
+  end
+
+  context "when workers are not attributed" do
+    before do
+      stub_const('TestNonAttributedWorker', Class.new)
+      TestNonAttributedWorker.class_eval do
+        include Sidekiq::Worker
+      end
+    end
+
+    it_behaves_like "a metrics middleware" do
+      let(:worker) { TestNonAttributedWorker.new }
+      let(:labels) { default_labels.merge(urgency: "") }
+    end
+  end
+
+  context "when a worker is wrapped into ActiveJob" do
+    before do
+      stub_const('TestWrappedWorker', Class.new)
+      TestWrappedWorker.class_eval do
+        include Sidekiq::Worker
+      end
+    end
+
+    it_behaves_like "a metrics middleware" do
+      let(:job) do
+        {
+          "class" => ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper,
+          "wrapped" => TestWrappedWorker
+        }
+      end
+
+      let(:worker) { TestWrappedWorker.new }
+      let(:labels) { default_labels.merge(urgency: "") }
+    end
+  end
+
+  context "when workers are attributed" do
+    def create_attributed_worker_class(urgency, external_dependencies, resource_boundary, category)
+      klass = Class.new do
+        include Sidekiq::Worker
+        include WorkerAttributes
+
+        urgency urgency if urgency
+        worker_has_external_dependencies! if external_dependencies
+        worker_resource_boundary resource_boundary unless resource_boundary == :unknown
+        feature_category category unless category.nil?
+      end
+      stub_const("TestAttributedWorker", klass)
+    end
+
+    let(:urgency) { nil }
+    let(:external_dependencies) { false }
+    let(:resource_boundary) { :unknown }
+    let(:feature_category) { nil }
+    let(:worker_class) { create_attributed_worker_class(urgency, external_dependencies, resource_boundary, feature_category) }
+    let(:worker) { worker_class.new }
+
+    context "high urgency" do
+      it_behaves_like "a metrics middleware" do
+        let(:urgency) { :high }
+        let(:labels) { default_labels.merge(urgency: "high") }
+      end
+    end
+
+    context "no urgency" do
+      it_behaves_like "a metrics middleware" do
+        let(:urgency) { :throttled }
+        let(:labels) { default_labels.merge(urgency: "throttled") }
+      end
+    end
+
+    context "external dependencies" do
+      it_behaves_like "a metrics middleware" do
+        let(:external_dependencies) { true }
+        let(:labels) { default_labels.merge(external_dependencies: "yes") }
+      end
+    end
+
+    context "cpu boundary" do
+      it_behaves_like "a metrics middleware" do
+        let(:resource_boundary) { :cpu }
+        let(:labels) { default_labels.merge(boundary: "cpu") }
+      end
+    end
+
+    context "memory boundary" do
+      it_behaves_like "a metrics middleware" do
+        let(:resource_boundary) { :memory }
+        let(:labels) { default_labels.merge(boundary: "memory") }
+      end
+    end
+
+    context "feature category" do
+      it_behaves_like "a metrics middleware" do
+        let(:feature_category) { :authentication }
+        let(:labels) { default_labels.merge(feature_category: "authentication") }
+      end
+    end
+
+    context "combined" do
+      it_behaves_like "a metrics middleware" do
+        let(:urgency) { :high }
+        let(:external_dependencies) { true }
+        let(:resource_boundary) { :cpu }
+        let(:feature_category) { :authentication }
+        let(:labels) do
+          default_labels.merge(
+            urgency: "high",
+            external_dependencies: "yes",
+            boundary: "cpu",
+            feature_category: "authentication")
+        end
+      end
+    end
+  end
+end
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
index 7bf2456c5480202fe35f47b2dba82e03663a7666..1b110ab02b58a190aa072f8bdced96840e246466 100644
--- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -16,7 +16,9 @@
             db_primary_duration_s:  record_query ? 0.002 : 0,
             db_replica_cached_count:  0,
             db_replica_count:  0,
-            db_replica_duration_s:  0.0
+            db_replica_duration_s:  0.0,
+            db_primary_wal_count: record_wal_query ? 1 : 0,
+            db_replica_wal_count: 0
           )
         elsif db_role == :replica
           expect(described_class.db_counter_payload).to eq(
@@ -28,7 +30,9 @@
             db_primary_duration_s:  0.0,
             db_replica_cached_count:  record_cached_query ? 1 : 0,
             db_replica_count:  record_query ? 1 : 0,
-            db_replica_duration_s:  record_query ? 0.002 : 0
+            db_replica_duration_s:  record_query ? 0.002 : 0,
+            db_replica_wal_count: record_wal_query ? 1 : 0,
+            db_primary_wal_count: 0
           )
         else
           expect(described_class.db_counter_payload).to eq(
@@ -66,6 +70,12 @@
       expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role
     end
 
+    if record_wal_query
+      expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role
+    else
+      expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role
+    end
+
     subscriber.sql(event)
   end
 
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 638bf8ab12b64d066931697b03ac599caabc70a4..5aca5d686770ca38d8b26232adf4a62b43f0a1c2 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -39,18 +39,6 @@
 
           subject
         end
-
-        context 'when async_add_build_failure_todo disabled' do
-          before do
-            stub_feature_flags(async_add_build_failure_todo: false)
-          end
-
-          it 'does not add a todo' do
-            expect(::Ci::MergeRequests::AddTodoWhenBuildFailsWorker).not_to receive(:perform_async)
-
-            subject
-          end
-        end
       end
 
       context 'when build has a chat' do
diff --git a/workhorse/.gitlab-ci.yml b/workhorse/.gitlab-ci.yml
index 69f25fde9689fe0719fe995c5121033e8d9f59cc..60d51b868b70a55af80477066b16b245261e3b3b 100644
--- a/workhorse/.gitlab-ci.yml
+++ b/workhorse/.gitlab-ci.yml
@@ -10,7 +10,7 @@ workflow:
     - if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable$/'
 
 default:
-  image: golang:1.13
+  image: golang:1.16
   tags:
     - gitlab-org
 
@@ -41,18 +41,14 @@ changelog:
   - apt-get update && apt-get -y install libimage-exiftool-perl
   - make test
 
-test using go 1.13:
-  extends: .test
-  image: golang:1.13
-
-test using go 1.14:
-  extends: .test
-  image: golang:1.14
-
 test using go 1.15:
   extends: .test
   image: golang:1.15
 
+test using go 1.16:
+  extends: .test
+  image: golang:1.16
+
 test:release:
   rules:
     - if: '$CI_COMMIT_TAG'
diff --git a/workhorse/doc/operations/install.md b/workhorse/doc/operations/install.md
index 28efc407515797d1cf09a3cd6802345226b8565c..3bee13e26839b791e8205ea580500cd1ea224858 100644
--- a/workhorse/doc/operations/install.md
+++ b/workhorse/doc/operations/install.md
@@ -1,6 +1,6 @@
 # Installation
 
-To install GitLab Workhorse you need [Go 1.13 or
+To install GitLab Workhorse you need [Go 1.15 or
 newer](https://golang.org/dl) and [GNU
 Make](https://www.gnu.org/software/make/).
 
diff --git a/workhorse/go.mod b/workhorse/go.mod
index ee50ed690aa458225989db0a8974b59cbb4ff375..e565feef37da6e5b0b949eda2d232e310f8b1e33 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -1,6 +1,6 @@
 module gitlab.com/gitlab-org/gitlab-workhorse
 
-go 1.13
+go 1.15
 
 require (
 	github.com/Azure/azure-storage-blob-go v0.11.1-0.20201209121048-6df5d9af221d
@@ -34,13 +34,12 @@ require (
 	golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
 	golang.org/x/lint v0.0.0-20200302205851-738671d3881b
 	golang.org/x/net v0.0.0-20201224014010-6772e930b67b
-	golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 // indirect
 	golang.org/x/text v0.3.5 // indirect
-	golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516
+	golang.org/x/tools v0.1.0
 	google.golang.org/genproto v0.0.0-20210111234610-22ae2b108f89 // indirect
 	google.golang.org/grpc v1.34.1
 	google.golang.org/grpc/examples v0.0.0-20201226181154-53788aa5dcb4 // indirect
-	honnef.co/go/tools v0.0.1-2020.1.5
+	honnef.co/go/tools v0.1.3
 )
 
 // go get tries to enforce semantic version compatibility via module paths.
diff --git a/workhorse/go.sum b/workhorse/go.sum
index 4796d40638b803da81384e62f38828a203b8337b..260cb9138df3bc9334fd8b1d7cfd39dab8b89abd 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -24,16 +24,13 @@ cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNF
 cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.4.0/go.mod h1:NjjGEnxCS3CAKYp+vmALu20QzcqasGodQp48WxJGAYc=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
 cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
 cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
 cloud.google.com/go/pubsub v1.9.0/go.mod h1:G3o6/kJvEMIEAN5urdkaP4be49WQsjNiykBIto9LFtY=
 cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
@@ -183,7 +180,6 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
-github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
@@ -214,7 +210,6 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
-github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -279,7 +274,6 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
 github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
@@ -308,7 +302,6 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
 github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -340,7 +333,6 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -357,7 +349,6 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -446,7 +437,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
@@ -705,7 +695,6 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
@@ -731,7 +720,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -770,7 +758,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
 golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -811,7 +798,6 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@@ -819,7 +805,6 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
 golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -827,7 +812,6 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
@@ -839,7 +823,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -896,23 +879,19 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk=
 golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061 h1:DQmQoKxQWtyybCtX/3dIuDBcAhFszqq8YiNeS6sNu1c=
-golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -981,11 +960,11 @@ golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4X
 golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516 h1:E8xavSjXY8LFvcMSu/8Fjztt+SerwKnuAUOdS+aCXUM=
 golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1020,7 +999,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@@ -1063,7 +1041,6 @@ google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497 h1:jDYzwXmX9tLnuG4sL85HPmE1ruErXOopALp2i/0AHnI=
 google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20210111234610-22ae2b108f89 h1:R2owLnwrU3BdTJ5R9cnHDNsnEmBQ7n5lZjKShnbISe4=
 google.golang.org/genproto v0.0.0-20210111234610-22ae2b108f89/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
@@ -1082,7 +1059,6 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
@@ -1102,7 +1078,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
 google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
@@ -1111,7 +1086,6 @@ gopkg.in/DataDog/dd-trace-go.v1 v1.7.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzw
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1146,8 +1120,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75k=
-honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
 nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=